messageformat2 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- messageformat2/__init__.py +5 -0
- messageformat2/builtins.py +323 -0
- messageformat2/cli.py +51 -0
- messageformat2/datamodel.py +507 -0
- messageformat2/errors.py +151 -0
- messageformat2/message.py +127 -0
- messageformat2/parser.py +557 -0
- messageformat2/runtime.py +301 -0
- messageformat2-0.1.0.dist-info/LICENSE +21 -0
- messageformat2-0.1.0.dist-info/METADATA +148 -0
- messageformat2-0.1.0.dist-info/RECORD +14 -0
- messageformat2-0.1.0.dist-info/WHEEL +5 -0
- messageformat2-0.1.0.dist-info/entry_points.txt +2 -0
- messageformat2-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Literal as TypingLiteral
|
|
4
|
+
|
|
5
|
+
from messageformat2.errors import (
|
|
6
|
+
DuplicateDeclaration,
|
|
7
|
+
DuplicateOptionName,
|
|
8
|
+
MissingFallbackVariant,
|
|
9
|
+
MissingSelectorAnnotation,
|
|
10
|
+
VariantKeyMismatch,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Node:
|
|
15
|
+
"""Base class for all nodes in the data model AST."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def fields(self) -> tuple[str, ...]:
|
|
19
|
+
return ()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def dump(node: Node) -> str:
|
|
23
|
+
"""Return a string representation of the node and its children.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
node: The node to dump.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> from messageformat2.message import Message
|
|
30
|
+
>>> msg = Message("Hello, {$name}!")
|
|
31
|
+
>>> dump(msg.datamodel)
|
|
32
|
+
'Hello, {$name}!'
|
|
33
|
+
"""
|
|
34
|
+
return str(node)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class VariableRef(Node):
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return f"${self.name}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Literal(Node):
|
|
47
|
+
value: str
|
|
48
|
+
|
|
49
|
+
def __str__(self) -> str:
|
|
50
|
+
value = self.value.replace("|", r"\|").replace("\\", r"\\")
|
|
51
|
+
return f"|{value}|"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Attribute(Node):
|
|
56
|
+
name: str
|
|
57
|
+
value: Literal | VariableRef | None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def fields(self) -> tuple[str, ...]:
|
|
61
|
+
return ("value",)
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
if not self.value:
|
|
65
|
+
return f"@{self.name}"
|
|
66
|
+
return f"@{self.name}={self.value}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Option(Node):
|
|
71
|
+
name: str
|
|
72
|
+
value: Literal | VariableRef
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def fields(self) -> tuple[str, ...]:
|
|
76
|
+
return ("value",)
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
return f"{self.name}={self.value}"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class FunctionAnnotation(Node):
|
|
84
|
+
name: str
|
|
85
|
+
options: list[Option]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def fields(self) -> tuple[str, ...]:
|
|
89
|
+
return ("options",)
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
options = " ".join([str(opt) for opt in self.options])
|
|
93
|
+
if options:
|
|
94
|
+
options = f" {options}"
|
|
95
|
+
return f":{self.name}{options}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class UnsupportedAnnotation(Node):
|
|
100
|
+
source: str
|
|
101
|
+
|
|
102
|
+
def __str__(self) -> str:
|
|
103
|
+
return self.source
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Expression(Node):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class VariableExpression(Expression):
|
|
112
|
+
arg: VariableRef
|
|
113
|
+
annotation: FunctionAnnotation | UnsupportedAnnotation | None
|
|
114
|
+
attributes: list[Attribute]
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def fields(self) -> tuple[str, ...]:
|
|
118
|
+
return ("arg", "annotation", "attributes")
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
annotation = f" {self.annotation}" if self.annotation else ""
|
|
122
|
+
attributes = " ".join([str(attr) for attr in self.attributes])
|
|
123
|
+
if attributes:
|
|
124
|
+
attributes = f" {attributes}"
|
|
125
|
+
return f"{{{self.arg}{annotation}{attributes}}}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class LiteralExpression(Expression):
|
|
130
|
+
arg: Literal
|
|
131
|
+
annotation: FunctionAnnotation | UnsupportedAnnotation | None
|
|
132
|
+
attributes: list[Attribute]
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def fields(self) -> tuple[str, ...]:
|
|
136
|
+
return ("arg", "annotation", "attributes")
|
|
137
|
+
|
|
138
|
+
def __str__(self) -> str:
|
|
139
|
+
annotation = f" {self.annotation}" if self.annotation else ""
|
|
140
|
+
attributes = " ".join([str(attr) for attr in self.attributes])
|
|
141
|
+
if attributes:
|
|
142
|
+
attributes = f" {attributes}"
|
|
143
|
+
return f"{{{self.arg}{annotation}{attributes}}}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class FunctionExpression(Expression):
|
|
148
|
+
annotation: FunctionAnnotation
|
|
149
|
+
attributes: list[Attribute]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def fields(self) -> tuple[str, ...]:
|
|
153
|
+
return ("annotation", "attributes")
|
|
154
|
+
|
|
155
|
+
def __str__(self) -> str:
|
|
156
|
+
attributes = " ".join([str(attr) for attr in self.attributes])
|
|
157
|
+
if attributes:
|
|
158
|
+
attributes = f" {attributes}"
|
|
159
|
+
return f"{{{self.annotation}{attributes}}}"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class UnsupportedExpression(Expression):
|
|
164
|
+
annotation: UnsupportedAnnotation
|
|
165
|
+
attributes: list[Attribute]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def fields(self) -> tuple[str, ...]:
|
|
169
|
+
return ("annotation", "attributes")
|
|
170
|
+
|
|
171
|
+
def __str__(self) -> str:
|
|
172
|
+
attributes = " ".join([str(attr) for attr in self.attributes])
|
|
173
|
+
if attributes:
|
|
174
|
+
attributes = f" {attributes}"
|
|
175
|
+
return f"{{{self.annotation}{attributes}}}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass
|
|
179
|
+
class Markup(Node):
|
|
180
|
+
kind: TypingLiteral["open", "standalone", "close"]
|
|
181
|
+
name: str
|
|
182
|
+
options: list[Option]
|
|
183
|
+
attributes: list[Attribute]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def fields(self) -> tuple[str, ...]:
|
|
187
|
+
return ("options", "attributes")
|
|
188
|
+
|
|
189
|
+
def __str__(self) -> str:
|
|
190
|
+
options = " ".join([str(opt) for opt in self.options])
|
|
191
|
+
attributes = " ".join([str(attr) for attr in self.attributes])
|
|
192
|
+
if options:
|
|
193
|
+
options = f" {options}"
|
|
194
|
+
if attributes:
|
|
195
|
+
attributes = f" {attributes}"
|
|
196
|
+
if self.kind == "open":
|
|
197
|
+
return f"{{#{self.name}{options}{attributes}}}"
|
|
198
|
+
if self.kind == "standalone":
|
|
199
|
+
return f"{{#{self.name}{options}{attributes}/}}"
|
|
200
|
+
return f"{{/{self.name}{options}{attributes}}}"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
type Pattern = list[str | Expression | Markup]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class CatchallKey(Node):
|
|
208
|
+
value: str | None
|
|
209
|
+
|
|
210
|
+
def __str__(self) -> str:
|
|
211
|
+
return "*"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class Variant(Node):
|
|
216
|
+
keys: list[Literal | CatchallKey]
|
|
217
|
+
value: Pattern
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def fields(self) -> tuple[str, ...]:
|
|
221
|
+
return ("keys", "value")
|
|
222
|
+
|
|
223
|
+
def __str__(self) -> str:
|
|
224
|
+
keys = " ".join([str(key) for key in self.keys])
|
|
225
|
+
pattern = "".join([str(part) for part in self.value])
|
|
226
|
+
return f"{keys} {{{{{pattern}}}}}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Helper class, not part of the Data Model
|
|
230
|
+
@dataclass
|
|
231
|
+
class _Matcher:
|
|
232
|
+
selectors: list[Expression]
|
|
233
|
+
variants: list[Variant]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class InputDeclaration(Node):
|
|
238
|
+
name: str
|
|
239
|
+
value: VariableExpression
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def fields(self) -> tuple[str, ...]:
|
|
243
|
+
return ("value",)
|
|
244
|
+
|
|
245
|
+
def __str__(self) -> str:
|
|
246
|
+
return f".input {self.value}"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class LocalDeclaration(Node):
|
|
251
|
+
name: str
|
|
252
|
+
value: Expression
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def fields(self) -> tuple[str, ...]:
|
|
256
|
+
return ("value",)
|
|
257
|
+
|
|
258
|
+
def __str__(self) -> str:
|
|
259
|
+
return f".local {self.name} = {self.value}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class UnsupportedStatement(Node):
|
|
264
|
+
keyword: str
|
|
265
|
+
body: str | None
|
|
266
|
+
expressions: list[Expression]
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def fields(self) -> tuple[str, ...]:
|
|
270
|
+
return ("expressions",)
|
|
271
|
+
|
|
272
|
+
def __str__(self) -> str:
|
|
273
|
+
body = self.body or ""
|
|
274
|
+
expressions = "\n".join([str(expr) for expr in self.expressions])
|
|
275
|
+
return f".{self.keyword}{body}\n{expressions}"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
type Declaration = InputDeclaration | LocalDeclaration | UnsupportedStatement
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class PatternMessage(Node):
|
|
283
|
+
declarations: list[Declaration]
|
|
284
|
+
pattern: Pattern
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def fields(self) -> tuple[str, ...]:
|
|
288
|
+
return ("declarations", "pattern")
|
|
289
|
+
|
|
290
|
+
def __str__(self) -> str:
|
|
291
|
+
declarations = "\n".join([str(decl) for decl in self.declarations])
|
|
292
|
+
pattern = "".join([str(part) for part in self.pattern])
|
|
293
|
+
if not declarations:
|
|
294
|
+
return pattern
|
|
295
|
+
return f"{declarations}\n{{{pattern}}}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class SelectMessage(Node):
|
|
300
|
+
declarations: list[Declaration]
|
|
301
|
+
selectors: list[Expression]
|
|
302
|
+
variants: list[Variant]
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def fields(self) -> tuple[str, ...]:
|
|
306
|
+
return ("declarations", "selectors", "variants")
|
|
307
|
+
|
|
308
|
+
def __str__(self) -> str:
|
|
309
|
+
declarations = "\n".join([str(decl) for decl in self.declarations])
|
|
310
|
+
selectors = " ".join([str(sel) for sel in self.selectors])
|
|
311
|
+
variants = "\n".join([str(var) for var in self.variants])
|
|
312
|
+
if not declarations:
|
|
313
|
+
return f".match {selectors}\n{variants}"
|
|
314
|
+
return f"{declarations}\n.match {selectors}\n{variants}"
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
type Message = PatternMessage | SelectMessage
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class DataModelVisitor:
|
|
321
|
+
"""Data model AST visitor.
|
|
322
|
+
|
|
323
|
+
Works the same way as the ast.NodeVisitor from the Python stdlib.
|
|
324
|
+
|
|
325
|
+
Examples:
|
|
326
|
+
>>> from messageformat2 import Message
|
|
327
|
+
>>> from messageformat2.datamodel import DataModelVisitor
|
|
328
|
+
>>> class VariableVisitor(DataModelVisitor):
|
|
329
|
+
... def visit_VariableRef(self, node):
|
|
330
|
+
... print(f"${node.name}")
|
|
331
|
+
... self.generic_visit(node)
|
|
332
|
+
...
|
|
333
|
+
>>> message = Message("Hello, {$name}!")
|
|
334
|
+
>>> visitor = VariableVisitor()
|
|
335
|
+
>>> visitor.visit(message.datamodel)
|
|
336
|
+
$name
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def visit(self, node: Node) -> Any:
|
|
340
|
+
method_name = f"visit_{type(node).__name__}"
|
|
341
|
+
visitor = getattr(self, method_name, self.generic_visit)
|
|
342
|
+
return visitor(node)
|
|
343
|
+
|
|
344
|
+
def generic_visit(self, node: Node) -> Any:
|
|
345
|
+
for name in node.fields:
|
|
346
|
+
if (value := getattr(node, name)) is None:
|
|
347
|
+
continue
|
|
348
|
+
match value:
|
|
349
|
+
case [*items]:
|
|
350
|
+
for item in items:
|
|
351
|
+
if isinstance(item, Node):
|
|
352
|
+
self.visit(item)
|
|
353
|
+
case _:
|
|
354
|
+
self.visit(value)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class DataModelTransformer(DataModelVisitor):
|
|
358
|
+
"""Data model AST transformer.
|
|
359
|
+
|
|
360
|
+
Works the same way as the ast.NodeTransformer from the Python stdlib.
|
|
361
|
+
|
|
362
|
+
Examples:
|
|
363
|
+
>>> from messageformat2 import Message
|
|
364
|
+
>>> from messageformat2.datamodel import Literal, DataModelTransformer
|
|
365
|
+
>>> class LiteralTransformer(DataModelTransformer):
|
|
366
|
+
... def visit_Literal(self, node):
|
|
367
|
+
... return Literal(node.value.upper())
|
|
368
|
+
...
|
|
369
|
+
>>> message = Message("{|foo|}")
|
|
370
|
+
>>> ast = LiteralTransformer().visit(message.datamodel)
|
|
371
|
+
>>> str(ast)
|
|
372
|
+
'{|FOO|}'
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def generic_visit[T: Node](self, node: T) -> T:
|
|
376
|
+
for name in node.fields:
|
|
377
|
+
old_value = getattr(node, name)
|
|
378
|
+
if old_value is None:
|
|
379
|
+
continue
|
|
380
|
+
match old_value:
|
|
381
|
+
case [*items]:
|
|
382
|
+
new_value = []
|
|
383
|
+
for item in items:
|
|
384
|
+
if not isinstance(item, Node):
|
|
385
|
+
new_value.append(item)
|
|
386
|
+
continue
|
|
387
|
+
value = self.visit(item)
|
|
388
|
+
if isinstance(value, list):
|
|
389
|
+
new_value.extend(value)
|
|
390
|
+
elif value is not None:
|
|
391
|
+
new_value.append(value)
|
|
392
|
+
setattr(node, name, new_value)
|
|
393
|
+
case _:
|
|
394
|
+
new_value = self.visit(old_value)
|
|
395
|
+
if new_value is None:
|
|
396
|
+
delattr(node, name)
|
|
397
|
+
else:
|
|
398
|
+
setattr(node, name, new_value)
|
|
399
|
+
return node
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class DataModelValidator(DataModelVisitor):
|
|
403
|
+
def visit_PatternMessage(self, node: SelectMessage) -> None:
|
|
404
|
+
self._check_declarations(node.declarations)
|
|
405
|
+
self.generic_visit(node)
|
|
406
|
+
|
|
407
|
+
def visit_SelectMessage(self, node: SelectMessage) -> None:
|
|
408
|
+
self._check_declarations(node.declarations)
|
|
409
|
+
self._check_missing_selector_annotation(node.selectors, node.declarations)
|
|
410
|
+
self._check_variant_key_mismatch(node.selectors, node.variants)
|
|
411
|
+
self._check_missing_fallback_variant(node.variants)
|
|
412
|
+
self.generic_visit(node)
|
|
413
|
+
|
|
414
|
+
def visit_Markup(self, node: Markup) -> None:
|
|
415
|
+
self._check_duplicate_options(node.options)
|
|
416
|
+
self.generic_visit(node)
|
|
417
|
+
|
|
418
|
+
def visit_FunctionAnnotation(self, node: FunctionAnnotation) -> None:
|
|
419
|
+
self._check_duplicate_options(node.options)
|
|
420
|
+
self.generic_visit(node)
|
|
421
|
+
|
|
422
|
+
def _check_declarations(self, declarations: list[Declaration]) -> None:
|
|
423
|
+
self._check_circular_reference(declarations)
|
|
424
|
+
self._check_duplicate_declarations(declarations)
|
|
425
|
+
self._check_implicit_redeclaration(declarations)
|
|
426
|
+
|
|
427
|
+
def _check_missing_selector_annotation(self, selectors: list[Expression], declarations: list[Declaration]) -> None:
|
|
428
|
+
for selector in selectors:
|
|
429
|
+
if not self._is_annotated(selector, declarations):
|
|
430
|
+
msg = "Missing selector annotation"
|
|
431
|
+
raise MissingSelectorAnnotation(msg)
|
|
432
|
+
|
|
433
|
+
def _is_annotated(self, selector: Expression, declarations: list[Declaration]) -> bool:
|
|
434
|
+
match selector:
|
|
435
|
+
case LiteralExpression(annotation=None):
|
|
436
|
+
return False
|
|
437
|
+
case VariableExpression(annotation=None, arg=VariableRef(name=name)):
|
|
438
|
+
for decl in declarations:
|
|
439
|
+
if isinstance(decl, UnsupportedStatement) or decl.name != name:
|
|
440
|
+
continue
|
|
441
|
+
match decl:
|
|
442
|
+
case InputDeclaration(value=VariableExpression(annotation=annotation)):
|
|
443
|
+
return annotation is not None
|
|
444
|
+
case _:
|
|
445
|
+
return self._is_annotated(decl.value, declarations)
|
|
446
|
+
return False
|
|
447
|
+
return True
|
|
448
|
+
|
|
449
|
+
def _check_variant_key_mismatch(self, selectors: list[Expression], variants: list[Variant]) -> None:
|
|
450
|
+
for variant in variants:
|
|
451
|
+
if len(selectors) != len(variant.keys):
|
|
452
|
+
msg = f'Key count does not match selector count: "{variant}"'
|
|
453
|
+
raise VariantKeyMismatch(msg)
|
|
454
|
+
|
|
455
|
+
def _check_missing_fallback_variant(self, variants: list[Variant]) -> None:
|
|
456
|
+
has_fallback = any(all(isinstance(key, CatchallKey) for key in variant.keys) for variant in variants)
|
|
457
|
+
if not has_fallback:
|
|
458
|
+
msg = "Missing fallback variant"
|
|
459
|
+
raise MissingFallbackVariant(msg)
|
|
460
|
+
|
|
461
|
+
def _check_circular_reference(self, declarations: list[Declaration]) -> None:
|
|
462
|
+
for decl in declarations:
|
|
463
|
+
match decl:
|
|
464
|
+
case LocalDeclaration(name=name, value=VariableExpression(arg=VariableRef(name=ref))):
|
|
465
|
+
if name == ref:
|
|
466
|
+
msg = f"Duplicate declaration: {name}"
|
|
467
|
+
raise DuplicateDeclaration(msg)
|
|
468
|
+
|
|
469
|
+
def _check_duplicate_declarations(self, declarations: list[Declaration]) -> None:
|
|
470
|
+
names = set()
|
|
471
|
+
for decl in declarations:
|
|
472
|
+
if isinstance(decl, UnsupportedStatement):
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
if decl.name in names:
|
|
476
|
+
msg = f"Duplicate declaration: {decl.name}"
|
|
477
|
+
raise DuplicateDeclaration(msg)
|
|
478
|
+
names.add(decl.name)
|
|
479
|
+
|
|
480
|
+
def _check_implicit_redeclaration(self, declarations: list[Declaration]) -> None:
|
|
481
|
+
implicit = set()
|
|
482
|
+
for decl in declarations:
|
|
483
|
+
if isinstance(decl, UnsupportedStatement):
|
|
484
|
+
continue
|
|
485
|
+
|
|
486
|
+
if decl.name in implicit:
|
|
487
|
+
msg = f"Redeclaration of an implicit variable: {decl.name}"
|
|
488
|
+
raise DuplicateDeclaration(msg)
|
|
489
|
+
|
|
490
|
+
match decl.value:
|
|
491
|
+
case (
|
|
492
|
+
VariableExpression(annotation=FunctionAnnotation() as annotation)
|
|
493
|
+
| LiteralExpression(annotation=FunctionAnnotation() as annotation)
|
|
494
|
+
| FunctionExpression(annotation=FunctionAnnotation() as annotation)
|
|
495
|
+
) if annotation is not None:
|
|
496
|
+
for opt in annotation.options:
|
|
497
|
+
match opt.value:
|
|
498
|
+
case VariableRef(name=name):
|
|
499
|
+
implicit.add(name)
|
|
500
|
+
|
|
501
|
+
def _check_duplicate_options(self, options: list[Option]) -> None:
|
|
502
|
+
options_map = {}
|
|
503
|
+
for opt in options:
|
|
504
|
+
if opt.name in options_map:
|
|
505
|
+
msg = f"Duplicate option name: '{opt.name}'"
|
|
506
|
+
raise DuplicateOptionName(msg)
|
|
507
|
+
options_map[opt.name] = opt.value
|
messageformat2/errors.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
class MessageFormatError(Exception):
|
|
2
|
+
"""Base class for all messageformat2 errors.
|
|
3
|
+
|
|
4
|
+
Use this class to catch arbitrary messageformat2 errors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParseError(MessageFormatError):
|
|
9
|
+
"""Raised when invalid syntax is encountered during parsing.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
>>> from messageformat2 import Message
|
|
13
|
+
>>> Message("{ Unclosed pattern") # doctest: +ELLIPSIS
|
|
14
|
+
Traceback (most recent call last):
|
|
15
|
+
...
|
|
16
|
+
messageformat2.errors.ParseError: ...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DataModelError(MessageFormatError):
|
|
21
|
+
"""Base class for all messageformat2 data model errors."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VariantKeyMismatch(DataModelError):
|
|
25
|
+
"""Raised when the number of variant keys does not match the number of selectors.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> from messageformat2 import Message
|
|
29
|
+
>>> Message(".match {$count :integer} 0 1 {{You have no notifications.}}") # doctest: +ELLIPSIS
|
|
30
|
+
Traceback (most recent call last):
|
|
31
|
+
...
|
|
32
|
+
messageformat2.errors.VariantKeyMismatch: ...
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MissingFallbackVariant(DataModelError):
|
|
37
|
+
"""Raised when a fallback variant (all catch-all keys) is not provided.
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> from messageformat2 import Message
|
|
41
|
+
>>> Message(".match {$count :integer} one {{You have one notification.}}") # doctest: +ELLIPSIS
|
|
42
|
+
Traceback (most recent call last):
|
|
43
|
+
...
|
|
44
|
+
messageformat2.errors.MissingFallbackVariant: Missing fallback variant
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MissingSelectorAnnotation(DataModelError):
|
|
49
|
+
"""Raised when a selector annotation is missing.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
>>> from messageformat2 import Message
|
|
53
|
+
>>> Message(".match {$count} * {{You have {$count} notifications.}}") # doctest: +ELLIPSIS
|
|
54
|
+
Traceback (most recent call last):
|
|
55
|
+
...
|
|
56
|
+
messageformat2.errors.MissingSelectorAnnotation: ...
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DuplicateDeclaration(DataModelError):
|
|
61
|
+
"""Raised when a variable is declared more than once.
|
|
62
|
+
|
|
63
|
+
This includes variables declared as '.input'.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
>>> from messageformat2 import Message
|
|
67
|
+
>>> Message(".input {$count} .local $count = {42} {{Duplicate declaration}}") # doctest: +ELLIPSIS
|
|
68
|
+
Traceback (most recent call last):
|
|
69
|
+
...
|
|
70
|
+
messageformat2.errors.DuplicateDeclaration: ...
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DuplicateOptionName(DataModelError):
|
|
75
|
+
"""Raised when an option is declared more than once.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> from messageformat2 import Message
|
|
79
|
+
>>> Message("{42 :integer opt=1 opt=2}") # doctest: +ELLIPSIS
|
|
80
|
+
Traceback (most recent call last):
|
|
81
|
+
...
|
|
82
|
+
messageformat2.errors.DuplicateOptionName: ...
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FormatError(MessageFormatError):
|
|
87
|
+
"""Base class for all errors raised during formatting."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ResolutionError(FormatError):
|
|
91
|
+
"""Raised when a part of the message cannot be determined."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class UnresolvedVariable(ResolutionError):
|
|
95
|
+
"""Raised when a variable cannot be resolved.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> from messageformat2 import Message
|
|
99
|
+
>>> Message("Hello, {$name}!").format() # doctest: +ELLIPSIS
|
|
100
|
+
Traceback (most recent call last):
|
|
101
|
+
...
|
|
102
|
+
messageformat2.errors.UnresolvedVariable: ...
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UnknownFunction(ResolutionError):
|
|
107
|
+
"""Raised when a function is not found.
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
>>> from messageformat2 import Message
|
|
111
|
+
>>> Message("{Alice :unknown}").format() # doctest: +ELLIPSIS
|
|
112
|
+
Traceback (most recent call last):
|
|
113
|
+
...
|
|
114
|
+
messageformat2.errors.UnknownFunction: ...
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class UnsupportedExpression(ResolutionError):
|
|
119
|
+
"""Raised when an expression uses reserved syntax.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
>>> from messageformat2 import Message
|
|
123
|
+
>>> Message("The value is {!horse}").format() # doctest: +ELLIPSIS
|
|
124
|
+
Traceback (most recent call last):
|
|
125
|
+
...
|
|
126
|
+
messageformat2.errors.UnsupportedExpression: ...
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class UnsupportedStatement(ResolutionError):
|
|
131
|
+
"""Raised when the message contains a reserved statement.
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
>>> from messageformat2 import Message
|
|
135
|
+
>>> Message(".unknown {$x} .match {$x :string} * {{}}").format() # doctest: +ELLIPSIS
|
|
136
|
+
Traceback (most recent call last):
|
|
137
|
+
...
|
|
138
|
+
messageformat2.errors.UnsupportedStatement: ...
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class InvalidExpression(ResolutionError):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class OperandMismatch(InvalidExpression):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class SelectionError(FormatError):
|
|
151
|
+
"""Raised when message selection fails."""
|