Skip to content

Commit 3dcf0bc

Browse files
committed
Make resolver threadsaf(er) again, fix isolation
This patch removes state from the compiled tree. For one, the recursion detection on patterns keeps its state in the `ResolverEnvironment` now, instead of in the compiled tree. Also, the question of bidi isolation is now kept in the resolver context, instead of in the compiled tree. For single placeables, which don't get bidi-isolated ever, there's now a distinct NeverIsolatingPlaceable, which also takes care of resolving fluent values to strings. This should fix the test cases in #108 and #109
1 parent 7894b71 commit 3dcf0bc

File tree

4 files changed

+60
-18
lines changed

4 files changed

+60
-18
lines changed

fluent.runtime/fluent/runtime/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ def __init__(self, locales, functions=None, use_isolating=True):
3232
if functions:
3333
_functions.update(functions)
3434
self._functions = _functions
35-
self._use_isolating = use_isolating
35+
self.use_isolating = use_isolating
3636
self._messages_and_terms = {}
3737
self._compiled = {}
38-
self._compiler = Compiler(use_isolating=use_isolating)
38+
self._compiler = Compiler()
3939
self._babel_locale = self._get_babel_locale()
4040
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)
4141

fluent.runtime/fluent/runtime/prepare.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55

66
class Compiler(object):
7-
def __init__(self, use_isolating=False):
8-
self.use_isolating = use_isolating
9-
107
def __call__(self, item):
118
if isinstance(item, FTL.BaseNode):
129
return self.compile(item)
@@ -28,19 +25,17 @@ def compile_generic(self, nodename, **kwargs):
2825
return getattr(resolver, nodename)(**kwargs)
2926

3027
def compile_Placeable(self, _, expression, **kwargs):
31-
if self.use_isolating:
32-
return resolver.IsolatingPlaceable(expression=expression, **kwargs)
3328
if isinstance(expression, resolver.Literal):
3429
return expression
3530
return resolver.Placeable(expression=expression, **kwargs)
3631

3732
def compile_Pattern(self, _, elements, **kwargs):
3833
if (
3934
len(elements) == 1 and
40-
isinstance(elements[0], resolver.IsolatingPlaceable)
35+
isinstance(elements[0], resolver.Placeable)
4136
):
4237
# Don't isolate isolated placeables
43-
return elements[0].expression
38+
return resolver.NeverIsolatingPlaceable(elements[0].expression)
4439
if any(
4540
not isinstance(child, resolver.Literal)
4641
for child in elements

fluent.runtime/fluent/runtime/resolver.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class CurrentEnvironment(object):
5353
class ResolverEnvironment(object):
5454
context = attr.ib()
5555
errors = attr.ib()
56-
part_count = attr.ib(default=0)
56+
part_count = attr.ib(default=0, init=False)
57+
active_patterns = attr.ib(factory=set, init=False)
5758
current = attr.ib(factory=CurrentEnvironment)
5859

5960
@contextlib.contextmanager
@@ -105,15 +106,14 @@ class Pattern(FTL.Pattern, BaseResolver):
105106

106107
def __init__(self, *args, **kwargs):
107108
super(Pattern, self).__init__(*args, **kwargs)
108-
self.dirty = False
109109

110110
def __call__(self, env):
111-
if self.dirty:
111+
if self in env.active_patterns:
112112
env.errors.append(FluentCyclicReferenceError("Cyclic reference"))
113113
return FluentNone()
114114
if env.part_count > self.MAX_PARTS:
115115
return ""
116-
self.dirty = True
116+
env.active_patterns.add(self)
117117
elements = self.elements
118118
remaining_parts = self.MAX_PARTS - env.part_count
119119
if len(self.elements) > remaining_parts:
@@ -124,7 +124,7 @@ def __call__(self, env):
124124
resolve(element(env), env) for element in elements
125125
)
126126
env.part_count += len(elements)
127-
self.dirty = False
127+
env.active_patterns.remove(self)
128128
return retval
129129

130130

@@ -144,13 +144,16 @@ def __call__(self, env):
144144

145145
class Placeable(FTL.Placeable, BaseResolver):
146146
def __call__(self, env):
147-
return self.expression(env)
147+
inner = resolve(self.expression(env), env)
148+
if not env.context.use_isolating:
149+
return inner
150+
return "\u2068" + inner + "\u2069"
148151

149152

150-
class IsolatingPlaceable(FTL.Placeable, BaseResolver):
153+
class NeverIsolatingPlaceable(FTL.Placeable, BaseResolver):
151154
def __call__(self, env):
152-
inner = self.expression(env)
153-
return "\u2068" + resolve(inner, env) + "\u2069"
155+
inner = resolve(self.expression(env), env)
156+
return inner
154157

155158

156159
class StringLiteral(FTL.StringLiteral, Literal):

fluent.runtime/tests/format/test_placeables.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,47 @@ def test_allowed_self_reference(self):
109109
val, errs = self.ctx.format('self-parent-ref-ok.attr', {})
110110
self.assertEqual(val, 'Attribute Parent')
111111
self.assertEqual(len(errs), 0)
112+
113+
114+
class TestSingleElementPattern(unittest.TestCase):
115+
def test_single_literal_number_isolating(self):
116+
self.ctx = FluentBundle(['en-US'], use_isolating=True)
117+
self.ctx.add_messages('foo = { 1 }')
118+
val, errs = self.ctx.format('foo')
119+
self.assertEqual(val, '1')
120+
self.assertEqual(errs, [])
121+
122+
def test_single_literal_number_non_isolating(self):
123+
self.ctx = FluentBundle(['en-US'], use_isolating=False)
124+
self.ctx.add_messages('foo = { 1 }')
125+
val, errs = self.ctx.format('foo')
126+
self.assertEqual(val, '1')
127+
self.assertEqual(errs, [])
128+
129+
def test_single_arg_number_isolating(self):
130+
self.ctx = FluentBundle(['en-US'], use_isolating=True)
131+
self.ctx.add_messages('foo = { $arg }')
132+
val, errs = self.ctx.format('foo', {'arg': 1})
133+
self.assertEqual(val, '1')
134+
self.assertEqual(errs, [])
135+
136+
def test_single_arg_number_non_isolating(self):
137+
self.ctx = FluentBundle(['en-US'], use_isolating=False)
138+
self.ctx.add_messages('foo = { $arg }')
139+
val, errs = self.ctx.format('foo', {'arg': 1})
140+
self.assertEqual(val, '1')
141+
self.assertEqual(errs, [])
142+
143+
def test_single_arg_missing_isolating(self):
144+
self.ctx = FluentBundle(['en-US'], use_isolating=True)
145+
self.ctx.add_messages('foo = { $arg }')
146+
val, errs = self.ctx.format('foo')
147+
self.assertEqual(val, 'arg')
148+
self.assertEqual(len(errs), 1)
149+
150+
def test_single_arg_missing_non_isolating(self):
151+
self.ctx = FluentBundle(['en-US'], use_isolating=False)
152+
self.ctx.add_messages('foo = { $arg }')
153+
val, errs = self.ctx.format('foo')
154+
self.assertEqual(val, 'arg')
155+
self.assertEqual(len(errs), 1)

0 commit comments

Comments
 (0)