codekingpro/portable-devtools
114k
1"""2 babel.localedata3 ~~~~~~~~~~~~~~~~4 5 Low-level locale data access.6 7 :note: The `Locale` class, which uses this module under the hood, provides a8 more convenient interface for accessing the locale data.9 10 :copyright: (c) 2013-2024 by the Babel Team.11 :license: BSD, see LICENSE for more details.12"""13 14from __future__ import annotations15 16import os17import pickle18import re19import sys20import threading21from collections import abc22from collections.abc import Iterator, Mapping, MutableMapping23from functools import lru_cache24from itertools import chain25from typing import Any26 27_cache: dict[str, Any] = {}28_cache_lock = threading.RLock()29_dirname = os.path.join(os.path.dirname(__file__), 'locale-data')30_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I)31 32 33def normalize_locale(name: str) -> str | None:34 """Normalize a locale ID by stripping spaces and apply proper casing.35 36 Returns the normalized locale ID string or `None` if the ID is not37 recognized.38 """39 if not name or not isinstance(name, str):40 return None41 name = name.strip().lower()42 for locale_id in chain.from_iterable([_cache, locale_identifiers()]):43 if name == locale_id.lower():44 return locale_id45 46 47def resolve_locale_filename(name: os.PathLike[str] | str) -> str:48 """49 Resolve a locale identifier to a `.dat` path on disk.50 """51 52 # Clean up any possible relative paths.53 name = os.path.basename(name)54 55 # Ensure we're not left with one of the Windows reserved names.56 if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]):57 raise ValueError(f"Name {name} is invalid on Windows")58 59 # Build the path.60 return os.path.join(_dirname, f"{name}.dat")61 62 63def exists(name: str) -> bool:64 """Check whether locale data is available for the given locale.65 66 Returns `True` if it exists, `False` otherwise.67 68 :param name: the locale identifier string69 """70 if not name or not isinstance(name, str):71 return False72 if name in _cache:73 return True74 file_found = os.path.exists(resolve_locale_filename(name))75 return True if file_found else bool(normalize_locale(name))76 77 78@lru_cache(maxsize=None)79def locale_identifiers() -> list[str]:80 """Return a list of all locale identifiers for which locale data is81 available.82 83 This data is cached after the first invocation.84 You can clear the cache by calling `locale_identifiers.cache_clear()`.85 86 .. versionadded:: 0.8.187 88 :return: a list of locale identifiers (strings)89 """90 return [91 stem92 for stem, extension in93 (os.path.splitext(filename) for filename in os.listdir(_dirname))94 if extension == '.dat' and stem != 'root'95 ]96 97 98def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:99 """Load the locale data for the given locale.100 101 The locale data is a dictionary that contains much of the data defined by102 the Common Locale Data Repository (CLDR). This data is stored as a103 collection of pickle files inside the ``babel`` package.104 105 >>> d = load('en_US')106 >>> d['languages']['sv']107 u'Swedish'108 109 Note that the results are cached, and subsequent requests for the same110 locale return the same dictionary:111 112 >>> d1 = load('en_US')113 >>> d2 = load('en_US')114 >>> d1 is d2115 True116 117 :param name: the locale identifier string (or "root")118 :param merge_inherited: whether the inherited data should be merged into119 the data of the requested locale120 :raise `IOError`: if no locale data file is found for the given locale121 identifier, or one of the locales it inherits from122 """123 name = os.path.basename(name)124 _cache_lock.acquire()125 try:126 data = _cache.get(name)127 if not data:128 # Load inherited data129 if name == 'root' or not merge_inherited:130 data = {}131 else:132 from babel.core import get_global133 parent = get_global('parent_exceptions').get(name)134 if not parent:135 parts = name.split('_')136 parent = "root" if len(parts) == 1 else "_".join(parts[:-1])137 data = load(parent).copy()138 filename = resolve_locale_filename(name)139 with open(filename, 'rb') as fileobj:140 if name != 'root' and merge_inherited:141 merge(data, pickle.load(fileobj))142 else:143 data = pickle.load(fileobj)144 _cache[name] = data145 return data146 finally:147 _cache_lock.release()148 149 150def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:151 """Merge the data from `dict2` into the `dict1` dictionary, making copies152 of nested dictionaries.153 154 >>> d = {1: 'foo', 3: 'baz'}155 >>> merge(d, {1: 'Foo', 2: 'Bar'})156 >>> sorted(d.items())157 [(1, 'Foo'), (2, 'Bar'), (3, 'baz')]158 159 :param dict1: the dictionary to merge into160 :param dict2: the dictionary containing the data that should be merged161 """162 for key, val2 in dict2.items():163 if val2 is not None:164 val1 = dict1.get(key)165 if isinstance(val2, dict):166 if val1 is None:167 val1 = {}168 if isinstance(val1, Alias):169 val1 = (val1, val2)170 elif isinstance(val1, tuple):171 alias, others = val1172 others = others.copy()173 merge(others, val2)174 val1 = (alias, others)175 else:176 val1 = val1.copy()177 merge(val1, val2)178 else:179 val1 = val2180 dict1[key] = val1181 182 183class Alias:184 """Representation of an alias in the locale data.185 186 An alias is a value that refers to some other part of the locale data,187 as specified by the `keys`.188 """189 190 def __init__(self, keys: tuple[str, ...]) -> None:191 self.keys = tuple(keys)192 193 def __repr__(self) -> str:194 return f"<{type(self).__name__} {self.keys!r}>"195 196 def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:197 """Resolve the alias based on the given data.198 199 This is done recursively, so if one alias resolves to a second alias,200 that second alias will also be resolved.201 202 :param data: the locale data203 :type data: `dict`204 """205 base = data206 for key in self.keys:207 data = data[key]208 if isinstance(data, Alias):209 data = data.resolve(base)210 elif isinstance(data, tuple):211 alias, others = data212 data = alias.resolve(base)213 return data214 215 216class LocaleDataDict(abc.MutableMapping):217 """Dictionary wrapper that automatically resolves aliases to the actual218 values.219 """220 221 def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None):222 self._data = data223 if base is None:224 base = data225 self.base = base226 227 def __len__(self) -> int:228 return len(self._data)229 230 def __iter__(self) -> Iterator[str | int | None]:231 return iter(self._data)232 233 def __getitem__(self, key: str | int | None) -> Any:234 orig = val = self._data[key]235 if isinstance(val, Alias): # resolve an alias236 val = val.resolve(self.base)237 if isinstance(val, tuple): # Merge a partial dict with an alias238 alias, others = val239 val = alias.resolve(self.base).copy()240 merge(val, others)241 if isinstance(val, dict): # Return a nested alias-resolving dict242 val = LocaleDataDict(val, base=self.base)243 if val is not orig:244 self._data[key] = val245 return val246 247 def __setitem__(self, key: str | int | None, value: Any) -> None:248 self._data[key] = value249 250 def __delitem__(self, key: str | int | None) -> None:251 del self._data[key]252 253 def copy(self) -> LocaleDataDict:254 return LocaleDataDict(self._data.copy(), base=self.base)255 