Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/localedata.py: 27%
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_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)
33def normalize_locale(name: str) -> str | None:
34 """Normalize a locale ID by stripping spaces and apply proper casing.
36 Returns the normalized locale ID string or `None` if the ID is not
37 recognized.
38 """
39 if not name or not isinstance(name, str):
40 return None
41 name = name.strip().lower()
42 for locale_id in chain.from_iterable([_cache, locale_identifiers()]):
43 if name == locale_id.lower():
44 return locale_id
47def resolve_locale_filename(name: os.PathLike[str] | str) -> str:
48 """
49 Resolve a locale identifier to a `.dat` path on disk.
50 """
52 # Clean up any possible relative paths.
53 name = os.path.basename(name)
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")
59 # Build the path.
60 return os.path.join(_dirname, f"{name}.dat")
63@lru_cache(maxsize=None)
64def exists(name: str) -> bool:
65 """Check whether locale data is available for the given locale.
67 Returns `True` if it exists, `False` otherwise.
69 :param name: the locale identifier string
70 """
71 if not name or not isinstance(name, str):
72 return False
73 if name in _cache:
74 return True
75 file_found = os.path.exists(resolve_locale_filename(name))
76 return file_found or bool(normalize_locale(name))
79@lru_cache(maxsize=None)
80def locale_identifiers() -> list[str]:
81 """Return a list of all locale identifiers for which locale data is
82 available.
84 This data is cached after the first invocation.
85 You can clear the cache by calling `locale_identifiers.cache_clear()`.
87 .. versionadded:: 0.8.1
89 :return: a list of locale identifiers (strings)
90 """
91 return [
92 stem
93 for stem, extension in (
94 os.path.splitext(filename) for filename in os.listdir(_dirname)
95 )
96 if extension == '.dat' and stem != 'root'
97 ]
100def _is_non_likely_script(name: str) -> bool:
101 """Return whether the locale is of the form ``lang_Script``,
102 and the script is not the likely script for the language.
104 This implements the behavior of the ``nonlikelyScript`` value of the
105 ``localRules`` attribute for parent locales added in CLDR 45.
106 """
107 from babel.core import get_global, parse_locale
109 try:
110 lang, territory, script, variant, *rest = parse_locale(name)
111 except ValueError:
112 return False
114 if lang and script and not territory and not variant and not rest:
115 likely_subtag = get_global('likely_subtags').get(lang)
116 _, _, likely_script, *_ = parse_locale(likely_subtag)
117 return script != likely_script
118 return False
121def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
122 """Load the locale data for the given locale.
124 The locale data is a dictionary that contains much of the data defined by
125 the Common Locale Data Repository (CLDR). This data is stored as a
126 collection of pickle files inside the ``babel`` package.
128 >>> d = load('en_US')
129 >>> d['languages']['sv']
130 'Swedish'
132 Note that the results are cached, and subsequent requests for the same
133 locale return the same dictionary:
135 >>> d1 = load('en_US')
136 >>> d2 = load('en_US')
137 >>> d1 is d2
138 True
140 :param name: the locale identifier string (or "root")
141 :param merge_inherited: whether the inherited data should be merged into
142 the data of the requested locale
143 :raise `IOError`: if no locale data file is found for the given locale
144 identifier, or one of the locales it inherits from
145 """
146 name = os.path.basename(name)
147 _cache_lock.acquire()
148 try:
149 data = _cache.get(name)
150 if not data:
151 # Load inherited data
152 if name == 'root' or not merge_inherited:
153 data = {}
154 else:
155 from babel.core import get_global
157 parent = get_global('parent_exceptions').get(name)
158 if not parent:
159 if _is_non_likely_script(name):
160 parent = 'root'
161 else:
162 parts = name.split('_')
163 parent = "root" if len(parts) == 1 else "_".join(parts[:-1])
164 data = load(parent).copy()
165 filename = resolve_locale_filename(name)
166 with open(filename, 'rb') as fileobj:
167 if name != 'root' and merge_inherited:
168 merge(data, pickle.load(fileobj))
169 else:
170 data = pickle.load(fileobj)
171 _cache[name] = data
172 return data
173 finally:
174 _cache_lock.release()
177def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:
178 """Merge the data from `dict2` into the `dict1` dictionary, making copies
179 of nested dictionaries.
181 >>> d = {1: 'foo', 3: 'baz'}
182 >>> merge(d, {1: 'Foo', 2: 'Bar'})
183 >>> sorted(d.items())
184 [(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
186 :param dict1: the dictionary to merge into
187 :param dict2: the dictionary containing the data that should be merged
188 """
189 for key, val2 in dict2.items():
190 if val2 is not None:
191 val1 = dict1.get(key)
192 if isinstance(val2, dict):
193 if val1 is None:
194 val1 = {}
195 if isinstance(val1, Alias):
196 val1 = (val1, val2)
197 elif isinstance(val1, tuple):
198 alias, others = val1
199 others = others.copy()
200 merge(others, val2)
201 val1 = (alias, others)
202 else:
203 val1 = val1.copy()
204 merge(val1, val2)
205 else:
206 val1 = val2
207 dict1[key] = val1
210class Alias:
211 """Representation of an alias in the locale data.
213 An alias is a value that refers to some other part of the locale data,
214 as specified by the `keys`.
215 """
217 def __init__(self, keys: tuple[str, ...]) -> None:
218 self.keys = tuple(keys)
220 def __repr__(self) -> str:
221 return f"<{type(self).__name__} {self.keys!r}>"
223 def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:
224 """Resolve the alias based on the given data.
226 This is done recursively, so if one alias resolves to a second alias,
227 that second alias will also be resolved.
229 :param data: the locale data
230 :type data: `dict`
231 """
232 base = data
233 for key in self.keys:
234 data = data[key]
235 if isinstance(data, Alias):
236 data = data.resolve(base)
237 elif isinstance(data, tuple):
238 alias, others = data
239 data = alias.resolve(base)
240 return data
243class LocaleDataDict(abc.MutableMapping):
244 """Dictionary wrapper that automatically resolves aliases to the actual
245 values.
246 """
248 def __init__(
249 self,
250 data: MutableMapping[str | int | None, Any],
251 base: Mapping[str | int | None, Any] | None = None,
252 ):
253 self._data = data
254 if base is None:
255 base = data
256 self.base = base
258 def __len__(self) -> int:
259 return len(self._data)
261 def __iter__(self) -> Iterator[str | int | None]:
262 return iter(self._data)
264 def __getitem__(self, key: str | int | None) -> Any:
265 orig = val = self._data[key]
266 if isinstance(val, Alias): # resolve an alias
267 val = val.resolve(self.base)
268 if isinstance(val, tuple): # Merge a partial dict with an alias
269 alias, others = val
270 val = alias.resolve(self.base).copy()
271 merge(val, others)
272 if isinstance(val, dict): # Return a nested alias-resolving dict
273 val = LocaleDataDict(val, base=self.base)
274 if val is not orig:
275 self._data[key] = val
276 return val
278 def __setitem__(self, key: str | int | None, value: Any) -> None:
279 self._data[key] = value
281 def __delitem__(self, key: str | int | None) -> None:
282 del self._data[key]
284 def copy(self) -> LocaleDataDict:
285 return LocaleDataDict(self._data.copy(), base=self.base)