From 1d03e6da95443ddb7b7d932d175dd3f8806eba92 Mon Sep 17 00:00:00 2001 From: AryanRG Date: Wed, 14 Oct 2026 15:52:21 +0530 Subject: [PATCH] Fix fallback for partial list patterns Resolve missing list-pattern parts individually without mutating locale data. Follow the CLDR root chain for unit-narrow so narrow lists do not fall back to unrelated wide-unit parts. Cover partial start/end patterns, missing two-item patterns, precedence, and locale-cache preservation with regressions. --- babel/lists.py | 22 ++++++++++++++-------- tests/test_lists.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/babel/lists.py b/babel/lists.py index b6c859800..049905a9d 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -126,16 +126,22 @@ def format_list( "standard-narrow": ["standard-short", "standard"], "standard-short": ["standard"], "unit": ["unit-short", "standard"], - "unit-narrow": ["unit-short", "unit", "standard"], + "unit-narrow": ["unit-short", "standard"], "unit-short": ["standard"], } def _resolve_list_style(locale: Locale, style: str): - for style in (style, *(_style_fallbacks.get(style, []))): # noqa: B020 - if style in locale.list_patterns: - return locale.list_patterns[style] - raise ValueError( - f"Locale {locale} does not support list formatting style {style!r} " - f"(supported are {sorted(locale.list_patterns)})", - ) + # A style may define only some of its parts, so missing parts are + # filled in from the fallback styles, in order. + patterns = {} + for fallback_style in (style, *(_style_fallbacks.get(style, []))): + if fallback_style in locale.list_patterns: + for part, pattern in locale.list_patterns[fallback_style].items(): + patterns.setdefault(part, pattern) + if not patterns: + raise ValueError( + f"Locale {locale} does not support list formatting style {style!r} " + f"(supported are {sorted(locale.list_patterns)})", + ) + return patterns diff --git a/tests/test_lists.py b/tests/test_lists.py index 448366076..32c82bcc5 100644 --- a/tests/test_lists.py +++ b/tests/test_lists.py @@ -1,6 +1,6 @@ import pytest -from babel import lists, units +from babel import Locale, lists, units @pytest.mark.parametrize( @@ -43,3 +43,38 @@ def test_issue_1098(): def test_lists_default_locale_deprecation(): with pytest.warns(DeprecationWarning): _ = lists.DEFAULT_LOCALE + + +@pytest.mark.parametrize( + ('list', 'style', 'locale', 'expected'), + [ + # Styles defining only some parts get the missing ones from fallback styles. + (['A', 'B', 'C'], 'unit', 'de', 'A, B und C'), + (['A', 'B', 'C'], 'standard-narrow', 'fr', 'A, B, C'), + (['A', 'B', 'C', 'D'], 'standard-narrow', 'fr', 'A, B, C, D'), + (['A', 'B', 'C'], 'unit', 'hy', 'A, B և C'), + (['A', 'B', 'C'], 'unit-narrow', 'is', 'A B og C'), + (['A', 'B', 'C'], 'unit', 'ar_OM', 'A، وB، وC'), + # The nearest fallback style wins: 'end' from standard-short, 'start' from standard. + (['A', 'B', 'C'], 'standard-narrow', 'wo_SN', 'A, B, & C'), + # Two items with no '2' part use the fallback '2' pattern. + (['A', 'B'], 'standard-short', 'hi', 'A और B'), + (['A', 'B'], 'standard-narrow', 'lo', 'A ແລະ B'), + (['A', 'B'], 'unit-narrow', 'wo_SN', 'A, B'), + (['A', 'B'], 'unit-narrow', 'sat', 'A, B'), + (['A', 'B'], 'unit', 'ar_OM', 'A وB'), + # Parts defined by the requested style take precedence. + (['A', 'B', 'C'], 'standard-narrow', 'lo', 'A, B, C'), + (['A', 'B'], 'unit-short', 'de', 'A, B'), + ], +) +def test_format_list_partial_style_fallback(list, style, locale, expected): + assert lists.format_list(list, style=style, locale=locale) == expected + + +def test_format_list_does_not_modify_locale_patterns(): + locale = Locale.parse('fr') + before = {style: dict(parts) for style, parts in locale.list_patterns.items()} + lists.format_list(['A', 'B', 'C', 'D'], style='standard-narrow', locale=locale) + after = {style: dict(parts) for style, parts in locale.list_patterns.items()} + assert after == before