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

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

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)