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.
@@ -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
@@ -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."""