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

135 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-2025 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 

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

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

65 

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

67 

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

76 

77 

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. 

82 

83 This data is cached after the first invocation. 

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

85 

86 .. versionadded:: 0.8.1 

87 

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 ) 

95 if extension == '.dat' and stem != 'root' 

96 ] 

97 

98 

99def _is_non_likely_script(name: str) -> bool: 

100 """Return whether the locale is of the form ``lang_Script``, 

101 and the script is not the likely script for the language. 

102 

103 This implements the behavior of the ``nonlikelyScript`` value of the 

104 ``localRules`` attribute for parent locales added in CLDR 45. 

105 """ 

106 from babel.core import get_global, parse_locale 

107 

108 try: 

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

110 except ValueError: 

111 return False 

112 

113 if lang and script and not territory and not variant and not rest: 

114 likely_subtag = get_global('likely_subtags').get(lang) 

115 _, _, likely_script, *_ = parse_locale(likely_subtag) 

116 return script != likely_script 

117 return False 

118 

119 

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

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

122 

123 The locale data is a dictionary that contains much of the data defined by 

124 the Common Locale Data Repository (CLDR). This data is stored as a 

125 collection of pickle files inside the ``babel`` package. 

126 

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

128 >>> d['languages']['sv'] 

129 'Swedish' 

130 

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

132 locale return the same dictionary: 

133 

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

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

136 >>> d1 is d2 

137 True 

138 

139 :param name: the locale identifier string (or "root") 

140 :param merge_inherited: whether the inherited data should be merged into 

141 the data of the requested locale 

142 :raise `IOError`: if no locale data file is found for the given locale 

143 identifier, or one of the locales it inherits from 

144 """ 

145 name = os.path.basename(name) 

146 _cache_lock.acquire() 

147 try: 

148 data = _cache.get(name) 

149 if not data: 

150 # Load inherited data 

151 if name == 'root' or not merge_inherited: 

152 data = {} 

153 else: 

154 from babel.core import get_global 

155 

156 parent = get_global('parent_exceptions').get(name) 

157 if not parent: 

158 if _is_non_likely_script(name): 

159 parent = 'root' 

160 else: 

161 parts = name.split('_') 

162 parent = "root" if len(parts) == 1 else "_".join(parts[:-1]) 

163 data = load(parent).copy() 

164 filename = resolve_locale_filename(name) 

165 with open(filename, 'rb') as fileobj: 

166 if name != 'root' and merge_inherited: 

167 merge(data, pickle.load(fileobj)) 

168 else: 

169 data = pickle.load(fileobj) 

170 _cache[name] = data 

171 return data 

172 finally: 

173 _cache_lock.release() 

174 

175 

176def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None: 

177 """Merge the data from `dict2` into the `dict1` dictionary, making copies 

178 of nested dictionaries. 

179 

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

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

182 >>> sorted(d.items()) 

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

184 

185 :param dict1: the dictionary to merge into 

186 :param dict2: the dictionary containing the data that should be merged 

187 """ 

188 for key, val2 in dict2.items(): 

189 if val2 is not None: 

190 val1 = dict1.get(key) 

191 if isinstance(val2, dict): 

192 if val1 is None: 

193 val1 = {} 

194 if isinstance(val1, Alias): 

195 val1 = (val1, val2) 

196 elif isinstance(val1, tuple): 

197 alias, others = val1 

198 others = others.copy() 

199 merge(others, val2) 

200 val1 = (alias, others) 

201 else: 

202 val1 = val1.copy() 

203 merge(val1, val2) 

204 else: 

205 val1 = val2 

206 dict1[key] = val1 

207 

208 

209class Alias: 

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

211 

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

213 as specified by the `keys`. 

214 """ 

215 

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

217 self.keys = tuple(keys) 

218 

219 def __repr__(self) -> str: 

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

221 

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

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

224 

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

226 that second alias will also be resolved. 

227 

228 :param data: the locale data 

229 :type data: `dict` 

230 """ 

231 base = data 

232 for key in self.keys: 

233 data = data[key] 

234 if isinstance(data, Alias): 

235 data = data.resolve(base) 

236 elif isinstance(data, tuple): 

237 alias, others = data 

238 data = alias.resolve(base) 

239 return data 

240 

241 

242class LocaleDataDict(abc.MutableMapping): 

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

244 values. 

245 """ 

246 

247 def __init__( 

248 self, 

249 data: MutableMapping[str | int | None, Any], 

250 base: Mapping[str | int | None, Any] | None = None, 

251 ): 

252 self._data = data 

253 if base is None: 

254 base = data 

255 self.base = base 

256 

257 def __len__(self) -> int: 

258 return len(self._data) 

259 

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

261 return iter(self._data) 

262 

263 def __getitem__(self, key: str | int | None) -> Any: 

264 orig = val = self._data[key] 

265 if isinstance(val, Alias): # resolve an alias 

266 val = val.resolve(self.base) 

267 if isinstance(val, tuple): # Merge a partial dict with an alias 

268 alias, others = val 

269 val = alias.resolve(self.base).copy() 

270 merge(val, others) 

271 if isinstance(val, dict): # Return a nested alias-resolving dict 

272 val = LocaleDataDict(val, base=self.base) 

273 if val is not orig: 

274 self._data[key] = val 

275 return val 

276 

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

278 self._data[key] = value 

279 

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

281 del self._data[key] 

282 

283 def copy(self) -> LocaleDataDict: 

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