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,5 @@
1
+ from messageformat2.message import Message, format_message
2
+
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["Message", "format_message"]
@@ -0,0 +1,323 @@
1
+ import datetime as _datetime
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Literal, Protocol, Self
4
+
5
+ from babel import Locale
6
+ from babel.dates import format_date as _format_date
7
+ from babel.dates import format_datetime as _format_datetime
8
+ from babel.dates import format_time as _format_time
9
+ from babel.dates import match_skeleton
10
+ from babel.numbers import format_compact_decimal, format_decimal, format_percent, format_scientific
11
+
12
+ from messageformat2.errors import InvalidExpression
13
+
14
+
15
+ def format_skeleton(
16
+ skeleton: str,
17
+ dt: _datetime.datetime,
18
+ locale: Locale,
19
+ ) -> str:
20
+ locale = Locale.parse(locale)
21
+ matched = skeleton
22
+ if skeleton not in locale.datetime_skeletons:
23
+ matched = match_skeleton(
24
+ skeleton,
25
+ locale.datetime_skeletons,
26
+ allow_different_fields=True)
27
+ fmt = locale.datetime_skeletons[matched]
28
+ return _format_datetime(dt, format=fmt, locale=locale)
29
+
30
+
31
+ class Formatter(Protocol):
32
+ def __call__(self, value: Any, locale: Locale, options: dict[str, Any]) -> Any: ...
33
+
34
+
35
+ class Selector(Protocol):
36
+ def __call__(self, value: Any, locale: Locale, options: dict[str, Any], keys: list[str]) -> list[str]: ...
37
+
38
+
39
+ def string_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str: # noqa: ARG001
40
+ return str(value)
41
+
42
+
43
+ def string_selector(value: Any, locale: Locale, options: dict[str, Any], keys: list[str]) -> list[str]:
44
+ string = string_formatter(value, locale=locale, options=options)
45
+ return [string] if string in keys else []
46
+
47
+
48
+ def format_number( # noqa: PLR0913
49
+ number: Any,
50
+ *,
51
+ locale: Locale,
52
+ compactDisplay: Literal["short", "long"] = "short",
53
+ notation: Literal["standard", "scientific", "engineering", "compact"] = "standard",
54
+ numberingSystem: str = "default",
55
+ signDisplay: Literal["auto", "always", "exceptZero", "negative", "never"] = "auto", # noqa: ARG001
56
+ style: Literal["decimal", "percent"] = "decimal",
57
+ useGrouping: Literal["auto", "always", "never", "min2"] = "auto",
58
+ minimumFractionDigits: int | None = None, # noqa: ARG001
59
+ maximumFractionDigits: int | None = None, # noqa: ARG001
60
+ **kwargs, # noqa: ARG001
61
+ ) -> str:
62
+ if notation == "compact":
63
+ # fraction_digits =
64
+ return format_compact_decimal(
65
+ number, format_type=compactDisplay, locale=locale, numbering_system=numberingSystem
66
+ )
67
+ if notation in ("scientific", "engineering"):
68
+ # babel has 'format_engineering' method..
69
+ return format_scientific(number, locale=locale, numbering_system=numberingSystem)
70
+ group_separator = useGrouping != "never"
71
+ if style == "percent":
72
+ return format_percent(
73
+ number,
74
+ locale=locale,
75
+ group_separator=group_separator,
76
+ numbering_system=numberingSystem,
77
+ )
78
+ return format_decimal(number, locale=locale, group_separator=group_separator, numbering_system=numberingSystem)
79
+
80
+
81
+ def keyword_selector(
82
+ number: float, *, locale: Locale, keys: list[str], select: Literal["exact", "plural", "ordinal"] = "plural"
83
+ ) -> list[str]:
84
+ if select == "plural":
85
+ keyword = locale.plural_form(number)
86
+ elif select == "ordinal":
87
+ keyword = locale.ordinal_form(number)
88
+ else:
89
+ msg = f"Unknown select type: {select}"
90
+ raise InvalidExpression(msg)
91
+
92
+ return [keyword] if keyword in keys else []
93
+
94
+
95
+ def number_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str:
96
+ return format_number(value, locale=locale, **options)
97
+
98
+
99
+ def number_selector(value: Any, locale: Locale, options: dict[str, Any], keys: list[str]) -> list[str]:
100
+ select = options.get("select", "plural")
101
+ number = format_number(value, locale=locale, **options)
102
+
103
+ exact_keys = [number] if number in keys else []
104
+ if select == "exact":
105
+ return exact_keys
106
+ keyword_keys = keyword_selector(value, locale=locale, keys=keys, select=select)
107
+ return exact_keys + keyword_keys
108
+
109
+
110
+ def format_integer( # noqa: PLR0913
111
+ value: Any,
112
+ *,
113
+ locale: Locale,
114
+ numberingSystem: str = "default",
115
+ signDisplay: Literal["auto", "always", "exceptZero", "negative", "never"] = "auto", # noqa: ARG001
116
+ style: Literal["decimal", "percent"] = "decimal",
117
+ useGrouping: Literal["auto", "always", "min2"] = "auto", # noqa: ARG001
118
+ minimumIntegerDigits: int | None = 1, # noqa: ARG001
119
+ maximumSignificantDigits: int | None = None, # noqa: ARG001
120
+ **kwargs, # noqa: ARG001
121
+ ) -> str:
122
+ if style == "percent":
123
+ return format_percent(
124
+ value,
125
+ locale=locale,
126
+ numbering_system=numberingSystem,
127
+ )
128
+ if style == "decimal":
129
+ return format_decimal(
130
+ value,
131
+ locale=locale,
132
+ numbering_system=numberingSystem,
133
+ )
134
+ msg = f"Unknown style: {style}"
135
+ raise InvalidExpression(msg)
136
+
137
+
138
+ def integer_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str: # noqa: ARG001
139
+ return str(int(value))
140
+
141
+
142
+ def integer_selector(value: Any, locale: Locale, options: dict[str, Any], keys: list[str]) -> list[str]:
143
+ select = options.get("select", "plural")
144
+ integer = format_integer(value, locale=locale, **options)
145
+
146
+ exact_keys = [integer] if integer in keys else []
147
+ if select == "exact":
148
+ return exact_keys
149
+ keyword_keys = keyword_selector(value, locale=locale, keys=keys, select=select)
150
+ return exact_keys + keyword_keys
151
+
152
+
153
+ def format_datetime( # noqa: PLR0915, PLR0913, PLR0912, C901
154
+ dt: _datetime.datetime,
155
+ locale: Locale,
156
+ dateStyle: Literal["full", "long", "medium", "short"] | None = None,
157
+ timeStyle: Literal["full", "long", "medium", "short"] | None = None, # noqa: ARG001
158
+ weekday: Literal["long", "short", "narrow"] | None = None,
159
+ era: Literal["long", "short", "narrow"] | None = None,
160
+ year: Literal["numeric", "2-digit"] | None = None,
161
+ month: Literal["numeric", "2-digit", "long", "short", "narrow"] | None = None,
162
+ day: Literal["numeric", "2-digit"] | None = None,
163
+ hour: Literal["numeric", "2-digit"] | None = None,
164
+ minute: Literal["numeric", "2-digit"] | None = None,
165
+ second: Literal["numeric", "2-digit"] | None = None,
166
+ fractionalSecondDigits: Literal["1", "2", "3"] | None = None,
167
+ hourCycle: Literal["h11", "h12", "h23", "h24"] | None = None,
168
+ timeZoneName: Literal["long", "short", "shortOffset", "longOffset", "shortGeneric", "longGeneric"] | None = None, # noqa: ARG001
169
+ **kwargs, # noqa: ARG001
170
+ ) -> str:
171
+ if dateStyle:
172
+ return _format_datetime(dt, format=dateStyle, locale=locale)
173
+
174
+ skeleton = ""
175
+ if weekday:
176
+ if weekday == "short":
177
+ skeleton += "EEE"
178
+ elif weekday == "long":
179
+ skeleton += "EEEE"
180
+ else:
181
+ skeleton += "EEEEE"
182
+ if era:
183
+ if era == "short":
184
+ skeleton += "GGG"
185
+ elif era == "long":
186
+ skeleton += "GGGG"
187
+ else:
188
+ skeleton += "GGGGG"
189
+
190
+ if year:
191
+ if year == "numeric":
192
+ skeleton += "y"
193
+ else:
194
+ skeleton += "yy"
195
+
196
+ if month:
197
+ if month == "numeric":
198
+ skeleton += "M"
199
+ elif month == "2-digit":
200
+ skeleton += "MM"
201
+ elif month == "short":
202
+ skeleton += "MMM"
203
+ elif month == "narrow":
204
+ skeleton += "MMMMM"
205
+ else:
206
+ skeleton += "MMMM"
207
+
208
+ if day:
209
+ if day == "numeric":
210
+ skeleton += "d"
211
+ else:
212
+ skeleton += "dd"
213
+
214
+ if hour:
215
+ h = "h"
216
+ if hourCycle:
217
+ if hourCycle == "h11":
218
+ h = "K"
219
+ elif hourCycle == "h12":
220
+ h = "h"
221
+ elif hourCycle == "h23":
222
+ h = "H"
223
+ elif hourCycle == "h24":
224
+ h = "k"
225
+
226
+ if hour == "numeric":
227
+ skeleton += f"{h}"
228
+ else:
229
+ skeleton += f"{h}{h}"
230
+
231
+ if minute:
232
+ if minute == "numeric":
233
+ skeleton += "m"
234
+ else:
235
+ skeleton += "mm"
236
+
237
+ if second:
238
+ if second == "numeric":
239
+ skeleton += "s"
240
+ else:
241
+ skeleton += "ss"
242
+
243
+ if fractionalSecondDigits:
244
+ if fractionalSecondDigits == "1":
245
+ skeleton += "S"
246
+ elif fractionalSecondDigits == "2":
247
+ skeleton += "SS"
248
+ else:
249
+ skeleton += "SSS"
250
+
251
+ return format_skeleton(skeleton, dt, locale=locale)
252
+
253
+
254
+ def datetime_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str:
255
+ if isinstance(value, str):
256
+ value = _datetime.datetime.fromisoformat(value)
257
+ return _format_datetime(value, locale=locale, **options)
258
+
259
+
260
+ def format_date(
261
+ dt: _datetime.datetime,
262
+ *,
263
+ locale: Locale,
264
+ style: Literal["full", "long", "medium", "short"] = "short",
265
+ **kwargs, # noqa: ARG001
266
+ ) -> str:
267
+ return _format_date(dt, format=style, locale=locale)
268
+
269
+
270
+ def date_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str:
271
+ if isinstance(value, str):
272
+ value = _datetime.datetime.fromisoformat(value)
273
+ return format_date(value, locale=locale, **options)
274
+
275
+
276
+ def format_time(
277
+ time: _datetime.time,
278
+ *,
279
+ locale: Locale,
280
+ style: Literal["full", "long", "medium", "short"] = "short",
281
+ **kwargs, # noqa: ARG001
282
+ ) -> str:
283
+ return _format_time(time, format=style, locale=locale)
284
+
285
+
286
+ def time_formatter(value: Any, locale: Locale, options: dict[str, Any]) -> str:
287
+ if isinstance(value, str):
288
+ value = _datetime.datetime.fromisoformat(value)
289
+ if isinstance(value, _datetime.datetime):
290
+ value = value.time()
291
+ return format_time(value, locale=locale, **options)
292
+
293
+
294
+ @dataclass(frozen=True)
295
+ class Registry:
296
+ formatters: dict[str, Formatter] = field(default_factory=dict)
297
+ selectors: dict[str, Selector] = field(default_factory=dict)
298
+
299
+ def extend(
300
+ self, *, formatters: dict[str, Formatter] | None = None, selectors: dict[str, Selector] | None = None
301
+ ) -> Self:
302
+ return type(self)(
303
+ formatters={**self.formatters, **(formatters or {})},
304
+ selectors={**self.selectors, **(selectors or {})},
305
+ )
306
+
307
+
308
+ default_registry = Registry(
309
+ formatters={
310
+ "string": string_formatter,
311
+ "number": number_formatter,
312
+ "integer": integer_formatter,
313
+ "datetime": datetime_formatter,
314
+ "date": date_formatter,
315
+ "time": time_formatter,
316
+ },
317
+ selectors={
318
+ "string": string_selector,
319
+ "number": number_selector,
320
+ "integer": integer_selector,
321
+ },
322
+ )
323
+ """The default registry of formatters and selectors."""
messageformat2/cli.py ADDED
@@ -0,0 +1,51 @@
1
+ import ast
2
+ import hashlib
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import click
7
+
8
+ import messageformat2
9
+
10
+
11
+ @click.group(invoke_without_command=True)
12
+ @click.option("--version", is_flag=True)
13
+ @click.pass_context
14
+ def cli(ctx: Any, version: bool) -> None: # noqa: FBT001
15
+ if ctx.invoked_subcommand is None:
16
+ if version:
17
+ click.echo(messageformat2.__version__)
18
+ else:
19
+ click.echo(ctx.get_help())
20
+
21
+
22
+ @cli.command()
23
+ @click.argument("filename", type=click.Path(exists=True))
24
+ def extract(filename: Path) -> None:
25
+ """Extract messages."""
26
+ filename = Path(filename)
27
+
28
+ class MessageExtractor(ast.NodeVisitor):
29
+ def __init__(self) -> None:
30
+ super().__init__()
31
+ self.messages = []
32
+
33
+ def visit_Call(self, node: ast.Call) -> None:
34
+ match node:
35
+ case ast.Call(func=ast.Name(id="_"), args=[ast.Constant(value=message), *_]):
36
+ self.messages.append(message)
37
+ self.generic_visit(node)
38
+
39
+ files = filename.rglob("*.py") if filename.is_dir() else [filename]
40
+
41
+ extractor = MessageExtractor()
42
+ for f in files:
43
+ source = f.read_text()
44
+ tree = ast.parse(source)
45
+ extractor.visit(tree)
46
+
47
+ output = ""
48
+ for message in extractor.messages:
49
+ msg_id = hashlib.sha256(message.encode()).hexdigest()
50
+ output += f"{msg_id} = {message}\n\n"
51
+ click.echo(output)