Skip to content

Commit f3a629f

Browse files
committed
runtime: Add type hints
1 parent f148e8e commit f3a629f

File tree

9 files changed

+322
-219
lines changed

9 files changed

+322
-219
lines changed

fluent.runtime/fluent/runtime/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fluent.syntax import FluentParser
2+
from fluent.syntax.ast import Resource
23

34
from .bundle import FluentBundle
45
from .fallback import FluentLocalization, AbstractResourceLoader, FluentResourceLoader
@@ -13,6 +14,6 @@
1314
]
1415

1516

16-
def FluentResource(source):
17+
def FluentResource(source: str) -> Resource:
1718
parser = FluentParser()
1819
return parser.parse(source)
Lines changed: 3 additions & 2 deletions
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
}

fluent.runtime/fluent/runtime/bundle.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import babel
22
import babel.numbers
33
import babel.plural
4+
from typing import Any, Callable, Dict, List, Literal, TYPE_CHECKING, Tuple, Union, cast
45

5-
from fluent.syntax.ast import Message, Term
6+
from fluent.syntax import ast as FTL
67

78
from .builtins import BUILTINS
89
from .prepare import Compiler
9-
from .resolver import ResolverEnvironment, CurrentEnvironment
10+
from .resolver import CurrentEnvironment, Message, Pattern, ResolverEnvironment
1011
from .utils import native_to_fluent
1112

13+
if TYPE_CHECKING:
14+
from .types import FluentNone, FluentType
15+
16+
PluralCategory = Literal['zero', 'one', 'two', 'few', 'many', 'other']
17+
1218

1319
class FluentBundle:
1420
"""
@@ -25,37 +31,42 @@ class FluentBundle:
2531
See the documentation of the Fluent syntax for more information.
2632
"""
2733

28-
def __init__(self, locales, functions=None, use_isolating=True):
34+
def __init__(self,
35+
locales: List[str],
36+
functions: Union[Dict[str, Callable[[Any], 'FluentType']], None] = None,
37+
use_isolating: bool = True):
2938
self.locales = locales
3039
_functions = BUILTINS.copy()
3140
if functions:
3241
_functions.update(functions)
3342
self._functions = _functions
3443
self.use_isolating = use_isolating
35-
self._messages = {}
36-
self._terms = {}
37-
self._compiled = {}
38-
self._compiler = Compiler()
44+
self._messages: Dict[str, Union[FTL.Message, FTL.Term]] = {}
45+
self._terms: Dict[str, Union[FTL.Message, FTL.Term]] = {}
46+
self._compiled: Dict[str, Message] = {}
47+
# The compiler is not typed, and this cast is only valid for the public API
48+
self._compiler = cast(Callable[[Union[FTL.Message, FTL.Term]], Message], Compiler())
3949
self._babel_locale = self._get_babel_locale()
40-
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)
50+
self._plural_form = cast(Callable[[Union[int, float]], PluralCategory],
51+
babel.plural.to_python(self._babel_locale.plural_form))
4152

42-
def add_resource(self, resource, allow_overrides=False):
53+
def add_resource(self, resource: FTL.Resource, allow_overrides: bool = False) -> None:
4354
# TODO - warn/error about duplicates
4455
for item in resource.body:
45-
if not isinstance(item, (Message, Term)):
56+
if not isinstance(item, (FTL.Message, FTL.Term)):
4657
continue
47-
map_ = self._messages if isinstance(item, Message) else self._terms
58+
map_ = self._messages if isinstance(item, FTL.Message) else self._terms
4859
full_id = item.id.name
4960
if full_id not in map_ or allow_overrides:
5061
map_[full_id] = item
5162

52-
def has_message(self, message_id):
63+
def has_message(self, message_id: str) -> bool:
5364
return message_id in self._messages
5465

55-
def get_message(self, message_id):
66+
def get_message(self, message_id: str) -> Message:
5667
return self._lookup(message_id)
5768

58-
def _lookup(self, entry_id, term=False):
69+
def _lookup(self, entry_id: str, term: bool = False) -> Message:
5970
if term:
6071
compiled_id = '-' + entry_id
6172
else:
@@ -68,7 +79,10 @@ def _lookup(self, entry_id, term=False):
6879
self._compiled[compiled_id] = self._compiler(entry)
6980
return self._compiled[compiled_id]
7081

71-
def format_pattern(self, pattern, args=None):
82+
def format_pattern(self,
83+
pattern: Pattern,
84+
args: Union[Dict[str, Any], None] = None
85+
) -> Tuple[Union[str, 'FluentNone'], List[Exception]]:
7286
if args is not None:
7387
fluent_args = {
7488
argname: native_to_fluent(argvalue)
@@ -77,7 +91,7 @@ def format_pattern(self, pattern, args=None):
7791
else:
7892
fluent_args = {}
7993

80-
errors = []
94+
errors: List[Exception] = []
8195
env = ResolverEnvironment(context=self,
8296
current=CurrentEnvironment(args=fluent_args),
8397
errors=errors)
@@ -86,9 +100,9 @@ def format_pattern(self, pattern, args=None):
86100
except ValueError as e:
87101
errors.append(e)
88102
result = '{???}'
89-
return [result, errors]
103+
return (result, errors)
90104

91-
def _get_babel_locale(self):
105+
def _get_babel_locale(self) -> babel.Locale:
92106
for lc in self.locales:
93107
try:
94108
return babel.Locale.parse(lc.replace('-', '_'))

fluent.runtime/fluent/runtime/errors.py

Lines changed: 5 additions & 3 deletions
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):

fluent.runtime/fluent/runtime/fallback.py

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import codecs
22
import os
3+
from typing import Any, Callable, Dict, Generator, List, TYPE_CHECKING, Type, Union, cast
4+
5+
from fluent.syntax import FluentParser
6+
7+
from .bundle import FluentBundle
8+
9+
if TYPE_CHECKING:
10+
from fluent.syntax.ast import Resource
11+
from .types import FluentType
312

413

514
class FluentLocalization:
@@ -9,41 +18,42 @@ class FluentLocalization:
918
This handles language fallback, bundle creation and string localization.
1019
It uses the given resource loader to load and parse Fluent data.
1120
"""
21+
1222
def __init__(
13-
self, locales, resource_ids, resource_loader,
14-
use_isolating=False,
15-
bundle_class=None, functions=None,
23+
self,
24+
locales: List[str],
25+
resource_ids: List[str],
26+
resource_loader: 'AbstractResourceLoader',
27+
use_isolating: bool = False,
28+
bundle_class: Type[FluentBundle] = FluentBundle,
29+
functions: Union[Dict[str, Callable[[Any], 'FluentType']], None] = None,
1630
):
1731
self.locales = locales
1832
self.resource_ids = resource_ids
1933
self.resource_loader = resource_loader
2034
self.use_isolating = use_isolating
21-
if bundle_class is None:
22-
from fluent.runtime import FluentBundle
23-
self.bundle_class = FluentBundle
24-
else:
25-
self.bundle_class = bundle_class
35+
self.bundle_class = bundle_class
2636
self.functions = functions
27-
self._bundle_cache = []
37+
self._bundle_cache: List[FluentBundle] = []
2838
self._bundle_it = self._iterate_bundles()
2939

30-
def format_value(self, msg_id, args=None):
40+
def format_value(self, msg_id: str, args: Union[Dict[str, Any], None] = None) -> str:
3141
for bundle in self._bundles():
3242
if not bundle.has_message(msg_id):
3343
continue
3444
msg = bundle.get_message(msg_id)
3545
if not msg.value:
3646
continue
37-
val, errors = bundle.format_pattern(msg.value, args)
38-
return val
47+
val, _errors = bundle.format_pattern(msg.value, args)
48+
return cast(str, val) # Never FluentNone when format_pattern called externally
3949
return msg_id
4050

41-
def _create_bundle(self, locales):
51+
def _create_bundle(self, locales: List[str]) -> FluentBundle:
4252
return self.bundle_class(
4353
locales, functions=self.functions, use_isolating=self.use_isolating
4454
)
4555

46-
def _bundles(self):
56+
def _bundles(self) -> Generator[FluentBundle, None, None]:
4757
bundle_pointer = 0
4858
while True:
4959
if bundle_pointer == len(self._bundle_cache):
@@ -54,7 +64,7 @@ def _bundles(self):
5464
yield self._bundle_cache[bundle_pointer]
5565
bundle_pointer += 1
5666

57-
def _iterate_bundles(self):
67+
def _iterate_bundles(self) -> Generator[FluentBundle, None, None]:
5868
for first_loc in range(0, len(self.locales)):
5969
locs = self.locales[first_loc:]
6070
for resources in self.resource_loader.resources(locs[0], self.resource_ids):
@@ -68,7 +78,8 @@ class AbstractResourceLoader:
6878
"""
6979
Interface to implement for resource loaders.
7080
"""
71-
def resources(self, locale, resource_ids):
81+
82+
def resources(self, locale: str, resource_ids: List[str]) -> Generator[List['Resource'], None, None]:
7283
"""
7384
Yield lists of FluentResource objects, corresponding to
7485
each of the resource_ids.
@@ -89,26 +100,26 @@ class FluentResourceLoader(AbstractResourceLoader):
89100
This loader does not support loading resources for one bundle from
90101
different roots.
91102
"""
92-
def __init__(self, roots):
103+
104+
def __init__(self, roots: Union[str, List[str]]):
93105
"""
94106
Create a resource loader. The roots may be a string for a single
95107
location on disk, or a list of strings.
96108
"""
97109
self.roots = [roots] if isinstance(roots, str) else roots
98-
from fluent.runtime import FluentResource
99110
self.Resource = FluentResource
100111

101-
def resources(self, locale, resource_ids):
112+
def resources(self, locale: str, resource_ids: List[str]) -> Generator[List['Resource'], None, None]:
102113
for root in self.roots:
103-
resources = []
114+
resources: List[Any] = []
104115
for resource_id in resource_ids:
105116
path = self.localize_path(os.path.join(root, resource_id), locale)
106117
if not os.path.isfile(path):
107118
continue
108119
content = codecs.open(path, 'r', 'utf-8').read()
109-
resources.append(self.Resource(content))
120+
resources.append(FluentParser().parse(content))
110121
if resources:
111122
yield resources
112123

113-
def localize_path(self, path, locale):
124+
def localize_path(self, path: str, locale: str) -> str:
114125
return path.format(locale=locale)

fluent.runtime/fluent/runtime/prepare.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
1+
from typing import Any, Dict, List
12
from fluent.syntax import ast as FTL
23
from . import resolver
34

45

56
class Compiler:
6-
def __call__(self, item):
7+
def __call__(self, item: Any) -> Any:
78
if isinstance(item, FTL.BaseNode):
89
return self.compile(item)
910
if isinstance(item, (tuple, list)):
1011
return [self(elem) for elem in item]
1112
return item
1213

13-
def compile(self, node):
14-
nodename = type(node).__name__
14+
def compile(self, node: Any) -> Any:
15+
nodename: str = type(node).__name__
1516
if not hasattr(resolver, nodename):
1617
return node
17-
kwargs = vars(node).copy()
18+
kwargs: Dict[str, Any] = vars(node).copy()
1819
for propname, propvalue in kwargs.items():
1920
kwargs[propname] = self(propvalue)
2021
handler = getattr(self, 'compile_' + nodename, self.compile_generic)
2122
return handler(nodename, **kwargs)
2223

23-
def compile_generic(self, nodename, **kwargs):
24+
def compile_generic(self, nodename: str, **kwargs: Any) -> Any:
2425
return getattr(resolver, nodename)(**kwargs)
2526

26-
def compile_Placeable(self, _, expression, **kwargs):
27+
def compile_Placeable(self, _: Any, expression: Any, **kwargs: Any) -> Any:
2728
if isinstance(expression, resolver.Literal):
2829
return expression
2930
return resolver.Placeable(expression=expression, **kwargs)
3031

31-
def compile_Pattern(self, _, elements, **kwargs):
32-
if (
33-
len(elements) == 1 and
34-
isinstance(elements[0], resolver.Placeable)
35-
):
32+
def compile_Pattern(self, _: Any, elements: List[Any], **kwargs: Any) -> Any:
33+
if len(elements) == 1 and isinstance(elements[0], resolver.Placeable):
3634
# Don't isolate isolated placeables
3735
return resolver.NeverIsolatingPlaceable(elements[0].expression)
3836
if any(

0 commit comments

Comments
 (0)