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 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@cache
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@cache
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)