polyglossa 2026.11.6__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.
polyglossa/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """English Wiktionary's languages, language families and scripts.
2
+
3
+ >>> english = Language["en"]
4
+ >>> english.name
5
+ 'English'
6
+ >>> english.family.name
7
+ 'Anglic'
8
+ >>> Language.get("xx") is None
9
+ True
10
+ >>> "ang" in Language
11
+ True
12
+ """
13
+
14
+ from polyglossa._catalog import (
15
+ Catalog,
16
+ Language,
17
+ LanguageFamily,
18
+ LanguageType,
19
+ NotACatalogError,
20
+ Script,
21
+ ScriptDirection,
22
+ UnknownFamilyError,
23
+ UnknownLanguageError,
24
+ UnknownScriptError,
25
+ Variety,
26
+ )
27
+ from polyglossa._version import __version__
28
+
29
+ __all__ = [
30
+ "Catalog",
31
+ "Language",
32
+ "LanguageFamily",
33
+ "LanguageType",
34
+ "NotACatalogError",
35
+ "Script",
36
+ "ScriptDirection",
37
+ "UnknownFamilyError",
38
+ "UnknownLanguageError",
39
+ "UnknownScriptError",
40
+ "Variety",
41
+ "__version__",
42
+ ]
polyglossa/_catalog.py ADDED
@@ -0,0 +1,486 @@
1
+ """The catalog and what it holds, read from the tab-separated ``catalog.tsv`` the Kotlin library writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ import functools
7
+ from dataclasses import dataclass, field
8
+ from importlib import resources
9
+ from typing import Dict, Iterator, List, Optional, Tuple
10
+
11
+ HEADER = "polyglossa\t1"
12
+
13
+
14
+ class UnknownLanguageError(KeyError):
15
+ """Wiktionary has no language with this code."""
16
+
17
+
18
+ class UnknownFamilyError(KeyError):
19
+ """Wiktionary has no family with this code."""
20
+
21
+
22
+ class UnknownScriptError(KeyError):
23
+ """Wiktionary has no script with this code."""
24
+
25
+
26
+ class NotACatalogError(ValueError):
27
+ """The text is not a catalog Polyglossa wrote."""
28
+
29
+
30
+ class LanguageType(enum.Enum):
31
+ """What kind of language Wiktionary takes one to be, which decides where its entries may live."""
32
+
33
+ REGULAR = "regular"
34
+ RECONSTRUCTED = "reconstructed"
35
+ APPENDIX_CONSTRUCTED = "appendix-constructed"
36
+ ETYMOLOGY_ONLY = "etymology-only"
37
+
38
+
39
+ class ScriptDirection(enum.Enum):
40
+ """Which way a script runs."""
41
+
42
+ LEFT_TO_RIGHT = "ltr"
43
+ RIGHT_TO_LEFT = "rtl"
44
+ VERTICAL_LEFT_TO_RIGHT = "vertical-ltr"
45
+ VERTICAL_RIGHT_TO_LEFT = "vertical-rtl"
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Variety:
50
+ """A variety of a language Wiktionary names without giving it a code, such as a dialect."""
51
+
52
+ name: str
53
+ other_names: Tuple[str, ...] = ()
54
+
55
+ def __str__(self) -> str:
56
+ return self.name if not self.other_names else f"{self.name} ({', '.join(self.other_names)})"
57
+
58
+
59
+ class _Lookup(type):
60
+ """Lets a type be indexed and searched by code, ``Language["en"]`` and ``"en" in Language``."""
61
+
62
+ def __getitem__(cls, code: str):
63
+ return cls.of(code)
64
+
65
+ def __contains__(cls, code: object) -> bool:
66
+ return isinstance(code, str) and cls.get(code) is not None
67
+
68
+ def __iter__(cls) -> Iterator:
69
+ return iter(cls.all())
70
+
71
+
72
+ @dataclass(frozen=True, eq=True)
73
+ class Language(metaclass=_Lookup):
74
+ """One language as English Wiktionary's ``Module:languages`` and ``Module:etymology languages`` describe it."""
75
+
76
+ code: str
77
+ name: str
78
+ type: LanguageType
79
+ wikidata: Optional[str]
80
+ aliases: Tuple[str, ...]
81
+ varieties: Tuple[Variety, ...]
82
+ _family_code: Optional[str] = field(repr=False)
83
+ _script_codes: Tuple[str, ...] = field(repr=False)
84
+ _ancestor_codes: Tuple[str, ...] = field(repr=False)
85
+ _parent_code: Optional[str] = field(repr=False)
86
+ _catalog: "Catalog" = field(repr=False, compare=False, hash=False)
87
+
88
+ @property
89
+ def is_etymology_only(self) -> bool:
90
+ """Whether it is named in etymologies only, with no section of its own in an entry."""
91
+ return self.type is LanguageType.ETYMOLOGY_ONLY
92
+
93
+ @property
94
+ def family(self) -> Optional["LanguageFamily"]:
95
+ """The family it belongs to, or ``None``; an etymology-only variety has its language's."""
96
+ code = self._family_code or (self.parent._family_code if self.parent else None)
97
+ return self._catalog.family(code) if code else None
98
+
99
+ @property
100
+ def scripts(self) -> List["Script"]:
101
+ """The scripts it is written in, most usual first, those the catalog knows."""
102
+ return [s for s in (self._catalog.script(c) for c in self._script_codes) if s is not None]
103
+
104
+ @property
105
+ def ancestors(self) -> List["Language"]:
106
+ """The languages it descends from, those the catalog knows."""
107
+ return [a for a in (self._catalog.get(c) for c in self._ancestor_codes) if a is not None]
108
+
109
+ @property
110
+ def parent(self) -> Optional["Language"]:
111
+ """The language an etymology-only variety belongs to, or ``None`` for any other."""
112
+ return self._catalog.get(self._parent_code) if self._parent_code else None
113
+
114
+ @property
115
+ def full(self) -> "Language":
116
+ """The full language this stands for: itself, or for an etymology-only variety the one it belongs to."""
117
+ language, seen = self, {self.code}
118
+ while language.is_etymology_only and language.parent and language.parent.code not in seen:
119
+ language = language.parent
120
+ seen.add(language.code)
121
+ return language
122
+
123
+ def __str__(self) -> str:
124
+ return f"{self.name} ({self.code})"
125
+
126
+ @classmethod
127
+ def of(cls, code: str) -> "Language":
128
+ """The language with ``code``; raises :class:`UnknownLanguageError` when Wiktionary has none."""
129
+ language = Catalog.bundled().get(code)
130
+ if language is None:
131
+ raise UnknownLanguageError(code)
132
+ return language
133
+
134
+ @classmethod
135
+ def get(cls, code: str) -> Optional["Language"]:
136
+ """The language with ``code``, or ``None`` when Wiktionary has none."""
137
+ return Catalog.bundled().get(code)
138
+
139
+ @classmethod
140
+ def named(cls, name: str) -> Optional["Language"]:
141
+ """The language called ``name``, as its section in an entry is headed, or ``None``."""
142
+ return Catalog.bundled().named(name)
143
+
144
+ @classmethod
145
+ def all(cls) -> List["Language"]:
146
+ """Every language and etymology-only variety, sorted by code."""
147
+ return Catalog.bundled().languages
148
+
149
+
150
+ @dataclass(frozen=True, eq=True)
151
+ class LanguageFamily(metaclass=_Lookup):
152
+ """One language family as English Wiktionary's ``Module:families`` describes it."""
153
+
154
+ code: str
155
+ name: str
156
+ wikidata: Optional[str]
157
+ aliases: Tuple[str, ...]
158
+ _parent_code: Optional[str] = field(repr=False)
159
+ _proto_language_code: Optional[str] = field(repr=False)
160
+ _catalog: "Catalog" = field(repr=False, compare=False, hash=False)
161
+
162
+ @property
163
+ def parent(self) -> Optional["LanguageFamily"]:
164
+ """The family it belongs to, or ``None``."""
165
+ return self._catalog.family(self._parent_code) if self._parent_code else None
166
+
167
+ @property
168
+ def proto_language(self) -> Optional[Language]:
169
+ """The language it descends from: the one Wiktionary names, or else ``<code>-pro`` when there is one."""
170
+ return self._catalog.get(self._proto_language_code or f"{self.code}-pro")
171
+
172
+ @property
173
+ def languages(self) -> List[Language]:
174
+ """The languages in it directly, not those of the families under it, sorted by code."""
175
+ return self._catalog._in_family(self.code)
176
+
177
+ def __str__(self) -> str:
178
+ return f"{self.name} ({self.code})"
179
+
180
+ @classmethod
181
+ def of(cls, code: str) -> "LanguageFamily":
182
+ """The family with ``code``; raises :class:`UnknownFamilyError` when Wiktionary has none."""
183
+ family = Catalog.bundled().family(code)
184
+ if family is None:
185
+ raise UnknownFamilyError(code)
186
+ return family
187
+
188
+ @classmethod
189
+ def get(cls, code: str) -> Optional["LanguageFamily"]:
190
+ """The family with ``code``, or ``None`` when Wiktionary has none."""
191
+ return Catalog.bundled().family(code)
192
+
193
+ @classmethod
194
+ def all(cls) -> List["LanguageFamily"]:
195
+ """Every family, sorted by code."""
196
+ return Catalog.bundled().families
197
+
198
+
199
+ @dataclass(frozen=True, eq=True)
200
+ class Script(metaclass=_Lookup):
201
+ """One script as English Wiktionary's ``Module:scripts/data`` describes it."""
202
+
203
+ code: str
204
+ name: str
205
+ wikidata: Optional[str]
206
+ system: Optional[str]
207
+ aliases: Tuple[str, ...]
208
+ direction: ScriptDirection
209
+ _parent_code: Optional[str] = field(repr=False)
210
+ _local_name_codes: Tuple[Tuple[str, str], ...] = field(repr=False)
211
+ _catalog: "Catalog" = field(repr=False, compare=False, hash=False)
212
+
213
+ @property
214
+ def parent(self) -> Optional["Script"]:
215
+ """The script this is a variety of, such as Han for Simplified Han, or ``None``."""
216
+ return self._catalog.script(self._parent_code) if self._parent_code else None
217
+
218
+ def name_for(self, language: Language) -> str:
219
+ """The name ``language`` gives this script, which is :attr:`name` unless it has its own."""
220
+ return dict(self._local_name_codes).get(language.code, self.name)
221
+
222
+ @property
223
+ def local_names(self) -> Dict[Language, str]:
224
+ """The names some languages give it instead of :attr:`name`, such as Balbodh for Marathi's Devanagari."""
225
+ names = {}
226
+ for code, local in self._local_name_codes:
227
+ language = self._catalog.get(code)
228
+ if language is not None:
229
+ names[language] = local
230
+ return names
231
+
232
+ def __str__(self) -> str:
233
+ return f"{self.name} ({self.code})"
234
+
235
+ @classmethod
236
+ def of(cls, code: str) -> "Script":
237
+ """The script with ``code``; raises :class:`UnknownScriptError` when Wiktionary has none."""
238
+ script = Catalog.bundled().script(code)
239
+ if script is None:
240
+ raise UnknownScriptError(code)
241
+ return script
242
+
243
+ @classmethod
244
+ def get(cls, code: str) -> Optional["Script"]:
245
+ """The script with ``code``, or ``None`` when Wiktionary has none."""
246
+ return Catalog.bundled().script(code)
247
+
248
+ @classmethod
249
+ def named(cls, name: str) -> Optional["Script"]:
250
+ """The script called ``name``, or ``None``; the one without a parent when several are."""
251
+ return Catalog.bundled().script_named(name)
252
+
253
+ @classmethod
254
+ def all(cls) -> List["Script"]:
255
+ """Every script, sorted by code."""
256
+ return Catalog.bundled().scripts
257
+
258
+
259
+ class Catalog:
260
+ """A set of languages, families and scripts that know one another.
261
+
262
+ :meth:`bundled`, English Wiktionary's own, is the one :class:`Language`, :class:`LanguageFamily` and
263
+ :class:`Script` look up.
264
+ """
265
+
266
+ def __init__(self, tsv: str) -> None:
267
+ self.date: Optional[str] = None
268
+ self._languages: Dict[str, Language] = {}
269
+ self._families: Dict[str, LanguageFamily] = {}
270
+ self._scripts: Dict[str, Script] = {}
271
+ _read(tsv, self)
272
+ names: Dict[str, Language] = {}
273
+ for language in self._languages.values():
274
+ if language.is_etymology_only:
275
+ names[language.name] = language
276
+ for language in self._languages.values():
277
+ if not language.is_etymology_only:
278
+ names[language.name] = language
279
+ self._by_name = names
280
+ script_names: Dict[str, Script] = {}
281
+ for script in self._scripts.values():
282
+ if script._parent_code is not None:
283
+ script_names[script.name] = script
284
+ for script in self._scripts.values():
285
+ if script._parent_code is None:
286
+ script_names[script.name] = script
287
+ self._scripts_by_name = script_names
288
+ by_family: Dict[str, List[Language]] = {}
289
+ for language in sorted(self._languages.values(), key=lambda it: it.code):
290
+ if language._family_code:
291
+ by_family.setdefault(language._family_code, []).append(language)
292
+ self._by_family = by_family
293
+
294
+ @staticmethod
295
+ @functools.lru_cache(maxsize=None)
296
+ def bundled() -> "Catalog":
297
+ """English Wiktionary's languages, families and scripts as the package was built with them."""
298
+ text = resources.files("polyglossa").joinpath("catalog.tsv").read_text(encoding="utf-8")
299
+ return Catalog(text)
300
+
301
+ @property
302
+ def languages(self) -> List[Language]:
303
+ """Every language and etymology-only variety, sorted by code."""
304
+ return sorted(self._languages.values(), key=lambda it: it.code)
305
+
306
+ @property
307
+ def families(self) -> List[LanguageFamily]:
308
+ """Every family, sorted by code."""
309
+ return sorted(self._families.values(), key=lambda it: it.code)
310
+
311
+ @property
312
+ def scripts(self) -> List[Script]:
313
+ """Every script, sorted by code."""
314
+ return sorted(self._scripts.values(), key=lambda it: it.code)
315
+
316
+ def get(self, code: Optional[str]) -> Optional[Language]:
317
+ """The language with ``code``, or ``None``."""
318
+ return self._languages.get(code) if code is not None else None
319
+
320
+ def __getitem__(self, code: str) -> Language:
321
+ language = self.get(code)
322
+ if language is None:
323
+ raise UnknownLanguageError(code)
324
+ return language
325
+
326
+ def __contains__(self, code: object) -> bool:
327
+ return isinstance(code, str) and code in self._languages
328
+
329
+ def named(self, name: str) -> Optional[Language]:
330
+ """The language called ``name``; a full language wins over an etymology-only variety of the same name."""
331
+ return self._by_name.get(name.strip())
332
+
333
+ def family(self, code: Optional[str]) -> Optional[LanguageFamily]:
334
+ """The family with ``code``, or ``None``."""
335
+ return self._families.get(code) if code is not None else None
336
+
337
+ def script(self, code: Optional[str]) -> Optional[Script]:
338
+ """The script with ``code``, or ``None``."""
339
+ return self._scripts.get(code) if code is not None else None
340
+
341
+ def script_named(self, name: str) -> Optional[Script]:
342
+ """The script called ``name``; one that is no variety of another wins over one that is."""
343
+ return self._scripts_by_name.get(name.strip())
344
+
345
+ def _in_family(self, code: str) -> List[Language]:
346
+ return list(self._by_family.get(code, []))
347
+
348
+
349
+ _TYPES = {"": LanguageType.REGULAR, "r": LanguageType.RECONSTRUCTED, "a": LanguageType.APPENDIX_CONSTRUCTED,
350
+ "e": LanguageType.ETYMOLOGY_ONLY}
351
+ _DIRECTIONS = {"": ScriptDirection.LEFT_TO_RIGHT, "rtl": ScriptDirection.RIGHT_TO_LEFT,
352
+ "vertical-ltr": ScriptDirection.VERTICAL_LEFT_TO_RIGHT,
353
+ "vertical-rtl": ScriptDirection.VERTICAL_RIGHT_TO_LEFT}
354
+
355
+
356
+ def _read(text: str, catalog: Catalog) -> None:
357
+ lines = text.split("\n")
358
+ header = lines[0] if lines else ""
359
+ if header != HEADER and not header.startswith(HEADER + "\t"):
360
+ raise NotACatalogError("not a catalog Polyglossa wrote")
361
+ catalog.date = header[len(HEADER) + 1:] or None
362
+ for line in lines[1:]:
363
+ if not line:
364
+ continue
365
+ cells = _Cells(line.split("\t"))
366
+ kind = cells.next()
367
+ if kind == "L":
368
+ language = _language(cells, catalog)
369
+ catalog._languages[language.code] = language
370
+ elif kind == "F":
371
+ family = _family(cells, catalog)
372
+ catalog._families[family.code] = family
373
+ elif kind == "S":
374
+ script = _script(cells, catalog)
375
+ catalog._scripts[script.code] = script
376
+ else:
377
+ raise NotACatalogError(f"not a language, a family or a script: {line}")
378
+
379
+
380
+ class _Cells:
381
+ """The cells of one line, read in order; one left off the end reads as empty."""
382
+
383
+ def __init__(self, cells: List[str]) -> None:
384
+ self._cells = cells
385
+ self._at = 0
386
+
387
+ def next(self) -> str:
388
+ cell = self._cells[self._at] if self._at < len(self._cells) else ""
389
+ self._at += 1
390
+ return cell
391
+
392
+
393
+ def _language(cells: _Cells, catalog: Catalog) -> Language:
394
+ code = cells.next()
395
+ name = _unescape(cells.next())
396
+ mark = cells.next()
397
+ if mark not in _TYPES:
398
+ raise NotACatalogError(f"no language type {mark}")
399
+ wikidata = _item(cells.next())
400
+ family = cells.next() or None
401
+ scripts = _codes(cells.next())
402
+ ancestors = _codes(cells.next())
403
+ aliases = _names(cells.next())
404
+ parent = cells.next() or None
405
+ varieties = []
406
+ for variety in _split(cells.next(), ";"):
407
+ names = [_unescape(it) for it in _split(variety, "|")]
408
+ varieties.append(Variety(names[0], tuple(names[1:])))
409
+ return Language(code, name, _TYPES[mark], wikidata, aliases, tuple(varieties), family, scripts, ancestors,
410
+ parent, catalog)
411
+
412
+
413
+ def _family(cells: _Cells, catalog: Catalog) -> LanguageFamily:
414
+ code = cells.next()
415
+ name = _unescape(cells.next())
416
+ wikidata = _item(cells.next())
417
+ parent = cells.next() or None
418
+ proto = cells.next() or None
419
+ aliases = _names(cells.next())
420
+ return LanguageFamily(code, name, wikidata, aliases, parent, proto, catalog)
421
+
422
+
423
+ def _script(cells: _Cells, catalog: Catalog) -> Script:
424
+ code = cells.next()
425
+ name = _unescape(cells.next())
426
+ wikidata = _item(cells.next())
427
+ system = _unescape(cells.next()) or None
428
+ aliases = _names(cells.next())
429
+ parent = cells.next() or None
430
+ direction = _DIRECTIONS.get(cells.next(), ScriptDirection.LEFT_TO_RIGHT)
431
+ local = []
432
+ for entry in _names(cells.next()):
433
+ language, _, local_name = entry.partition("=")
434
+ local.append((language, local_name))
435
+ return Script(code, name, wikidata, system, aliases, direction, parent, tuple(local), catalog)
436
+
437
+
438
+ def _item(cell: str) -> Optional[str]:
439
+ return "Q" + cell if cell else None
440
+
441
+
442
+ def _codes(cell: str) -> Tuple[str, ...]:
443
+ return tuple(cell.split(",")) if cell else ()
444
+
445
+
446
+ def _names(cell: str) -> Tuple[str, ...]:
447
+ return tuple(_unescape(it) for it in _split(cell, ";"))
448
+
449
+
450
+ def _split(cell: str, separator: str) -> List[str]:
451
+ """``cell`` split at each ``separator`` that is not escaped, the pieces left escaped; none for an empty cell."""
452
+ if not cell:
453
+ return []
454
+ pieces, current, escaped = [], [], False
455
+ for character in cell:
456
+ if escaped:
457
+ current.append(character)
458
+ escaped = False
459
+ elif character == "\\":
460
+ current.append(character)
461
+ escaped = True
462
+ elif character == separator:
463
+ pieces.append("".join(current))
464
+ current = []
465
+ else:
466
+ current.append(character)
467
+ pieces.append("".join(current))
468
+ return pieces
469
+
470
+
471
+ def _unescape(text: str) -> str:
472
+ """``text`` with the Kotlin library's escapes undone: ``\\t`` and ``\\n``, and any other character as itself."""
473
+ if "\\" not in text:
474
+ return text
475
+ out, escaped = [], False
476
+ for character in text:
477
+ if escaped:
478
+ out.append({"t": "\t", "n": "\n"}.get(character, character))
479
+ escaped = False
480
+ elif character == "\\":
481
+ escaped = True
482
+ else:
483
+ out.append(character)
484
+ if escaped:
485
+ out.append("\\")
486
+ return "".join(out)
polyglossa/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2026.11.6"