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

156 statements  

1""" 

2babel.localedata 

3~~~~~~~~~~~~~~~~ 

4 

5Low-level locale data access. 

6 

7:note: The `Locale` class, which uses this module under the hood, provides a 

8 more convenient interface for accessing the locale data. 

9 

10:copyright: (c) 2013-2026 by the Babel Team. 

11:license: BSD, see LICENSE for more details. 

12""" 

13 

14from __future__ import annotations 

15 

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 

26 

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) 

32 

33 

34def normalize_locale(name: str) -> str | None: 

35 """Normalize a locale ID by stripping spaces and apply proper casing. 

36 

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 

46 

47 

48def resolve_locale_filename(name: os.PathLike[str] | str) -> str: 

49 """ 

50 Resolve a locale identifier to a `.dat` path on disk. 

51 """ 

52 

53 # Clean up any possible relative paths. 

54 name = os.path.basename(name) 

55 

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") 

59 

60 # Build the path. 

61 return os.path.join(_dirname, f"{name}.dat") 

62 

63 

64@lru_cache(maxsize=None) 

65def exists(name: str) -> bool: 

66 """Check whether locale data is available for the given locale. 

67 

68 Returns `True` if it exists, `False` otherwise. 

69 

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

78 

79 

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. 

84 

85 This data is cached after the first invocation. 

86 You can clear the cache by calling `locale_identifiers.cache_clear()`. 

87 

88 .. versionadded:: 0.8.1 

89 

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 ] 

99 

100 

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. 

104 

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 

109 

110 try: 

111 lang, territory, script, variant, *rest = parse_locale(name) 

112 except ValueError: 

113 return False 

114 

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 

120 

121 

122def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]: 

123 """Load the locale data for the given locale. 

124 

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. 

128 

129 >>> d = load('en_US') 

130 >>> d['languages']['sv'] 

131 'Swedish' 

132 

133 Note that the results are cached, and subsequent requests for the same 

134 locale return the same dictionary: 

135 

136 >>> d1 = load('en_US') 

137 >>> d2 = load('en_US') 

138 >>> d1 is d2 

139 True 

140 

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 

157 

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() 

176 

177 

178def clear_caches() -> None: 

179 """ 

180 Clear locale data caches. 

181 """ 

182 with _cache_lock: 

183 _cache.clear() 

184 _dict_cache.clear() 

185 

186 

187def get_locale_data(name: str) -> LocaleDataDict: 

188 """Return an alias-resolving `LocaleDataDict` over the merged data for 

189 the given locale. 

190 

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

199 

200 

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. 

204 

205 >>> d = {1: 'foo', 3: 'baz'} 

206 >>> merge(d, {1: 'Foo', 2: 'Bar'}) 

207 >>> sorted(d.items()) 

208 [(1, 'Foo'), (2, 'Bar'), (3, 'baz')] 

209 

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 

234 

235 

236class Alias: 

237 """Representation of an alias in the locale data. 

238 

239 An alias is a value that refers to some other part of the locale data, 

240 as specified by the `keys`. 

241 """ 

242 

243 def __init__(self, keys: tuple[str, ...]) -> None: 

244 self.keys = tuple(keys) 

245 

246 def __repr__(self) -> str: 

247 return f"<{type(self).__name__} {self.keys!r}>" 

248 

249 def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]: 

250 """Resolve the alias based on the given data. 

251 

252 This is done recursively, so if one alias resolves to a second alias, 

253 that second alias will also be resolved. 

254 

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) 

268 

269 return data 

270 

271 

272_sentinel = object() 

273 

274 

275class LocaleDataDict(abc.MutableMapping): 

276 """Dictionary wrapper that automatically resolves aliases to the actual 

277 values. 

278 """ 

279 

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 

292 

293 def __len__(self) -> int: 

294 return len(self._data) 

295 

296 def __iter__(self) -> Iterator[str | int | None]: 

297 return iter(self._data) 

298 

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 

319 

320 def __setitem__(self, key: str | int | None, value: Any) -> None: 

321 self._resolved.pop(key, None) 

322 self._data[key] = value 

323 

324 def __delitem__(self, key: str | int | None) -> None: 

325 self._resolved.pop(key, None) 

326 del self._data[key] 

327 

328 def copy(self) -> LocaleDataDict: 

329 return LocaleDataDict(self._data.copy(), base=self.base)