1
1
from __future__ import absolute_import , unicode_literals
2
2
3
+ import contextlib
3
4
from datetime import date , datetime
4
5
from decimal import Decimal
5
6
6
7
import attr
7
8
import six
8
9
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 )
14
13
15
- from .errors import FluentCyclicReferenceError , FluentReferenceError
14
+ from .errors import FluentCyclicReferenceError , FluentFormatError , FluentReferenceError
16
15
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
18
17
19
18
try :
20
19
from functools import singledispatch
37
36
PDI = "\u2069 "
38
37
39
38
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
+
40
56
@attr .s
41
57
class ResolverEnvironment (object ):
42
58
context = attr .ib ()
43
- args = attr .ib ()
44
59
errors = attr .ib ()
45
60
dirty = attr .ib (factory = set )
46
61
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 )
47
80
48
81
49
82
def resolve (context , message , args ):
@@ -55,7 +88,7 @@ def resolve(context, message, args):
55
88
"""
56
89
errors = []
57
90
env = ResolverEnvironment (context = context ,
58
- args = args ,
91
+ current = CurrentEnvironment ( args = args ) ,
59
92
errors = errors )
60
93
return fully_resolve (message , env ), errors
61
94
@@ -71,8 +104,8 @@ def fully_resolve(expr, env):
71
104
retval = handle (expr , env )
72
105
if isinstance (retval , text_type ):
73
106
return retval
74
- else :
75
- return fully_resolve (retval , env )
107
+
108
+ return fully_resolve (retval , env )
76
109
77
110
78
111
@singledispatch
@@ -156,33 +189,38 @@ def handle_number_expression(number_expression, env):
156
189
157
190
@handle .register (MessageReference )
158
191
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 )
161
193
162
194
163
195
@handle .register (TermReference )
164
196
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
+
167
200
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 )
168
207
169
- def lookup_reference (name , env ):
170
- message = None
171
208
try :
172
- message = env .context ._messages_and_terms [name ]
209
+ return env .context ._messages_and_terms [ref_id ]
173
210
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
184
222
185
- return message
223
+ return FluentNone ( ref_id )
186
224
187
225
188
226
@handle .register (FluentNone )
@@ -200,10 +238,11 @@ def handle_none(none, env):
200
238
def handle_variable_reference (argument , env ):
201
239
name = argument .id .name
202
240
try :
203
- arg_val = env .args [name ]
241
+ arg_val = env .current . args [name ]
204
242
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 )))
207
246
return FluentNone (name )
208
247
209
248
if isinstance (arg_val ,
@@ -217,21 +256,13 @@ def handle_variable_reference(argument, env):
217
256
218
257
219
258
@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 )
226
261
227
- for message_attr in message .attributes :
228
- if message_attr .id .name == attr_name :
229
- return handle (message_attr .value , env )
230
262
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 )
235
266
236
267
237
268
@handle .register (VariantList )
@@ -260,8 +291,8 @@ def select_from_variant_list(variant_list, env, key):
260
291
found = default
261
292
if found is None :
262
293
return FluentNone ()
263
- else :
264
- return handle (found .value , env )
294
+
295
+ return handle (found .value , env )
265
296
266
297
267
298
@handle .register (SelectExpression )
@@ -287,8 +318,7 @@ def select_from_select_expression(expression, env, key):
287
318
found = default
288
319
if found is None :
289
320
return FluentNone ()
290
- else :
291
- return handle (found .value , env )
321
+ return handle (found .value , env )
292
322
293
323
294
324
def is_number (val ):
@@ -304,9 +334,8 @@ def match(val1, val2, env):
304
334
if not is_number (val2 ):
305
335
# Could be plural rule match
306
336
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 )
310
339
311
340
return val1 == val2
312
341
@@ -318,7 +347,7 @@ def handle_indentifier(identifier, env):
318
347
319
348
@handle .register (VariantExpression )
320
349
def handle_variant_expression (expression , env ):
321
- message = lookup_reference (expression .ref . id . name , env )
350
+ message = lookup_reference (expression .ref , env )
322
351
if isinstance (message , FluentNone ):
323
352
return message
324
353
@@ -334,16 +363,26 @@ def handle_variant_expression(expression, env):
334
363
335
364
@handle .register (CallExpression )
336
365
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
338
379
try :
339
380
function = env .context ._functions [function_name ]
340
381
except LookupError :
341
382
env .errors .append (FluentReferenceError ("Unknown function: {0}"
342
383
.format (function_name )))
343
384
return FluentNone (function_name + "()" )
344
385
345
- args = [handle (arg , env ) for arg in expression .positional ]
346
- kwargs = {kwarg .name .name : handle (kwarg .value , env ) for kwarg in expression .named }
347
386
try :
348
387
return function (* args , ** kwargs )
349
388
except Exception as e :
0 commit comments