done
This commit is contained in:
278
lib/python3.11/site-packages/babel/localedata.py
Normal file
278
lib/python3.11/site-packages/babel/localedata.py
Normal file
@ -0,0 +1,278 @@
|
||||
"""
|
||||
babel.localedata
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Low-level locale data access.
|
||||
|
||||
:note: The `Locale` class, which uses this module under the hood, provides a
|
||||
more convenient interface for accessing the locale data.
|
||||
|
||||
:copyright: (c) 2013-2025 by the Babel Team.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
from collections import abc
|
||||
from collections.abc import Iterator, Mapping, MutableMapping
|
||||
from functools import lru_cache
|
||||
from itertools import chain
|
||||
from typing import Any
|
||||
|
||||
_cache: dict[str, Any] = {}
|
||||
_cache_lock = threading.RLock()
|
||||
_dirname = os.path.join(os.path.dirname(__file__), 'locale-data')
|
||||
_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I)
|
||||
|
||||
|
||||
def normalize_locale(name: str) -> str | None:
|
||||
"""Normalize a locale ID by stripping spaces and apply proper casing.
|
||||
|
||||
Returns the normalized locale ID string or `None` if the ID is not
|
||||
recognized.
|
||||
"""
|
||||
if not name or not isinstance(name, str):
|
||||
return None
|
||||
name = name.strip().lower()
|
||||
for locale_id in chain.from_iterable([_cache, locale_identifiers()]):
|
||||
if name == locale_id.lower():
|
||||
return locale_id
|
||||
|
||||
|
||||
def resolve_locale_filename(name: os.PathLike[str] | str) -> str:
|
||||
"""
|
||||
Resolve a locale identifier to a `.dat` path on disk.
|
||||
"""
|
||||
|
||||
# Clean up any possible relative paths.
|
||||
name = os.path.basename(name)
|
||||
|
||||
# Ensure we're not left with one of the Windows reserved names.
|
||||
if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]):
|
||||
raise ValueError(f"Name {name} is invalid on Windows")
|
||||
|
||||
# Build the path.
|
||||
return os.path.join(_dirname, f"{name}.dat")
|
||||
|
||||
|
||||
def exists(name: str) -> bool:
|
||||
"""Check whether locale data is available for the given locale.
|
||||
|
||||
Returns `True` if it exists, `False` otherwise.
|
||||
|
||||
:param name: the locale identifier string
|
||||
"""
|
||||
if not name or not isinstance(name, str):
|
||||
return False
|
||||
if name in _cache:
|
||||
return True
|
||||
file_found = os.path.exists(resolve_locale_filename(name))
|
||||
return True if file_found else bool(normalize_locale(name))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def locale_identifiers() -> list[str]:
|
||||
"""Return a list of all locale identifiers for which locale data is
|
||||
available.
|
||||
|
||||
This data is cached after the first invocation.
|
||||
You can clear the cache by calling `locale_identifiers.cache_clear()`.
|
||||
|
||||
.. versionadded:: 0.8.1
|
||||
|
||||
:return: a list of locale identifiers (strings)
|
||||
"""
|
||||
return [
|
||||
stem
|
||||
for stem, extension in
|
||||
(os.path.splitext(filename) for filename in os.listdir(_dirname))
|
||||
if extension == '.dat' and stem != 'root'
|
||||
]
|
||||
|
||||
|
||||
def _is_non_likely_script(name: str) -> bool:
|
||||
"""Return whether the locale is of the form ``lang_Script``,
|
||||
and the script is not the likely script for the language.
|
||||
|
||||
This implements the behavior of the ``nonlikelyScript`` value of the
|
||||
``localRules`` attribute for parent locales added in CLDR 45.
|
||||
"""
|
||||
from babel.core import get_global, parse_locale
|
||||
|
||||
try:
|
||||
lang, territory, script, variant, *rest = parse_locale(name)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if lang and script and not territory and not variant and not rest:
|
||||
likely_subtag = get_global('likely_subtags').get(lang)
|
||||
_, _, likely_script, *_ = parse_locale(likely_subtag)
|
||||
return script != likely_script
|
||||
return False
|
||||
|
||||
|
||||
def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
|
||||
"""Load the locale data for the given locale.
|
||||
|
||||
The locale data is a dictionary that contains much of the data defined by
|
||||
the Common Locale Data Repository (CLDR). This data is stored as a
|
||||
collection of pickle files inside the ``babel`` package.
|
||||
|
||||
>>> d = load('en_US')
|
||||
>>> d['languages']['sv']
|
||||
u'Swedish'
|
||||
|
||||
Note that the results are cached, and subsequent requests for the same
|
||||
locale return the same dictionary:
|
||||
|
||||
>>> d1 = load('en_US')
|
||||
>>> d2 = load('en_US')
|
||||
>>> d1 is d2
|
||||
True
|
||||
|
||||
:param name: the locale identifier string (or "root")
|
||||
:param merge_inherited: whether the inherited data should be merged into
|
||||
the data of the requested locale
|
||||
:raise `IOError`: if no locale data file is found for the given locale
|
||||
identifier, or one of the locales it inherits from
|
||||
"""
|
||||
name = os.path.basename(name)
|
||||
_cache_lock.acquire()
|
||||
try:
|
||||
data = _cache.get(name)
|
||||
if not data:
|
||||
# Load inherited data
|
||||
if name == 'root' or not merge_inherited:
|
||||
data = {}
|
||||
else:
|
||||
from babel.core import get_global
|
||||
parent = get_global('parent_exceptions').get(name)
|
||||
if not parent:
|
||||
if _is_non_likely_script(name):
|
||||
parent = 'root'
|
||||
else:
|
||||
parts = name.split('_')
|
||||
parent = "root" if len(parts) == 1 else "_".join(parts[:-1])
|
||||
data = load(parent).copy()
|
||||
filename = resolve_locale_filename(name)
|
||||
with open(filename, 'rb') as fileobj:
|
||||
if name != 'root' and merge_inherited:
|
||||
merge(data, pickle.load(fileobj))
|
||||
else:
|
||||
data = pickle.load(fileobj)
|
||||
_cache[name] = data
|
||||
return data
|
||||
finally:
|
||||
_cache_lock.release()
|
||||
|
||||
|
||||
def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:
|
||||
"""Merge the data from `dict2` into the `dict1` dictionary, making copies
|
||||
of nested dictionaries.
|
||||
|
||||
>>> d = {1: 'foo', 3: 'baz'}
|
||||
>>> merge(d, {1: 'Foo', 2: 'Bar'})
|
||||
>>> sorted(d.items())
|
||||
[(1, 'Foo'), (2, 'Bar'), (3, 'baz')]
|
||||
|
||||
:param dict1: the dictionary to merge into
|
||||
:param dict2: the dictionary containing the data that should be merged
|
||||
"""
|
||||
for key, val2 in dict2.items():
|
||||
if val2 is not None:
|
||||
val1 = dict1.get(key)
|
||||
if isinstance(val2, dict):
|
||||
if val1 is None:
|
||||
val1 = {}
|
||||
if isinstance(val1, Alias):
|
||||
val1 = (val1, val2)
|
||||
elif isinstance(val1, tuple):
|
||||
alias, others = val1
|
||||
others = others.copy()
|
||||
merge(others, val2)
|
||||
val1 = (alias, others)
|
||||
else:
|
||||
val1 = val1.copy()
|
||||
merge(val1, val2)
|
||||
else:
|
||||
val1 = val2
|
||||
dict1[key] = val1
|
||||
|
||||
|
||||
class Alias:
|
||||
"""Representation of an alias in the locale data.
|
||||
|
||||
An alias is a value that refers to some other part of the locale data,
|
||||
as specified by the `keys`.
|
||||
"""
|
||||
|
||||
def __init__(self, keys: tuple[str, ...]) -> None:
|
||||
self.keys = tuple(keys)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{type(self).__name__} {self.keys!r}>"
|
||||
|
||||
def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:
|
||||
"""Resolve the alias based on the given data.
|
||||
|
||||
This is done recursively, so if one alias resolves to a second alias,
|
||||
that second alias will also be resolved.
|
||||
|
||||
:param data: the locale data
|
||||
:type data: `dict`
|
||||
"""
|
||||
base = data
|
||||
for key in self.keys:
|
||||
data = data[key]
|
||||
if isinstance(data, Alias):
|
||||
data = data.resolve(base)
|
||||
elif isinstance(data, tuple):
|
||||
alias, others = data
|
||||
data = alias.resolve(base)
|
||||
return data
|
||||
|
||||
|
||||
class LocaleDataDict(abc.MutableMapping):
|
||||
"""Dictionary wrapper that automatically resolves aliases to the actual
|
||||
values.
|
||||
"""
|
||||
|
||||
def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None):
|
||||
self._data = data
|
||||
if base is None:
|
||||
base = data
|
||||
self.base = base
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def __iter__(self) -> Iterator[str | int | None]:
|
||||
return iter(self._data)
|
||||
|
||||
def __getitem__(self, key: str | int | None) -> Any:
|
||||
orig = val = self._data[key]
|
||||
if isinstance(val, Alias): # resolve an alias
|
||||
val = val.resolve(self.base)
|
||||
if isinstance(val, tuple): # Merge a partial dict with an alias
|
||||
alias, others = val
|
||||
val = alias.resolve(self.base).copy()
|
||||
merge(val, others)
|
||||
if isinstance(val, dict): # Return a nested alias-resolving dict
|
||||
val = LocaleDataDict(val, base=self.base)
|
||||
if val is not orig:
|
||||
self._data[key] = val
|
||||
return val
|
||||
|
||||
def __setitem__(self, key: str | int | None, value: Any) -> None:
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key: str | int | None) -> None:
|
||||
del self._data[key]
|
||||
|
||||
def copy(self) -> LocaleDataDict:
|
||||
return LocaleDataDict(self._data.copy(), base=self.base)
|
Reference in New Issue
Block a user