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

1""" 

2 babel.localedata 

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

4 

5 Low-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-2023 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 if extension == '.dat' and stem != 'root' 

95 ] 

96 

97 

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

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

100 

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. 

104 

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

106 >>> d['languages']['sv'] 

107 u'Swedish' 

108 

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

110 locale return the same dictionary: 

111 

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

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

114 >>> d1 is d2 

115 True 

116 

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

148 

149 

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. 

153 

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

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

156 >>> sorted(d.items()) 

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

158 

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 

181 

182 

183class Alias: 

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

185 

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

187 as specified by the `keys`. 

188 """ 

189 

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

191 self.keys = tuple(keys) 

192 

193 def __repr__(self) -> str: 

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

195 

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

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

198 

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

200 that second alias will also be resolved. 

201 

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 

214 

215 

216class LocaleDataDict(abc.MutableMapping): 

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

218 values. 

219 """ 

220 

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 

226 

227 def __len__(self) -> int: 

228 return len(self._data) 

229 

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

231 return iter(self._data) 

232 

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 

246 

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

248 self._data[key] = value 

249 

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

251 del self._data[key] 

252 

253 def copy(self) -> LocaleDataDict: 

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