Skip to content

Commit 9c1b457

Browse files
authored
Merge pull request #88 from django-ftl/update_fluent_runtime_for_fluent_syntax_010
Update fluent.runtime for fluent.syntax 0.10
2 parents 3389c1b + 30eb856 commit 9c1b457

File tree

9 files changed

+425
-77
lines changed

9 files changed

+425
-77
lines changed

fluent.runtime/fluent/runtime/__init__.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .builtins import BUILTINS
1111
from .resolver import resolve
12+
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL, add_message_and_attrs_to_store, ast_to_id
1213

1314

1415
class FluentBundle(object):
@@ -41,33 +42,25 @@ def add_messages(self, source):
4142
# TODO - warn/error about duplicates
4243
for item in resource.body:
4344
if isinstance(item, (Message, Term)):
44-
if item.id.name not in self._messages_and_terms:
45-
self._messages_and_terms[item.id.name] = item
45+
full_id = ast_to_id(item)
46+
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)
4650

4751
def has_message(self, message_id):
48-
if message_id.startswith('-'):
52+
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
4953
return False
5054
return message_id in self._messages_and_terms
5155

5256
def format(self, message_id, args=None):
53-
message = self._get_message(message_id)
57+
if message_id.startswith(TERM_SIGIL):
58+
raise LookupError(message_id)
59+
message = self._messages_and_terms[message_id]
5460
if args is None:
5561
args = {}
5662
return resolve(self, message, args)
5763

58-
def _get_message(self, message_id):
59-
if message_id.startswith('-'):
60-
raise LookupError(message_id)
61-
if '.' in message_id:
62-
name, attr_name = message_id.split('.', 1)
63-
msg = self._messages_and_terms[name]
64-
for attribute in msg.attributes:
65-
if attribute.id.name == attr_name:
66-
return attribute.value
67-
raise LookupError(message_id)
68-
else:
69-
return self._messages_and_terms[message_id]
70-
7164
def _get_babel_locale(self):
7265
for l in self.locales:
7366
try:

fluent.runtime/fluent/runtime/resolver.py

+95-56
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
from __future__ import absolute_import, unicode_literals
22

3+
import contextlib
34
from datetime import date, datetime
45
from decimal import Decimal
56

67
import attr
78
import six
89

9-
from fluent.syntax.ast import (AttributeExpression, CallExpression, Message,
10-
MessageReference, NumberLiteral, Pattern,
11-
Placeable, SelectExpression, StringLiteral, Term,
12-
TermReference, TextElement, VariableReference,
13-
VariantExpression, VariantList, Identifier)
10+
from fluent.syntax.ast import (Attribute, AttributeExpression, CallExpression, Identifier, Message, MessageReference,
11+
NumberLiteral, Pattern, Placeable, SelectExpression, StringLiteral, Term, TermReference,
12+
TextElement, VariableReference, VariantExpression, VariantList)
1413

15-
from .errors import FluentCyclicReferenceError, FluentReferenceError
14+
from .errors import FluentCyclicReferenceError, FluentFormatError, FluentReferenceError
1615
from .types import FluentDateType, FluentNone, FluentNumber, fluent_date, fluent_number
17-
from .utils import numeric_to_native
16+
from .utils import numeric_to_native, reference_to_id, unknown_reference_error_obj
1817

1918
try:
2019
from functools import singledispatch
@@ -37,13 +36,47 @@
3736
PDI = "\u2069"
3837

3938

39+
@attr.s
40+
class CurrentEnvironment(object):
41+
# The parts of ResolverEnvironment that we want to mutate (and restore)
42+
# temporarily for some parts of a call chain.
43+
44+
# The values of attributes here must not be mutated, they must only be
45+
# swapped out for different objects using `modified` (see below).
46+
47+
# For Messages, VariableReference nodes are interpreted as external args,
48+
# but for Terms they are the values explicitly passed using CallExpression
49+
# syntax. So we have to be able to change 'args' for this purpose.
50+
args = attr.ib()
51+
# This controls whether we need to report an error if a VariableReference
52+
# refers to an arg that is not present in the args dict.
53+
error_for_missing_arg = attr.ib(default=True)
54+
55+
4056
@attr.s
4157
class ResolverEnvironment(object):
4258
context = attr.ib()
43-
args = attr.ib()
4459
errors = attr.ib()
4560
dirty = attr.ib(factory=set)
4661
part_count = attr.ib(default=0)
62+
current = attr.ib(factory=CurrentEnvironment)
63+
64+
@contextlib.contextmanager
65+
def modified(self, **replacements):
66+
"""
67+
Context manager that modifies the 'current' attribute of the
68+
environment, restoring the old data at the end.
69+
"""
70+
# CurrentEnvironment only has args that we never mutate, so the shallow
71+
# copy returned by attr.evolve is fine (at least for now).
72+
old_current = self.current
73+
self.current = attr.evolve(old_current, **replacements)
74+
yield self
75+
self.current = old_current
76+
77+
def modified_for_term_reference(self, args=None):
78+
return self.modified(args=args if args is not None else {},
79+
error_for_missing_arg=False)
4780

4881

4982
def resolve(context, message, args):
@@ -55,7 +88,7 @@ def resolve(context, message, args):
5588
"""
5689
errors = []
5790
env = ResolverEnvironment(context=context,
58-
args=args,
91+
current=CurrentEnvironment(args=args),
5992
errors=errors)
6093
return fully_resolve(message, env), errors
6194

@@ -71,8 +104,8 @@ def fully_resolve(expr, env):
71104
retval = handle(expr, env)
72105
if isinstance(retval, text_type):
73106
return retval
74-
else:
75-
return fully_resolve(retval, env)
107+
108+
return fully_resolve(retval, env)
76109

77110

78111
@singledispatch
@@ -156,33 +189,38 @@ def handle_number_expression(number_expression, env):
156189

157190
@handle.register(MessageReference)
158191
def handle_message_reference(message_reference, env):
159-
name = message_reference.id.name
160-
return handle(lookup_reference(name, env), env)
192+
return handle(lookup_reference(message_reference, env), env)
161193

162194

163195
@handle.register(TermReference)
164196
def handle_term_reference(term_reference, env):
165-
name = term_reference.id.name
166-
return handle(lookup_reference(name, env), env)
197+
with env.modified_for_term_reference():
198+
return handle(lookup_reference(term_reference, env), env)
199+
167200

201+
def lookup_reference(ref, env):
202+
"""
203+
Given a MessageReference, TermReference or AttributeExpression, returns the
204+
AST node, or FluentNone if not found, including fallback logic
205+
"""
206+
ref_id = reference_to_id(ref)
168207

169-
def lookup_reference(name, env):
170-
message = None
171208
try:
172-
message = env.context._messages_and_terms[name]
209+
return env.context._messages_and_terms[ref_id]
173210
except LookupError:
174-
if name.startswith("-"):
175-
env.errors.append(
176-
FluentReferenceError("Unknown term: {0}"
177-
.format(name)))
178-
else:
179-
env.errors.append(
180-
FluentReferenceError("Unknown message: {0}"
181-
.format(name)))
182-
if message is None:
183-
message = FluentNone(name)
211+
env.errors.append(unknown_reference_error_obj(ref_id))
212+
213+
if isinstance(ref, AttributeExpression):
214+
# Fallback
215+
parent_id = reference_to_id(ref.ref)
216+
try:
217+
return env.context._messages_and_terms[parent_id]
218+
except LookupError:
219+
# Don't add error here, because we already added error for the
220+
# actual thing we were looking for.
221+
pass
184222

185-
return message
223+
return FluentNone(ref_id)
186224

187225

188226
@handle.register(FluentNone)
@@ -200,10 +238,11 @@ def handle_none(none, env):
200238
def handle_variable_reference(argument, env):
201239
name = argument.id.name
202240
try:
203-
arg_val = env.args[name]
241+
arg_val = env.current.args[name]
204242
except LookupError:
205-
env.errors.append(
206-
FluentReferenceError("Unknown external: {0}".format(name)))
243+
if env.current.error_for_missing_arg:
244+
env.errors.append(
245+
FluentReferenceError("Unknown external: {0}".format(name)))
207246
return FluentNone(name)
208247

209248
if isinstance(arg_val,
@@ -217,21 +256,13 @@ def handle_variable_reference(argument, env):
217256

218257

219258
@handle.register(AttributeExpression)
220-
def handle_attribute_expression(attribute, env):
221-
parent_id = attribute.ref.id.name
222-
attr_name = attribute.name.name
223-
message = lookup_reference(parent_id, env)
224-
if isinstance(message, FluentNone):
225-
return message
259+
def handle_attribute_expression(attribute_ref, env):
260+
return handle(lookup_reference(attribute_ref, env), env)
226261

227-
for message_attr in message.attributes:
228-
if message_attr.id.name == attr_name:
229-
return handle(message_attr.value, env)
230262

231-
env.errors.append(
232-
FluentReferenceError("Unknown attribute: {0}.{1}"
233-
.format(parent_id, attr_name)))
234-
return handle(message, env)
263+
@handle.register(Attribute)
264+
def handle_attribute(attribute, env):
265+
return handle(attribute.value, env)
235266

236267

237268
@handle.register(VariantList)
@@ -260,8 +291,8 @@ def select_from_variant_list(variant_list, env, key):
260291
found = default
261292
if found is None:
262293
return FluentNone()
263-
else:
264-
return handle(found.value, env)
294+
295+
return handle(found.value, env)
265296

266297

267298
@handle.register(SelectExpression)
@@ -287,8 +318,7 @@ def select_from_select_expression(expression, env, key):
287318
found = default
288319
if found is None:
289320
return FluentNone()
290-
else:
291-
return handle(found.value, env)
321+
return handle(found.value, env)
292322

293323

294324
def is_number(val):
@@ -304,9 +334,8 @@ def match(val1, val2, env):
304334
if not is_number(val2):
305335
# Could be plural rule match
306336
return env.context._plural_form(val1) == val2
307-
else:
308-
if is_number(val2):
309-
return match(val2, val1, env)
337+
elif is_number(val2):
338+
return match(val2, val1, env)
310339

311340
return val1 == val2
312341

@@ -318,7 +347,7 @@ def handle_indentifier(identifier, env):
318347

319348
@handle.register(VariantExpression)
320349
def handle_variant_expression(expression, env):
321-
message = lookup_reference(expression.ref.id.name, env)
350+
message = lookup_reference(expression.ref, env)
322351
if isinstance(message, FluentNone):
323352
return message
324353

@@ -334,16 +363,26 @@ def handle_variant_expression(expression, env):
334363

335364
@handle.register(CallExpression)
336365
def handle_call_expression(expression, env):
337-
function_name = expression.callee.name
366+
args = [handle(arg, env) for arg in expression.positional]
367+
kwargs = {kwarg.name.name: handle(kwarg.value, env) for kwarg in expression.named}
368+
369+
if isinstance(expression.callee, (TermReference, AttributeExpression)):
370+
term = lookup_reference(expression.callee, env)
371+
if args:
372+
env.errors.append(FluentFormatError("Ignored positional arguments passed to term '{0}'"
373+
.format(reference_to_id(expression.callee))))
374+
with env.modified_for_term_reference(args=kwargs):
375+
return handle(term, env)
376+
377+
# builtin or custom function call
378+
function_name = expression.callee.id.name
338379
try:
339380
function = env.context._functions[function_name]
340381
except LookupError:
341382
env.errors.append(FluentReferenceError("Unknown function: {0}"
342383
.format(function_name)))
343384
return FluentNone(function_name + "()")
344385

345-
args = [handle(arg, env) for arg in expression.positional]
346-
kwargs = {kwarg.name.name: handle(kwarg.value, env) for kwarg in expression.named}
347386
try:
348387
return function(*args, **kwargs)
349388
except Exception as e:
+62-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
from fluent.syntax.ast import AttributeExpression, Term, TermReference
2+
3+
from .errors import FluentReferenceError
4+
5+
TERM_SIGIL = '-'
6+
ATTRIBUTE_SEPARATOR = '.'
7+
8+
9+
def ast_to_id(ast):
10+
"""
11+
Returns a string reference for a Term or Message
12+
"""
13+
if isinstance(ast, Term):
14+
return TERM_SIGIL + ast.id.name
15+
return ast.id.name
16+
17+
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+
128
def numeric_to_native(val):
229
"""
330
Given a numeric string (as defined by fluent spec),
@@ -7,5 +34,38 @@ def numeric_to_native(val):
734
# '-'? [0-9]+ ('.' [0-9]+)?
835
if '.' in val:
936
return float(val)
10-
else:
11-
return int(val)
37+
return int(val)
38+
39+
40+
def reference_to_id(ref):
41+
"""
42+
Returns a string reference for a MessageReference, TermReference or AttributeExpression
43+
AST node.
44+
45+
e.g.
46+
message
47+
message.attr
48+
-term
49+
-term.attr
50+
"""
51+
if isinstance(ref, AttributeExpression):
52+
return _make_attr_id(reference_to_id(ref.ref),
53+
ref.name.name)
54+
if isinstance(ref, TermReference):
55+
return TERM_SIGIL + ref.id.name
56+
return ref.id.name
57+
58+
59+
def unknown_reference_error_obj(ref_id):
60+
if ATTRIBUTE_SEPARATOR in ref_id:
61+
return FluentReferenceError("Unknown attribute: {0}".format(ref_id))
62+
if ref_id.startswith(TERM_SIGIL):
63+
return FluentReferenceError("Unknown term: {0}".format(ref_id))
64+
return FluentReferenceError("Unknown message: {0}".format(ref_id))
65+
66+
67+
def _make_attr_id(parent_ref_id, attr_name):
68+
"""
69+
Given a parent id and the attribute name, return the attribute id
70+
"""
71+
return ''.join([parent_ref_id, ATTRIBUTE_SEPARATOR, attr_name])

fluent.runtime/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
],
2727
packages=['fluent', 'fluent.runtime'],
2828
install_requires=[
29-
'fluent>=0.9,<0.10',
29+
'fluent.syntax>=0.10,<=0.11',
3030
'attrs',
3131
'babel',
3232
'pytz',

0 commit comments

Comments
 (0)