Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/babel/localedata.py: 28%
121 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:39 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:39 +0000
1"""
2 babel.localedata
3 ~~~~~~~~~~~~~~~~
5 Low-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-2023 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")
63def exists(name: str) -> bool:
64 """Check whether locale data is available for the given locale.
66 Returns `True` if it exists, `False` otherwise.
68 :param name: the locale identifier string
69 """
70 if not name or not isinstance(name, str):
71 return False
72 if name in _cache:
73 return True
74 file_found = os.path.exists(resolve_locale_filename(name))
75 return True if file_found else bool(normalize_locale(name))
78@lru_cache(maxsize=None)
79def locale_identifiers() -> list[str]:
80 """Return a list of all locale identifiers for which locale data is
81 available.
83 This data is cached after the first invocation.
84 You can clear the cache by calling `locale_identifiers.cache_clear()`.
86 .. versionadded:: 0.8.1
88 :return: a list of locale identifiers (strings)
89 """
90 return [
91 stem
92 for stem, extension in
93 (os.path.splitext(filename) for filename in os.listdir(_dirname))
94 if extension == '.dat' and stem != 'root'
95 ]
98def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
99 """Load the locale data for the given locale.
101 The locale data is a dictionary that contains much of the data defined by
102 the Common Locale Data Repository (CLDR). This data is stored as a
103 collection of pickle files inside the ``babel`` package.
105 >>> d = load('en_US')
106 >>> d['languages']['sv']
107 u'Swedish'
109 Note that the results are cached, and subsequent requests for the same
110 locale return the same dictionary:
112 >>> d1 = load('en_US')
113 >>> d2 = load('en_US')
114 >>> d1 is d2
115 True
117 :param name: the locale identifier string (or "root")
118 :param merge_inherited: whether the inherited data should be merged into
119 the data of the requested locale
120 :raise `IOError`: if no locale data file is found for the given locale
121 identifier, or one of the locales it inherits from
122 """
123 name = os.path.basename(name)
124 _cache_lock.acquire()
125 try:
126 data = _cache.get(name)
127 if not data:
128 # Load inherited data
129 if name == 'root' or not merge_inherited:
130 data = {}
131 else:
132 from babel.core import get_global
133 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] = data
145 return data
146 finally:
147 _cache_lock.release()
150def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:
151 """Merge the data from `dict2` into the `dict1` dictionary, making copies
152 of nested dictionaries.
154 >>> d = {1: 'foo', 3: 'baz'}
155 >>> merge(d, {1: 'Foo', 2: 'Bar'})
156 >>> sorted(d.items())
157 [(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
159 :param dict1: the dictionary to merge into
160 :param dict2: the dictionary containing the data that should be merged
161 """
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 = val1
172 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 = val2
180 dict1[key] = val1
183class Alias:
184 """Representation of an alias in the locale data.
186 An alias is a value that refers to some other part of the locale data,
187 as specified by the `keys`.
188 """
190 def __init__(self, keys: tuple[str, ...]) -> None:
191 self.keys = tuple(keys)
193 def __repr__(self) -> str:
194 return f"<{type(self).__name__} {self.keys!r}>"
196 def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:
197 """Resolve the alias based on the given data.
199 This is done recursively, so if one alias resolves to a second alias,
200 that second alias will also be resolved.
202 :param data: the locale data
203 :type data: `dict`
204 """
205 base = data
206 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 = data
212 data = alias.resolve(base)
213 return data
216class LocaleDataDict(abc.MutableMapping):
217 """Dictionary wrapper that automatically resolves aliases to the actual
218 values.
219 """
221 def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None):
222 self._data = data
223 if base is None:
224 base = data
225 self.base = base
227 def __len__(self) -> int:
228 return len(self._data)
230 def __iter__(self) -> Iterator[str | int | None]:
231 return iter(self._data)
233 def __getitem__(self, key: str | int | None) -> Any:
234 orig = val = self._data[key]
235 if isinstance(val, Alias): # resolve an alias
236 val = val.resolve(self.base)
237 if isinstance(val, tuple): # Merge a partial dict with an alias
238 alias, others = val
239 val = alias.resolve(self.base).copy()
240 merge(val, others)
241 if isinstance(val, dict): # Return a nested alias-resolving dict
242 val = LocaleDataDict(val, base=self.base)
243 if val is not orig:
244 self._data[key] = val
245 return val
247 def __setitem__(self, key: str | int | None, value: Any) -> None:
248 self._data[key] = value
250 def __delitem__(self, key: str | int | None) -> None:
251 del self._data[key]
253 def copy(self) -> LocaleDataDict:
254 return LocaleDataDict(self._data.copy(), base=self.base)