Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/localedata.py: 26%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2babel.localedata
3~~~~~~~~~~~~~~~~
5Low-level locale data access.
7:note: The `Locale` class, which uses this module under the hood, provides a
8 more convenient interface for accessing the locale data.
10:copyright: (c) 2013-2026 by the Babel Team.
11:license: BSD, see LICENSE for more details.
12"""
14from __future__ import annotations
16import os
17import pickle
18import re
19import sys
20import threading
21from collections import abc
22from collections.abc import Iterator, Mapping, MutableMapping
23from functools import lru_cache
24from itertools import chain
25from typing import Any
27_cache: dict[str, Any] = {}
28_dict_cache: dict[str, LocaleDataDict] = {}
29_cache_lock = threading.RLock()
30_dirname = os.path.join(os.path.dirname(__file__), 'locale-data')
31_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I)
34def normalize_locale(name: str) -> str | None:
35 """Normalize a locale ID by stripping spaces and apply proper casing.
37 Returns the normalized locale ID string or `None` if the ID is not
38 recognized.
39 """
40 if not name or not isinstance(name, str):
41 return None
42 name = name.strip().lower()
43 for locale_id in chain.from_iterable([_cache, locale_identifiers()]):
44 if name == locale_id.lower():
45 return locale_id
48def resolve_locale_filename(name: os.PathLike[str] | str) -> str:
49 """
50 Resolve a locale identifier to a `.dat` path on disk.
51 """
53 # Clean up any possible relative paths.
54 name = os.path.basename(name)
56 # Ensure we're not left with one of the Windows reserved names.
57 if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]):
58 raise ValueError(f"Name {name} is invalid on Windows")
60 # Build the path.
61 return os.path.join(_dirname, f"{name}.dat")
64@lru_cache(maxsize=None)
65def exists(name: str) -> bool:
66 """Check whether locale data is available for the given locale.
68 Returns `True` if it exists, `False` otherwise.
70 :param name: the locale identifier string
71 """
72 if not name or not isinstance(name, str):
73 return False
74 if name in _cache:
75 return True
76 file_found = os.path.exists(resolve_locale_filename(name))
77 return file_found or bool(normalize_locale(name))
80@lru_cache(maxsize=None)
81def locale_identifiers() -> list[str]:
82 """Return a list of all locale identifiers for which locale data is
83 available.
85 This data is cached after the first invocation.
86 You can clear the cache by calling `locale_identifiers.cache_clear()`.
88 .. versionadded:: 0.8.1
90 :return: a list of locale identifiers (strings)
91 """
92 return [
93 stem
94 for stem, extension in (
95 os.path.splitext(filename) for filename in os.listdir(_dirname)
96 )
97 if extension == '.dat' and stem != 'root'
98 ]
101def _is_non_likely_script(name: str) -> bool:
102 """Return whether the locale is of the form ``lang_Script``,
103 and the script is not the likely script for the language.
105 This implements the behavior of the ``nonlikelyScript`` value of the
106 ``localRules`` attribute for parent locales added in CLDR 45.
107 """
108 from babel.core import get_global, parse_locale
110 try:
111 lang, territory, script, variant, *rest = parse_locale(name)
112 except ValueError:
113 return False
115 if lang and script and not territory and not variant and not rest:
116 likely_subtag = get_global('likely_subtags').get(lang)
117 _, _, likely_script, *_ = parse_locale(likely_subtag)
118 return script != likely_script
119 return False
122def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
123 """Load the locale data for the given locale.
125 The locale data is a dictionary that contains much of the data defined by
126 the Common Locale Data Repository (CLDR). This data is stored as a
127 collection of pickle files inside the ``babel`` package.
129 >>> d = load('en_US')
130 >>> d['languages']['sv']
131 'Swedish'
133 Note that the results are cached, and subsequent requests for the same
134 locale return the same dictionary:
136 >>> d1 = load('en_US')
137 >>> d2 = load('en_US')
138 >>> d1 is d2
139 True
141 :param name: the locale identifier string (or "root")
142 :param merge_inherited: whether the inherited data should be merged into
143 the data of the requested locale
144 :raise `IOError`: if no locale data file is found for the given locale
145 identifier, or one of the locales it inherits from
146 """
147 name = os.path.basename(name)
148 _cache_lock.acquire()
149 try:
150 data = _cache.get(name)
151 if not data:
152 # Load inherited data
153 if name == 'root' or not merge_inherited:
154 data = {}
155 else:
156 from babel.core import get_global
158 parent = get_global('parent_exceptions').get(name)
159 if not parent:
160 if _is_non_likely_script(name):
161 parent = 'root'
162 else:
163 parts = name.split('_')
164 parent = "root" if len(parts) == 1 else "_".join(parts[:-1])
165 data = load(parent).copy()
166 filename = resolve_locale_filename(name)
167 with open(filename, 'rb') as fileobj:
168 if name != 'root' and merge_inherited:
169 merge(data, pickle.load(fileobj))
170 else:
171 data = pickle.load(fileobj)
172 _cache[name] = data
173 return data
174 finally:
175 _cache_lock.release()
178def clear_caches() -> None:
179 """
180 Clear locale data caches.
181 """
182 with _cache_lock:
183 _cache.clear()
184 _dict_cache.clear()
187def get_locale_data(name: str) -> LocaleDataDict:
188 """Return an alias-resolving `LocaleDataDict` over the merged data for
189 the given locale.
191 The wrapper is cached and shared: repeated requests for the same locale
192 return the same object. Alias resolutions memoized within it are
193 locale-specific.
194 """
195 try:
196 return _dict_cache[name]
197 except KeyError:
198 return _dict_cache.setdefault(name, LocaleDataDict(load(name)))
201def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:
202 """Merge the data from `dict2` into the `dict1` dictionary, making copies
203 of nested dictionaries.
205 >>> d = {1: 'foo', 3: 'baz'}
206 >>> merge(d, {1: 'Foo', 2: 'Bar'})
207 >>> sorted(d.items())
208 [(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
210 :param dict1: the dictionary to merge into
211 :param dict2: the dictionary containing the data that should be merged
212 """
213 for key, val2 in dict2.items():
214 if val2 is not None:
215 val1 = dict1.get(key)
216 if isinstance(val2, dict):
217 if val1 is None:
218 val1 = {}
219 if isinstance(val1, Alias):
220 # A dict overriding an alias becomes an `(alias, overrides)` tuple
221 # resolved in `Alias.resolve` or `LocaleDataDict.__getitem__`.
222 val1 = (val1, val2)
223 elif isinstance(val1, tuple):
224 alias, others = val1
225 others = others.copy()
226 merge(others, val2)
227 val1 = (alias, others)
228 else:
229 val1 = val1.copy()
230 merge(val1, val2)
231 else:
232 val1 = val2
233 dict1[key] = val1
236class Alias:
237 """Representation of an alias in the locale data.
239 An alias is a value that refers to some other part of the locale data,
240 as specified by the `keys`.
241 """
243 def __init__(self, keys: tuple[str, ...]) -> None:
244 self.keys = tuple(keys)
246 def __repr__(self) -> str:
247 return f"<{type(self).__name__} {self.keys!r}>"
249 def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:
250 """Resolve the alias based on the given data.
252 This is done recursively, so if one alias resolves to a second alias,
253 that second alias will also be resolved.
255 :param data: the locale data
256 """
257 base = data
258 for key in self.keys:
259 data = data[key]
260 if isinstance(data, Alias):
261 data = data.resolve(base)
262 elif isinstance(data, tuple):
263 alias, others = data
264 data = alias.resolve(base)
265 if others: # Apply overrides on a copy.
266 data = data.copy()
267 merge(data, others)
269 return data
272_sentinel = object()
275class LocaleDataDict(abc.MutableMapping):
276 """Dictionary wrapper that automatically resolves aliases to the actual
277 values.
278 """
280 def __init__(
281 self,
282 data: MutableMapping[str | int | None, Any],
283 base: Mapping[str | int | None, Any] | None = None,
284 ):
285 # May be shared between locales.
286 self._data = data
287 # Per-instance memoization of resolved values.
288 self._resolved: dict[str | int | None, Any] = {}
289 if base is None:
290 base = data
291 self.base = base
293 def __len__(self) -> int:
294 return len(self._data)
296 def __iter__(self) -> Iterator[str | int | None]:
297 return iter(self._data)
299 def __getitem__(self, key: str | int | None) -> Any:
300 val = self._resolved.get(key, _sentinel)
301 if val is not _sentinel:
302 return val
303 orig = val = self._data[key]
304 if isinstance(val, Alias): # resolve an alias
305 val = val.resolve(self.base)
306 if isinstance(val, tuple): # Merge a partial dict with an alias
307 alias, others = val
308 val = alias.resolve(self.base).copy()
309 merge(val, others)
310 if isinstance(val, dict): # Return a nested alias-resolving dict
311 val = LocaleDataDict(val, base=self.base)
312 if val is not orig:
313 # Only resolved/wrapped values are memoized.
314 # Scalars are always read from `self._data`, so that
315 # manual writes into the backing data (possibly shared)
316 # stay visible.
317 self._resolved[key] = val
318 return val
320 def __setitem__(self, key: str | int | None, value: Any) -> None:
321 self._resolved.pop(key, None)
322 self._data[key] = value
324 def __delitem__(self, key: str | int | None) -> None:
325 self._resolved.pop(key, None)
326 del self._data[key]
328 def copy(self) -> LocaleDataDict:
329 return LocaleDataDict(self._data.copy(), base=self.base)