polyglossa 2026.11.6__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fenakhay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ Polyglossa
2
+ Copyright (c) 2026 fenakhay
3
+
4
+ polyglossa/data/catalog.tsv, and the copy of it built into Polyglossa, holds English Wiktionary's language,
5
+ language family and script data, exported by Module:JSON data from Module:languages, Module:etymology
6
+ languages, Module:families and Module:scripts (https://en.wiktionary.org). It is available under the
7
+ Creative Commons Attribution-ShareAlike 4.0 License (https://creativecommons.org/licenses/by-sa/4.0/), not
8
+ under Polyglossa's own licence; see the page histories on English Wiktionary for its authors.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: polyglossa
3
+ Version: 2026.11.6
4
+ Summary: English Wiktionary's languages, language families and scripts.
5
+ Author: Fenakhay
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/fenakhay/polyglossa
8
+ Project-URL: Source, https://github.com/fenakhay/polyglossa
9
+ Keywords: wiktionary,languages,language codes,scripts,linguistics
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Text Processing :: Linguistic
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ License-File: NOTICE
17
+ Dynamic: license-file
18
+
19
+ # Polyglossa
20
+
21
+ English Wiktionary's languages, language families and scripts for Python: every code Wiktionary uses,
22
+ what it calls each one, and how they relate, bundled with the package so nothing is asked of the wiki.
23
+
24
+ ```python
25
+ from polyglossa import Language, LanguageFamily, Script
26
+
27
+ english = Language["en"]
28
+ english.name # 'English'
29
+ english.family.name # 'Anglic'
30
+ [a.name for a in english.ancestors] # ['Early Modern English']
31
+ english.scripts[0].name # 'Latin'
32
+ Language.get("xx") # None: no such language
33
+ Language.named("Old English").code # 'ang'
34
+ "ang" in Language # True
35
+ Language["en-ear"].full # English, for an etymology-only variety
36
+
37
+ LanguageFamily["ine"].proto_language.name # 'Proto-Indo-European'
38
+ Script.named("Arabic").direction # ScriptDirection.RIGHT_TO_LEFT
39
+ Script["Deva"].name_for(Language["mr"]) # 'Balbodh'
40
+ ```
41
+
42
+ `Language["xx"]` raises `UnknownLanguageError`, a `KeyError`; `Language.get("xx")` returns `None`. It
43
+ needs Python 3.11 or later and nothing else.
44
+
45
+ The version is the day the data was exported from Wiktionary. The data is English Wiktionary's, under
46
+ CC BY-SA 4.0; the code is under the MIT licence. See https://github.com/fenakhay/polyglossa.
@@ -0,0 +1,28 @@
1
+ # Polyglossa
2
+
3
+ English Wiktionary's languages, language families and scripts for Python: every code Wiktionary uses,
4
+ what it calls each one, and how they relate, bundled with the package so nothing is asked of the wiki.
5
+
6
+ ```python
7
+ from polyglossa import Language, LanguageFamily, Script
8
+
9
+ english = Language["en"]
10
+ english.name # 'English'
11
+ english.family.name # 'Anglic'
12
+ [a.name for a in english.ancestors] # ['Early Modern English']
13
+ english.scripts[0].name # 'Latin'
14
+ Language.get("xx") # None: no such language
15
+ Language.named("Old English").code # 'ang'
16
+ "ang" in Language # True
17
+ Language["en-ear"].full # English, for an etymology-only variety
18
+
19
+ LanguageFamily["ine"].proto_language.name # 'Proto-Indo-European'
20
+ Script.named("Arabic").direction # ScriptDirection.RIGHT_TO_LEFT
21
+ Script["Deva"].name_for(Language["mr"]) # 'Balbodh'
22
+ ```
23
+
24
+ `Language["xx"]` raises `UnknownLanguageError`, a `KeyError`; `Language.get("xx")` returns `None`. It
25
+ needs Python 3.11 or later and nothing else.
26
+
27
+ The version is the day the data was exported from Wiktionary. The data is English Wiktionary's, under
28
+ CC BY-SA 4.0; the code is under the MIT licence. See https://github.com/fenakhay/polyglossa.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "polyglossa"
7
+ dynamic = ["version"]
8
+ description = "English Wiktionary's languages, language families and scripts."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE", "NOTICE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Fenakhay" }]
14
+ keywords = ["wiktionary", "languages", "language codes", "scripts", "linguistics"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Text Processing :: Linguistic",
18
+ "Typing :: Typed",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/fenakhay/polyglossa"
23
+ Source = "https://github.com/fenakhay/polyglossa"
24
+
25
+ [tool.setuptools.dynamic]
26
+ version = { attr = "polyglossa._version.__version__" }
27
+
28
+ [tool.setuptools.package-data]
29
+ polyglossa = ["catalog.tsv", "py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -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)
@@ -0,0 +1 @@
1
+ __version__ = "2026.11.6"