Skip to content

Commit b8aa39d

Browse files
committed
resolver - store attributes in _messages_and_terms dict
This has the result of performance optimization in a number of places, because we don't need to traverse attributes each time (with probably a very small startup cost), and simplifying quite a bit of logic. Also, tests for the corner cases found by looking at code coverage for the changes, and cleanups of places that were handling message IDs
1 parent 82f985f commit b8aa39d

File tree

4 files changed

+78
-42
lines changed

4 files changed

+78
-42
lines changed

fluent.runtime/fluent/runtime/__init__.py

+9-18
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,35 +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-
prefix = "-" if isinstance(item, Term) else ""
45-
full_id = prefix + item.id.name
45+
full_id = ast_to_id(item)
4646
if full_id not in self._messages_and_terms:
47-
self._messages_and_terms[full_id] = item
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)
4850

4951
def has_message(self, message_id):
50-
if message_id.startswith('-'):
52+
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
5153
return False
5254
return message_id in self._messages_and_terms
5355

5456
def format(self, message_id, args=None):
55-
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]
5660
if args is None:
5761
args = {}
5862
return resolve(self, message, args)
5963

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

fluent.runtime/fluent/runtime/resolver.py

+30-18
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import attr
88
import six
99

10-
from fluent.syntax.ast import (AttributeExpression, CallExpression, Identifier, Message, MessageReference,
10+
from fluent.syntax.ast import (Attribute, AttributeExpression, CallExpression, Identifier, Message, MessageReference,
1111
NumberLiteral, Pattern, Placeable, SelectExpression, StringLiteral, Term, TermReference,
1212
TextElement, VariableReference, VariantExpression, VariantList)
1313

@@ -190,24 +190,31 @@ def handle_term_reference(term_reference, env):
190190

191191

192192
def lookup_reference(ref, env):
193+
"""
194+
Given a MessageReference, TermReference or AttributeExpression, returns the
195+
AST node, or FluentNone if not found, including fallback logic
196+
"""
193197
ref_id = reference_to_id(ref)
194-
if "." in ref_id:
195-
parent_id, attr_name = ref_id.split('.')
196-
if parent_id not in env.context._messages_and_terms:
197-
env.errors.append(unknown_reference_error_obj(ref_id))
198-
return FluentNone(ref_id)
199-
else:
200-
parent = env.context._messages_and_terms[parent_id]
201-
for attribute in parent.attributes:
202-
if attribute.id.name == attr_name:
203-
return attribute.value
204-
env.errors.append(unknown_reference_error_obj(ref_id))
205-
return parent
206-
else:
207-
if ref_id not in env.context._messages_and_terms:
208-
env.errors.append(unknown_reference_error_obj(ref_id))
209-
return FluentNone(ref_id)
210-
return env.context._messages_and_terms[ref_id]
198+
199+
message = None
200+
try:
201+
message = env.context._messages_and_terms[ref_id]
202+
except LookupError:
203+
env.errors.append(unknown_reference_error_obj(ref_id))
204+
205+
if isinstance(ref, AttributeExpression):
206+
parent_id = reference_to_id(ref.ref)
207+
try:
208+
message = env.context._messages_and_terms[parent_id]
209+
except LookupError:
210+
# Don't add error here, because we already added error for the
211+
# actual thing we were looking for.
212+
pass
213+
214+
if message is None:
215+
message = FluentNone(ref_id)
216+
217+
return message
211218

212219

213220
@handle.register(FluentNone)
@@ -247,6 +254,11 @@ def handle_attribute_expression(attribute_ref, env):
247254
return handle(lookup_reference(attribute_ref, env), env)
248255

249256

257+
@handle.register(Attribute)
258+
def handle_attribute(attribute, env):
259+
return handle(attribute.value, env)
260+
261+
250262
@handle.register(VariantList)
251263
def handle_variant_list(variant_list, env):
252264
return select_from_variant_list(variant_list, env, None)

fluent.runtime/fluent/runtime/utils.py

+33-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1-
from fluent.syntax.ast import AttributeExpression, TermReference
1+
from fluent.syntax.ast import AttributeExpression, Term, TermReference
22

33
from .errors import FluentReferenceError
44

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+
return (TERM_SIGIL if isinstance(ast, Term) else '') + ast.id.name
14+
15+
16+
def add_message_and_attrs_to_store(store, ref_id, item, is_parent=True):
17+
store[ref_id] = item
18+
if is_parent:
19+
for attr in item.attributes:
20+
add_message_and_attrs_to_store(store,
21+
_make_attr_id(ref_id, attr.id.name),
22+
attr,
23+
is_parent=False)
24+
525

626
def numeric_to_native(val):
727
"""
@@ -28,15 +48,22 @@ def reference_to_id(ref):
2848
-term.attr
2949
"""
3050
if isinstance(ref, AttributeExpression):
31-
return "{0}.{1}".format(reference_to_id(ref.ref),
32-
ref.name.name)
33-
return ('-' if isinstance(ref, TermReference) else '') + ref.id.name
51+
return _make_attr_id(reference_to_id(ref.ref),
52+
ref.name.name)
53+
return (TERM_SIGIL if isinstance(ref, TermReference) else '') + ref.id.name
3454

3555

3656
def unknown_reference_error_obj(ref_id):
37-
if '.' in ref_id:
57+
if ATTRIBUTE_SEPARATOR in ref_id:
3858
return FluentReferenceError("Unknown attribute: {0}".format(ref_id))
39-
elif ref_id.startswith('-'):
59+
elif ref_id.startswith(TERM_SIGIL):
4060
return FluentReferenceError("Unknown term: {0}".format(ref_id))
4161
else:
4262
return FluentReferenceError("Unknown message: {0}".format(ref_id))
63+
64+
65+
def _make_attr_id(parent_ref_id, attr_name):
66+
"""
67+
Given a parent id and the attribute name, return the attribute id
68+
"""
69+
return ''.join([parent_ref_id, ATTRIBUTE_SEPARATOR, attr_name])

fluent.runtime/tests/format/test_attributes.py

+6
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def setUp(self):
106106
ref-qux = { qux.missing }
107107
attr-only =
108108
.attr = Attr Only Attribute
109+
ref-double-missing = { missing.attr }
109110
"""))
110111

111112
def test_falls_back_for_msg_with_string_value_and_no_attributes(self):
@@ -147,3 +148,8 @@ def test_attr_only_attribute(self):
147148
val, errs = self.ctx.format('attr-only.attr', {})
148149
self.assertEqual(val, 'Attr Only Attribute')
149150
self.assertEqual(len(errs), 0)
151+
152+
def test_missing_message_and_attribute(self):
153+
val, errs = self.ctx.format('ref-double-missing', {})
154+
self.assertEqual(val, 'missing.attr')
155+
self.assertEqual(errs, [FluentReferenceError('Unknown attribute: missing.attr')])

0 commit comments

Comments
 (0)