From 0b6ac3d76287ac84a8a1c402dc0549d69d7c10f2 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 29 Jul 2026 17:29:50 +0300 Subject: [PATCH] Remove localedata writeback Fixes #1234 --- babel/localedata.py | 4 +--- tests/test_localedata.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/babel/localedata.py b/babel/localedata.py index 4648e662..0600a817 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -262,7 +262,7 @@ class LocaleDataDict(abc.MutableMapping): return iter(self._data) def __getitem__(self, key: str | int | None) -> Any: - orig = val = self._data[key] + 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 @@ -271,8 +271,6 @@ class LocaleDataDict(abc.MutableMapping): 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: diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 42810b99..030bb90a 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -55,6 +55,8 @@ def test_merge_with_alias_and_resolve(): assert d1 == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': (alias, {'b': 22, 'e': 25})} d = localedata.LocaleDataDict(d1) assert dict(d.items()) == {'x': {'a': 1, 'b': 12, 'c': 3, 'd': 14}, 'y': {'a': 1, 'b': 22, 'c': 3, 'd': 14, 'e': 25}} + # Resolving the partial alias must not have written the result back into the underlying data (GH-1234) + assert d1['y'] == (alias, {'b': 22, 'e': 25}) def test_load(): @@ -62,6 +64,43 @@ def test_load(): assert localedata.load('en_US') is localedata.load('en_US') +def _calendar_snapshot(name): + locale = Locale.parse(name) + return { + 'months': dict(locale.months['stand-alone']['wide']), + 'months_abbr': dict(locale.months['format']['abbreviated']), + 'days': dict(locale.days['stand-alone']['wide']), + 'quarters': dict(locale.quarters['stand-alone']['wide']), + 'eras': dict(locale.eras['wide']), + } + + +@pytest.mark.parametrize('reverse', (False, True)) +def test_no_cross_locale_contamination(reverse): + """ + Alias-heavy calendar data (e.g. what `format_date(..., 'LLLL')` reads) + must be identical whether a locale is loaded into a cold cache or after + other locales have already been read: e.g. + * reading 'he' must not turn Norwegian month names Hebrew + * reading 'aa' must not turn everyone else's stand-alone quarters into root's 'Q1' placeholders. + + Regression test for https://github.com/python-babel/babel/issues/1234 + """ + locales = ('aa', 'de', 'fr', 'he', 'ja', 'no') + cold = {} + for name in locales: + localedata._cache.clear() + cold[name] = _calendar_snapshot(name) + + assert cold['he']['months'] != cold['de']['months'] # Sanity check + + localedata._cache.clear() + warm = {name: _calendar_snapshot(name) for name in (reversed(locales) if reverse else locales)} + assert warm == cold + # Repeated reads in the warmed-up state must stay correct too + assert {name: _calendar_snapshot(name) for name in locales} == cold + + def test_load_inheritance(monkeypatch): from babel.localedata import _cache -- 2.47.3