Skip to content

Commit 02906ae

Browse files
authored
Merge pull request #180 from projectfluent/hints
Add type hints to fluent.syntax & fluent.runtime
2 parents f52b277 + 96df2e7 commit 02906ae

28 files changed

+712
-494
lines changed

.github/workflows/fluent.runtime.yml

+13-4
Original file line numberDiff line numberDiff line change
@@ -39,26 +39,35 @@ jobs:
3939
run: |
4040
python -m pip install wheel
4141
python -m pip install --upgrade pip
42-
python -m pip install fluent.syntax==${{ matrix.fluent-syntax }}
42+
python -m pip install fluent.syntax==${{ matrix.fluent-syntax }} six
4343
python -m pip install .
4444
- name: Test
4545
working-directory: ./fluent.runtime
4646
run: |
4747
./runtests.py
4848
lint:
49-
name: flake8
5049
runs-on: ubuntu-latest
5150
steps:
5251
- uses: actions/checkout@v3
5352
- uses: actions/setup-python@v4
5453
with:
5554
python-version: 3.9
5655
- name: Install dependencies
56+
working-directory: ./fluent.runtime
5757
run: |
5858
python -m pip install wheel
5959
python -m pip install --upgrade pip
60-
python -m pip install flake8==6
61-
- name: lint
60+
python -m pip install .
61+
python -m pip install flake8==6 mypy==1 types-babel types-pytz
62+
- name: Install latest fluent.syntax
63+
working-directory: ./fluent.syntax
64+
run: |
65+
python -m pip install .
66+
- name: flake8
6267
working-directory: ./fluent.runtime
6368
run: |
6469
python -m flake8
70+
- name: mypy
71+
working-directory: ./fluent.runtime
72+
run: |
73+
python -m mypy fluent/

.github/workflows/fluent.syntax.yml

+10-4
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,29 @@ jobs:
3535
run: |
3636
python -m pip install wheel
3737
python -m pip install --upgrade pip
38+
python -m pip install .
3839
- name: Test
3940
working-directory: ./fluent.syntax
4041
run: |
4142
./runtests.py
42-
syntax:
43-
name: flake8
43+
lint:
4444
runs-on: ubuntu-latest
4545
steps:
4646
- uses: actions/checkout@v3
4747
- uses: actions/setup-python@v4
4848
with:
4949
python-version: 3.9
5050
- name: Install dependencies
51+
working-directory: ./fluent.syntax
5152
run: |
5253
python -m pip install --upgrade pip
53-
python -m pip install flake8==6
54-
- name: lint
54+
python -m pip install .
55+
python -m pip install flake8==6 mypy==1
56+
- name: flake8
5557
working-directory: ./fluent.syntax
5658
run: |
5759
python -m flake8
60+
- name: mypy
61+
working-directory: ./fluent.syntax
62+
run: |
63+
python -m mypy fluent/

fluent.docs/setup.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from setuptools import setup, find_namespace_packages
1+
from setuptools import setup
22

33
setup(
44
name='fluent.docs',
5-
packages=find_namespace_packages(include=['fluent.*']),
5+
packages=['fluent.docs'],
6+
install_requires=[
7+
'typing-extensions>=3.7,<5'
8+
],
69
)
+3-98
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
import babel
2-
import babel.numbers
3-
import babel.plural
4-
51
from fluent.syntax import FluentParser
6-
from fluent.syntax.ast import Message, Term
2+
from fluent.syntax.ast import Resource
73

8-
from .builtins import BUILTINS
9-
from .prepare import Compiler
10-
from .resolver import ResolverEnvironment, CurrentEnvironment
11-
from .utils import native_to_fluent
4+
from .bundle import FluentBundle
125
from .fallback import FluentLocalization, AbstractResourceLoader, FluentResourceLoader
136

147

@@ -21,94 +14,6 @@
2114
]
2215

2316

24-
def FluentResource(source):
17+
def FluentResource(source: str) -> Resource:
2518
parser = FluentParser()
2619
return parser.parse(source)
27-
28-
29-
class FluentBundle:
30-
"""
31-
Bundles are single-language stores of translations. They are
32-
aggregate parsed Fluent resources in the Fluent syntax and can
33-
format translation units (entities) to strings.
34-
35-
Always use `FluentBundle.get_message` to retrieve translation units from
36-
a bundle. Generate the localized string by using `format_pattern` on
37-
`message.value` or `message.attributes['attr']`.
38-
Translations can contain references to other entities or
39-
external arguments, conditional logic in form of select expressions, traits
40-
which describe their grammatical features, and can use Fluent builtins.
41-
See the documentation of the Fluent syntax for more information.
42-
"""
43-
44-
def __init__(self, locales, functions=None, use_isolating=True):
45-
self.locales = locales
46-
_functions = BUILTINS.copy()
47-
if functions:
48-
_functions.update(functions)
49-
self._functions = _functions
50-
self.use_isolating = use_isolating
51-
self._messages = {}
52-
self._terms = {}
53-
self._compiled = {}
54-
self._compiler = Compiler()
55-
self._babel_locale = self._get_babel_locale()
56-
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)
57-
58-
def add_resource(self, resource, allow_overrides=False):
59-
# TODO - warn/error about duplicates
60-
for item in resource.body:
61-
if not isinstance(item, (Message, Term)):
62-
continue
63-
map_ = self._messages if isinstance(item, Message) else self._terms
64-
full_id = item.id.name
65-
if full_id not in map_ or allow_overrides:
66-
map_[full_id] = item
67-
68-
def has_message(self, message_id):
69-
return message_id in self._messages
70-
71-
def get_message(self, message_id):
72-
return self._lookup(message_id)
73-
74-
def _lookup(self, entry_id, term=False):
75-
if term:
76-
compiled_id = '-' + entry_id
77-
else:
78-
compiled_id = entry_id
79-
try:
80-
return self._compiled[compiled_id]
81-
except LookupError:
82-
pass
83-
entry = self._terms[entry_id] if term else self._messages[entry_id]
84-
self._compiled[compiled_id] = self._compiler(entry)
85-
return self._compiled[compiled_id]
86-
87-
def format_pattern(self, pattern, args=None):
88-
if args is not None:
89-
fluent_args = {
90-
argname: native_to_fluent(argvalue)
91-
for argname, argvalue in args.items()
92-
}
93-
else:
94-
fluent_args = {}
95-
96-
errors = []
97-
env = ResolverEnvironment(context=self,
98-
current=CurrentEnvironment(args=fluent_args),
99-
errors=errors)
100-
try:
101-
result = pattern(env)
102-
except ValueError as e:
103-
errors.append(e)
104-
result = '{???}'
105-
return [result, errors]
106-
107-
def _get_babel_locale(self):
108-
for lc in self.locales:
109-
try:
110-
return babel.Locale.parse(lc.replace('-', '_'))
111-
except babel.UnknownLocaleError:
112-
continue
113-
# TODO - log error
114-
return babel.Locale.default()
+3-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from .types import fluent_date, fluent_number
1+
from typing import Any, Callable, Dict
2+
from .types import FluentType, fluent_date, fluent_number
23

34
NUMBER = fluent_number
45
DATETIME = fluent_date
56

67

7-
BUILTINS = {
8+
BUILTINS: Dict[str, Callable[[Any], FluentType]] = {
89
'NUMBER': NUMBER,
910
'DATETIME': DATETIME,
1011
}
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import babel
2+
import babel.numbers
3+
import babel.plural
4+
from typing import Any, Callable, Dict, List, TYPE_CHECKING, Tuple, Union, cast
5+
from typing_extensions import Literal
6+
7+
from fluent.syntax import ast as FTL
8+
9+
from .builtins import BUILTINS
10+
from .prepare import Compiler
11+
from .resolver import CurrentEnvironment, Message, Pattern, ResolverEnvironment
12+
from .utils import native_to_fluent
13+
14+
if TYPE_CHECKING:
15+
from .types import FluentNone, FluentType
16+
17+
PluralCategory = Literal['zero', 'one', 'two', 'few', 'many', 'other']
18+
19+
20+
class FluentBundle:
21+
"""
22+
Bundles are single-language stores of translations. They are
23+
aggregate parsed Fluent resources in the Fluent syntax and can
24+
format translation units (entities) to strings.
25+
26+
Always use `FluentBundle.get_message` to retrieve translation units from
27+
a bundle. Generate the localized string by using `format_pattern` on
28+
`message.value` or `message.attributes['attr']`.
29+
Translations can contain references to other entities or
30+
external arguments, conditional logic in form of select expressions, traits
31+
which describe their grammatical features, and can use Fluent builtins.
32+
See the documentation of the Fluent syntax for more information.
33+
"""
34+
35+
def __init__(self,
36+
locales: List[str],
37+
functions: Union[Dict[str, Callable[[Any], 'FluentType']], None] = None,
38+
use_isolating: bool = True):
39+
self.locales = locales
40+
self._functions = {**BUILTINS, **(functions or {})}
41+
self.use_isolating = use_isolating
42+
self._messages: Dict[str, Union[FTL.Message, FTL.Term]] = {}
43+
self._terms: Dict[str, Union[FTL.Message, FTL.Term]] = {}
44+
self._compiled: Dict[str, Message] = {}
45+
# The compiler is not typed, and this cast is only valid for the public API
46+
self._compiler = cast(Callable[[Union[FTL.Message, FTL.Term]], Message], Compiler())
47+
self._babel_locale = self._get_babel_locale()
48+
self._plural_form = cast(Callable[[Any], Callable[[Union[int, float]], PluralCategory]],
49+
babel.plural.to_python)(self._babel_locale.plural_form)
50+
51+
def add_resource(self, resource: FTL.Resource, allow_overrides: bool = False) -> None:
52+
# TODO - warn/error about duplicates
53+
for item in resource.body:
54+
if not isinstance(item, (FTL.Message, FTL.Term)):
55+
continue
56+
map_ = self._messages if isinstance(item, FTL.Message) else self._terms
57+
full_id = item.id.name
58+
if full_id not in map_ or allow_overrides:
59+
map_[full_id] = item
60+
61+
def has_message(self, message_id: str) -> bool:
62+
return message_id in self._messages
63+
64+
def get_message(self, message_id: str) -> Message:
65+
return self._lookup(message_id)
66+
67+
def _lookup(self, entry_id: str, term: bool = False) -> Message:
68+
if term:
69+
compiled_id = '-' + entry_id
70+
else:
71+
compiled_id = entry_id
72+
try:
73+
return self._compiled[compiled_id]
74+
except LookupError:
75+
pass
76+
entry = self._terms[entry_id] if term else self._messages[entry_id]
77+
self._compiled[compiled_id] = self._compiler(entry)
78+
return self._compiled[compiled_id]
79+
80+
def format_pattern(self,
81+
pattern: Pattern,
82+
args: Union[Dict[str, Any], None] = None
83+
) -> Tuple[Union[str, 'FluentNone'], List[Exception]]:
84+
if args is not None:
85+
fluent_args = {
86+
argname: native_to_fluent(argvalue)
87+
for argname, argvalue in args.items()
88+
}
89+
else:
90+
fluent_args = {}
91+
92+
errors: List[Exception] = []
93+
env = ResolverEnvironment(context=self,
94+
current=CurrentEnvironment(args=fluent_args),
95+
errors=errors)
96+
try:
97+
result = pattern(env)
98+
except ValueError as e:
99+
errors.append(e)
100+
result = '{???}'
101+
return (result, errors)
102+
103+
def _get_babel_locale(self) -> babel.Locale:
104+
for lc in self.locales:
105+
try:
106+
return babel.Locale.parse(lc.replace('-', '_'))
107+
except babel.UnknownLocaleError:
108+
continue
109+
# TODO - log error
110+
return babel.Locale.default()

fluent.runtime/fluent/runtime/errors.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from typing import cast
2+
3+
14
class FluentFormatError(ValueError):
2-
def __eq__(self, other):
3-
return ((other.__class__ == self.__class__) and
4-
other.args == self.args)
5+
def __eq__(self, other: object) -> bool:
6+
return ((other.__class__ == self.__class__) and cast(ValueError, other).args == self.args)
57

68

79
class FluentReferenceError(FluentFormatError):

0 commit comments

Comments
 (0)