Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0509340

Browse files
authoredFeb 19, 2019
Refactor resolver into a tree of callable objects, or partially evaluated (#95)
1 parent a13ef99 commit 0509340

File tree

8 files changed

+301
-301
lines changed

8 files changed

+301
-301
lines changed
 

‎fluent.runtime/fluent/runtime/__init__.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from fluent.syntax.ast import Message, Term
99

1010
from .builtins import BUILTINS
11-
from .resolver import resolve
12-
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, add_message_and_attrs_to_store, ast_to_id
11+
from .prepare import Compiler
12+
from .resolver import ResolverEnvironment, CurrentEnvironment
13+
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, ast_to_id, native_to_fluent
1314

1415

1516
class FluentBundle(object):
@@ -33,6 +34,8 @@ def __init__(self, locales, functions=None, use_isolating=True):
3334
self._functions = _functions
3435
self._use_isolating = use_isolating
3536
self._messages_and_terms = {}
37+
self._compiled = {}
38+
self._compiler = Compiler(use_isolating=use_isolating)
3639
self._babel_locale = self._get_babel_locale()
3740
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)
3841

@@ -44,22 +47,41 @@ def add_messages(self, source):
4447
if isinstance(item, (Message, Term)):
4548
full_id = ast_to_id(item)
4649
if full_id not in self._messages_and_terms:
47-
# We add attributes to the store to enable faster looker
48-
# later, and more direct code in some instances.
49-
add_message_and_attrs_to_store(self._messages_and_terms, full_id, item)
50+
self._messages_and_terms[full_id] = item
5051

5152
def has_message(self, message_id):
5253
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
5354
return False
5455
return message_id in self._messages_and_terms
5556

57+
def lookup(self, full_id):
58+
if full_id not in self._compiled:
59+
entry_id = full_id.split(ATTRIBUTE_SEPARATOR, 1)[0]
60+
entry = self._messages_and_terms[entry_id]
61+
compiled = self._compiler(entry)
62+
if compiled.value is not None:
63+
self._compiled[entry_id] = compiled.value
64+
for attr in compiled.attributes:
65+
self._compiled[ATTRIBUTE_SEPARATOR.join([entry_id, attr.id.name])] = attr.value
66+
return self._compiled[full_id]
67+
5668
def format(self, message_id, args=None):
5769
if message_id.startswith(TERM_SIGIL):
5870
raise LookupError(message_id)
59-
message = self._messages_and_terms[message_id]
60-
if args is None:
61-
args = {}
62-
return resolve(self, message, args)
71+
if args is not None:
72+
fluent_args = {
73+
argname: native_to_fluent(argvalue)
74+
for argname, argvalue in args.items()
75+
}
76+
else:
77+
fluent_args = {}
78+
79+
errors = []
80+
resolve = self.lookup(message_id)
81+
env = ResolverEnvironment(context=self,
82+
current=CurrentEnvironment(args=fluent_args),
83+
errors=errors)
84+
return [resolve(env), errors]
6385

6486
def _get_babel_locale(self):
6587
for l in self.locales:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from __future__ import absolute_import, unicode_literals
2+
from fluent.syntax import ast as FTL
3+
from . import resolver
4+
5+
6+
class Compiler(object):
7+
def __init__(self, use_isolating=False):
8+
self.use_isolating = use_isolating
9+
10+
def __call__(self, item):
11+
if isinstance(item, FTL.BaseNode):
12+
return self.compile(item)
13+
if isinstance(item, (tuple, list)):
14+
return [self(elem) for elem in item]
15+
return item
16+
17+
def compile(self, node):
18+
nodename = type(node).__name__
19+
if not hasattr(resolver, nodename):
20+
return node
21+
kwargs = vars(node).copy()
22+
for propname, propvalue in kwargs.items():
23+
kwargs[propname] = self(propvalue)
24+
handler = getattr(self, 'compile_' + nodename, self.compile_generic)
25+
return handler(nodename, **kwargs)
26+
27+
def compile_generic(self, nodename, **kwargs):
28+
return getattr(resolver, nodename)(**kwargs)
29+
30+
def compile_Placeable(self, _, expression, **kwargs):
31+
if self.use_isolating:
32+
return resolver.IsolatingPlaceable(expression=expression, **kwargs)
33+
if isinstance(expression, resolver.Literal):
34+
return expression
35+
return resolver.Placeable(expression=expression, **kwargs)
36+
37+
def compile_Pattern(self, _, elements, **kwargs):
38+
if (
39+
len(elements) == 1 and
40+
isinstance(elements[0], resolver.IsolatingPlaceable)
41+
):
42+
# Don't isolate isolated placeables
43+
return elements[0].expression
44+
if any(
45+
not isinstance(child, resolver.Literal)
46+
for child in elements
47+
):
48+
return resolver.Pattern(elements=elements, **kwargs)
49+
if len(elements) == 1:
50+
return elements[0]
51+
return resolver.TextElement(
52+
''.join(child(None) for child in elements)
53+
)

‎fluent.runtime/fluent/runtime/resolver.py

Lines changed: 191 additions & 260 deletions
Large diffs are not rendered by default.

‎fluent.runtime/fluent/runtime/types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class NumberFormatOptions(object):
8181
maximumSignificantDigits = attr.ib(default=None)
8282

8383

84-
class FluentNumber(object):
84+
class FluentNumber(FluentType):
8585

8686
default_number_format_options = NumberFormatOptions()
8787

@@ -276,7 +276,7 @@ class DateFormatOptions(object):
276276
_SUPPORTED_DATETIME_OPTIONS = ['dateStyle', 'timeStyle', 'timeZone']
277277

278278

279-
class FluentDateType(object):
279+
class FluentDateType(FluentType):
280280
# We need to match signature of `__init__` and `__new__` due to the way
281281
# some Python implementation (e.g. PyPy) implement some methods.
282282
# So we leave those alone, and implement another `_init_options`

‎fluent.runtime/fluent/runtime/utils.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from __future__ import absolute_import, unicode_literals
2+
3+
from datetime import date, datetime
4+
from decimal import Decimal
5+
16
from fluent.syntax.ast import AttributeExpression, Term, TermReference
27

8+
from .types import FluentInt, FluentFloat, FluentDecimal, FluentDate, FluentDateTime
39
from .errors import FluentReferenceError
410

511
TERM_SIGIL = '-'
@@ -15,26 +21,22 @@ def ast_to_id(ast):
1521
return ast.id.name
1622

1723

18-
def add_message_and_attrs_to_store(store, ref_id, item, is_parent=True):
19-
store[ref_id] = item
20-
if is_parent:
21-
for attr in item.attributes:
22-
add_message_and_attrs_to_store(store,
23-
_make_attr_id(ref_id, attr.id.name),
24-
attr,
25-
is_parent=False)
26-
27-
28-
def numeric_to_native(val):
24+
def native_to_fluent(val):
2925
"""
30-
Given a numeric string (as defined by fluent spec),
31-
return an int or float
26+
Convert a python type to a Fluent Type.
3227
"""
33-
# val matches this EBNF:
34-
# '-'? [0-9]+ ('.' [0-9]+)?
35-
if '.' in val:
36-
return float(val)
37-
return int(val)
28+
if isinstance(val, int):
29+
return FluentInt(val)
30+
if isinstance(val, float):
31+
return FluentFloat(val)
32+
if isinstance(val, Decimal):
33+
return FluentDecimal(val)
34+
35+
if isinstance(val, datetime):
36+
return FluentDateTime.from_date_time(val)
37+
if isinstance(val, date):
38+
return FluentDate.from_date(val)
39+
return val
3840

3941

4042
def reference_to_id(ref):

‎fluent.runtime/setup.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22
from setuptools import setup
33
import sys
44

5-
if sys.version_info < (3, 4):
6-
extra_requires = ['singledispatch>=3.4']
7-
else:
8-
# functools.singledispatch is in stdlib from Python 3.4 onwards.
9-
extra_requires = []
10-
115
setup(name='fluent.runtime',
126
version='0.1',
137
description='Localization library for expressive translations.',
@@ -26,11 +20,11 @@
2620
],
2721
packages=['fluent', 'fluent.runtime'],
2822
install_requires=[
29-
'fluent.syntax>=0.10,<=0.11',
23+
'fluent.syntax>=0.12,<=0.13',
3024
'attrs',
3125
'babel',
3226
'pytz',
33-
] + extra_requires,
27+
],
3428
tests_require=['six'],
3529
test_suite='tests'
3630
)

‎fluent.runtime/tests/test_bomb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,5 @@ def test_max_expansions_protection(self):
3939
# Without protection, emptylolz will take a really long time to
4040
# evaluate, although it generates an empty message.
4141
val, errs = self.ctx.format('emptylolz')
42-
self.assertEqual(val, '???')
42+
self.assertEqual(val, '')
4343
self.assertEqual(len(errs), 1)

‎fluent.runtime/tox.ini

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ skipsdist=True
66
setenv =
77
PYTHONPATH = {toxinidir}
88
deps =
9-
fluent.syntax>=0.10,<=0.11
9+
fluent.syntax>=0.12,<=0.13
1010
six
1111
attrs
1212
Babel
13-
py27: singledispatch
14-
pypy: singledispatch
1513
commands = ./runtests.py

0 commit comments

Comments
 (0)
Please sign in to comment.