From a5022c19179efaa3f428422d05407f647a600903 Mon Sep 17 00:00:00 2001 From: Mate Barany Date: Thu, 10 Oct 2024 17:02:56 +0200 Subject: [PATCH] Add type annotations to CldrAccess Task-number: QTBUG-129613 Change-Id: I8a00cca718554909b7ab9dcad15cc9b9ac702e94 Reviewed-by: Edward Welbourne (cherry picked from commit defd1549de9a26607e888fae8d82029633ca6d17) --- util/locale_database/cldr.py | 116 +++++++++++++++------------- util/locale_database/localetools.py | 2 +- 2 files changed, 65 insertions(+), 53 deletions(-) diff --git a/util/locale_database/cldr.py b/util/locale_database/cldr.py index 254cb18b6b..825006e2df 100644 --- a/util/locale_database/cldr.py +++ b/util/locale_database/cldr.py @@ -10,7 +10,7 @@ The former should normally be all you need to access. See individual classes for further detail. """ -from typing import Iterable, TextIO +from typing import Callable, Iterable, Iterator, TextIO from xml.dom import minidom from weakref import WeakValueDictionary as CacheDict from pathlib import Path @@ -294,7 +294,7 @@ class CldrReader (object): # the cache. If a process were to instantiate this class with distinct # roots, each cache would be filled by the first to need it ! class CldrAccess (object): - def __init__(self, root: Path): + def __init__(self, root: Path) -> None: """Set up a master object for accessing CLDR data. Single parameter, root, is the file-system path to the root of @@ -302,20 +302,20 @@ class CldrAccess (object): contain dtd/, main/ and supplemental/ sub-directories.""" self.root = root - def xml(self, relative_path: str): + def xml(self, relative_path: str) -> XmlScanner: """Load a single XML file and return its root element as an XmlScanner. The path is interpreted relative to self.root""" return XmlScanner(Node(self.__xml(relative_path))) - def supplement(self, name): + def supplement(self, name: str) -> Supplement: """Loads supplemental data as a Supplement object. The name should be that of a file in common/supplemental/, without path. """ return Supplement(Node(self.__xml(f'common/supplemental/{name}'))) - def locale(self, name): + def locale(self, name: str) -> LocaleScanner: """Loads all data for a locale as a LocaleScanner object. The name should be a locale name; adding suffix '.xml' to it @@ -325,7 +325,7 @@ class CldrAccess (object): inheritance, where relevant.""" return LocaleScanner(name, self.__localeRoots(name), self.__rootLocale) - def englishNaming(self, tag): # see QLocaleXmlWriter.enumData() + def englishNaming(self, tag: str) -> Callable[[str], str]: # see QLocaleXmlWriter.enumData() return self.__codeMap(tag).get @property @@ -339,18 +339,18 @@ class CldrAccess (object): yield path.stem @property - def defaultContentLocales(self): + def defaultContentLocales(self) -> Iterator[str]: """Generator for the default content locales.""" for name, attrs in self.supplement('supplementalMetadata.xml').find('metadata/defaultContent'): try: - locales = attrs['locales'] + locales: str = attrs['locales'] except KeyError: pass else: for locale in locales.split(): yield locale - def likelySubTags(self): + def likelySubTags(self) -> Iterator[tuple[str, str]]: for ignore, attrs in self.supplement('likelySubtags.xml').find('likelySubtags'): yield attrs['from'], attrs['to'] @@ -365,7 +365,7 @@ class CldrAccess (object): except KeyError: raise Error(f'Unsupported number system: {system}') - def weekData(self, territory): + def weekData(self, territory: str) -> tuple[str, str, str]: """Data on the weekly cycle. Returns a triple (W, S, E) of en's short names for week-days; @@ -378,7 +378,7 @@ class CldrAccess (object): except KeyError: return self.__weekData['001'] - def currencyData(self, territory): + def currencyData(self, territory: str) -> tuple[str, int, int]: """Returns currency data for the given territory code. Return value is a tuple (ISO4217 code, digit count, rounding @@ -390,7 +390,9 @@ class CldrAccess (object): except KeyError: return '', 2, 1 - def codesToIdName(self, language, script, territory, variant = ''): + def codesToIdName(self, language: str, script: str, territory: str, variant: str = '' + ) -> tuple[tuple[int, str], tuple[int, str], + tuple[int, str], tuple[int, str]]: """Maps each code to the appropriate ID and name. Returns a 4-tuple of (ID, name) pairs corresponding to the @@ -402,7 +404,7 @@ class CldrAccess (object): Until we implement variant support (QTBUG-81051), the fourth member of the returned tuple is always 0 paired with a string that should not be used.""" - enum = self.__enumMap + enum: Callable[[str], dict[str, tuple[int, str]]] = self.__enumMap try: return (enum('language')[language], enum('script')[script], @@ -413,8 +415,9 @@ class CldrAccess (object): parts, values = [], [language, script, territory, variant] for index, key in enumerate(('language', 'script', 'territory', 'variant')): - naming, enums = self.__codeMap(key), enum(key) - value = values[index] + naming: dict[str, str] = self.__codeMap(key) + enums: dict[str, tuple[int, str]] = enum(key) + value: str = values[index] if value not in enums: text = f'{key} code {value}' name = naming.get(value) @@ -432,21 +435,22 @@ class CldrAccess (object): language, script, territory, variant) @staticmethod - def __checkEnum(given, proper, scraps): + def __checkEnum(given: dict[str, str], proper: dict[str, str], scraps: set[str] + ) -> Iterator[tuple[str, str]]: # Each is a { code: full name } mapping for code, name in given.items(): - try: right = proper[code] + try: right: str = proper[code] except KeyError: # No en.xml name for this code, but supplementalData's # parentLocale may still believe in it: if code not in scraps: yield name, f'[Found no CLDR name for code {code}]' continue - cleaned = names_clash(right, name) + cleaned: None | str = names_clash(right, name) if cleaned: yield name, cleaned - def checkEnumData(self, grumble): + def checkEnumData(self, grumble: Callable[[str], int]) -> None: scraps = set() for k in self.__parentLocale.keys(): for f in k.split('_'): @@ -477,7 +481,7 @@ enumdata.py (keeping the old name as an alias): + '\n') grumble('\n') - def bcp47Aliases(self): + def bcp47Aliases(self) -> tuple[dict[str, str], dict[str, str]]: """Reads the mapping from CLDR IDs to IANA IDs CLDR identifies timezones in various ways but its standard @@ -515,7 +519,8 @@ enumdata.py (keeping the old name as an alias): # If we ever need a mapping back to CLDR ID, we can make # (description, space-joined-list) the naming values. - alias, naming = {}, {} # { alias: iana }, { iana: description } + alias: dict[str, str] = {} # { alias: iana } + naming: dict[str, str] = {} # { iana: description } for item, attrs in root.find('keyword/key/type', exclude=('deprecated',)): assert 'description' in attrs, item assert 'alias' in attrs, item @@ -530,7 +535,8 @@ enumdata.py (keeping the old name as an alias): return alias, naming - def readWindowsTimeZones(self, alias): + def readWindowsTimeZones(self, alias: dict[str, str]) -> tuple[dict[str, str], + list[tuple[str, str, str]]]: """Digest CLDR's MS-Win time-zone name mapping. Single argument, alias, should be the first part of the pair @@ -567,7 +573,8 @@ enumdata.py (keeping the old name as an alias): mapZone element and the last is s, its cleaned-up list of IANA IDs.""" - defaults, windows = {}, [] + defaults: dict[str, str] = {} + windows: list[tuple[str, str, str]] = [] zones = self.supplement('windowsZones.xml') for name, attrs in zones.find('windowsZones/mapTimezones'): if name != 'mapZone': @@ -588,36 +595,36 @@ enumdata.py (keeping the old name as an alias): return defaults, windows @property - def cldrVersion(self): + def cldrVersion(self) -> str: # Evaluate so as to ensure __cldrVersion is set: self.__unDistinguishedAttributes return self.__cldrVersion # Implementation details - def __xml(self, relative_path: str, cache = CacheDict(), read = minidom.parse): + def __xml(self, relPath: str, cache = CacheDict(), read = minidom.parse) -> minidom.Element: try: - doc = cache[relative_path] + doc: minidom.Element = cache[relPath] except KeyError: - cache[relative_path] = doc = read(str(self.root.joinpath(relative_path))).documentElement + cache[relPath] = doc = read(str(self.root.joinpath(relPath))).documentElement return doc def __open(self, relative_path: str) -> TextIO: return self.root.joinpath(relative_path).open() @property - def __rootLocale(self, cache = []): + def __rootLocale(self, cache: list[XmlScanner] = []) -> XmlScanner: if not cache: cache.append(self.xml('common/main/root.xml')) return cache[0] @property - def __supplementalData(self, cache = []): + def __supplementalData(self, cache: list[Supplement] = []) -> Supplement: if not cache: cache.append(self.supplement('supplementalData.xml')) return cache[0] @property - def __numberSystems(self, cache = {}): + def __numberSystems(self, cache: dict[str, dict[str, str]] = {}) -> dict[str, dict[str, str]]: if not cache: for ignore, attrs in self.supplement('numberingSystems.xml').find('numberingSystems'): cache[attrs['id']] = attrs @@ -625,20 +632,22 @@ enumdata.py (keeping the old name as an alias): return cache @property - def __weekData(self, cache = {}): + def __weekData(self, cache: dict[str, tuple[str, str, str]] = {} + ) -> dict[str, tuple[str, str, str]]: if not cache: + # firstDay, weStart and weEnd are all dict[str, str] firstDay, weStart, weEnd = self.__getWeekData() # Massage those into an easily-consulted form: # World defaults given for code '001': mon, sat, sun = firstDay['001'], weStart['001'], weEnd['001'] - lands = set(firstDay) | set(weStart) | set(weEnd) + lands: set[str] = set(firstDay) | set(weStart) | set(weEnd) cache.update((land, (firstDay.get(land, mon), weStart.get(land, sat), weEnd.get(land, sun))) for land in lands) assert cache return cache - def __getWeekData(self): + def __getWeekData(self) -> Iterator[dict[str, str]]: """Scan for data on the weekly cycle. Yields three mappings from locales to en's short names for @@ -647,12 +656,12 @@ enumdata.py (keeping the old name as an alias): gives the day on which the week starts, the second gives the day on which the week-end starts, the third gives the last day of the week-end.""" - source = self.__supplementalData + source: Supplement = self.__supplementalData for key in ('firstDay', 'weekendStart', 'weekendEnd'): - result = {} + result: dict[str, str] = {} for ignore, attrs in source.find(f'weekData/{key}'): assert ignore == key - day = attrs['day'] + day: str = attrs['day'] assert day in ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'), day if 'alt' in attrs: continue @@ -661,7 +670,8 @@ enumdata.py (keeping the old name as an alias): yield result @property - def __currencyData(self, cache = {}): + def __currencyData(self, cache: dict[str, tuple[str, int, int]] = {} + ) -> dict[str, tuple[str, int, int]]: if not cache: source = self.__supplementalData for elt in source.findNodes('currencyData/region'): @@ -686,15 +696,16 @@ enumdata.py (keeping the old name as an alias): if iso: for tag, data in source.find( f'currencyData/fractions/info[iso4217={iso}]'): - digits = data['digits'] - rounding = data['rounding'] + digits = int(data['digits']) + rounding = int(data['rounding']) cache[territory] = iso, digits, rounding assert cache return cache @property - def __unDistinguishedAttributes(self, cache = {}): + def __unDistinguishedAttributes(self, cache: dict[str, tuple[str, ...]] = {} + ) -> dict[str, tuple[str, ...]]: """Mapping from tag names to lists of attributes. LDML defines some attributes as 'distinguishing': if a node @@ -714,7 +725,7 @@ enumdata.py (keeping the old name as an alias): return cache - def __scanLdmlDtd(self): + def __scanLdmlDtd(self) -> Iterator[tuple[str, tuple[str, ...]]]: """Scan the LDML DTD, record CLDR version Yields (tag, attrs) pairs: on elements with a given tag, @@ -756,7 +767,8 @@ enumdata.py (keeping the old name as an alias): if tag and ignored: yield tag, tuple(ignored) - def __enumMap(self, key, cache = {}): + def __enumMap(self, key: str, cache: dict[str, dict[str, tuple[int, str]]] = {} + ) -> dict[str, tuple[int, str]]: if not cache: cache['variant'] = {'': (0, 'This should never be seen outside ldml.py')} # They're mappings from numeric value to pairs of full @@ -779,19 +791,19 @@ enumdata.py (keeping the old name as an alias): return cache[key] - def __codeMap(self, key, cache = {}, + def __codeMap(self, key: str, cache: dict[str, dict[str, str]] = {}, # Maps our name for it to CLDR's name: naming = {'language': 'languages', 'script': 'scripts', - 'territory': 'territories', 'variant': 'variants'}): + 'territory': 'territories', 'variant': 'variants'}) -> dict[str, str]: if not cache: - root = self.xml('common/main/en.xml').root.findUniqueChild('localeDisplayNames') + root: Node = self.xml('common/main/en.xml').root.findUniqueChild('localeDisplayNames') for dst, src in naming.items(): cache[dst] = dict(self.__codeMapScan(root.findUniqueChild(src))) assert cache return cache[key] - def __codeMapScan(self, node): + def __codeMapScan(self, node: Node) -> Iterator[tuple[str, str]]: """Get mapping from codes to element values. Passed in node is a , , or @@ -822,12 +834,12 @@ enumdata.py (keeping the old name as an alias): # CLDR uses inheritance between locales to save repetition: @property - def __parentLocale(self, cache = {}): + def __parentLocale(self, cache: dict[str, str] = {}) -> dict[str, str]: # see http://www.unicode.org/reports/tr35/#Parent_Locales if not cache: for tag, attrs in self.__supplementalData.find('parentLocales', ('component',)): - parent = attrs.get('parent', '') + parent: str = attrs.get('parent', '') for child in attrs['locales'].split(): cache[child] = parent assert cache @@ -852,7 +864,7 @@ enumdata.py (keeping the old name as an alias): raise Error(f'Fatal error: found an alias "{aliasFor}" -> "{name}", ' 'but found no file for the alias') - def __scanLocaleRoots(self, name): + def __scanLocaleRoots(self, name: str) -> Iterator[Node]: while name and name != 'root': doc = self.__localeAsDoc(name) if doc is not None: @@ -867,11 +879,11 @@ enumdata.py (keeping the old name as an alias): break class __Seq (list): pass # No weakref for tuple and list, but list sub-class is ok. - def __localeRoots(self, name, cache = CacheDict()): + def __localeRoots(self, name: str, cache = CacheDict()) -> __Seq: try: - chain = cache[name] + chain: CldrAccess.__Seq = cache[name] except KeyError: - cache[name] = chain = self.__Seq(self.__scanLocaleRoots(name)) + cache[name] = chain = CldrAccess.__Seq(self.__scanLocaleRoots(name)) return chain # Unpolute the namespace: we don't need to export these. diff --git a/util/locale_database/localetools.py b/util/locale_database/localetools.py index f170b8a6a6..c1b9cd3833 100644 --- a/util/locale_database/localetools.py +++ b/util/locale_database/localetools.py @@ -48,7 +48,7 @@ def wrap_list(lst, perline=20): yield head return ",\n".join(", ".join(x) for x in split(lst, perline)) -def names_clash(cldr, enum): +def names_clash(cldr: str, enum: str) -> None | str: """True if the reader might not recognize cldr as the name of enum First argument, cldr, is the name CLDR gives for some language,