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

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

31 

32 

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

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

35 

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 

45 

46 

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

48 """ 

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

50 """ 

51 

52 # Clean up any possible relative paths. 

53 name = os.path.basename(name) 

54 

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

58 

59 # Build the path. 

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

61 

62 

63@lru_cache(maxsize=None) 

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

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

66 

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

68 

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

77 

78 

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. 

83 

84 This data is cached after the first invocation. 

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

86 

87 .. versionadded:: 0.8.1 

88 

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 ] 

98 

99 

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. 

103 

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 

108 

109 try: 

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

111 except ValueError: 

112 return False 

113 

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 

119 

120 

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

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

123 

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. 

127 

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

129 >>> d['languages']['sv'] 

130 'Swedish' 

131 

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

133 locale return the same dictionary: 

134 

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

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

137 >>> d1 is d2 

138 True 

139 

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 

156 

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

175 

176 

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. 

180 

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

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

183 >>> sorted(d.items()) 

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

185 

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 

208 

209 

210class Alias: 

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

212 

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

214 as specified by the `keys`. 

215 """ 

216 

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

218 self.keys = tuple(keys) 

219 

220 def __repr__(self) -> str: 

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

222 

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

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

225 

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

227 that second alias will also be resolved. 

228 

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 

241 

242 

243class LocaleDataDict(abc.MutableMapping): 

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

245 values. 

246 """ 

247 

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 

257 

258 def __len__(self) -> int: 

259 return len(self._data) 

260 

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

262 return iter(self._data) 

263 

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 

277 

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

279 self._data[key] = value 

280 

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

282 del self._data[key] 

283 

284 def copy(self) -> LocaleDataDict: 

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