From aee6d698b541dc50439280d7e093092cc0d4b832 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 14 Jan 2025 16:32:22 +0200 Subject: [PATCH 1/2] `default_locale`: support multiple keys Also ignore falsy values in `default_locale` args. --- babel/core.py | 22 ++++++++++++++++++---- tests/test_core.py | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/babel/core.py b/babel/core.py index 1c7732b53..bffef94db 100644 --- a/babel/core.py +++ b/babel/core.py @@ -1089,7 +1089,10 @@ def unit_display_names(self) -> localedata.LocaleDataDict: return self._data['unit_display_names'] -def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None: +def default_locale( + category: str | tuple[str, ...] | list[str] | None = None, + aliases: Mapping[str, str] = LOCALE_ALIASES, +) -> str | None: """Returns the system default locale for a given category, based on environment variables. @@ -1113,11 +1116,22 @@ def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOC - ``LC_CTYPE`` - ``LANG`` - :param category: one of the ``LC_XXX`` environment variable names + :param category: one or more of the ``LC_XXX`` environment variable names :param aliases: a dictionary of aliases for locale identifiers """ - varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') - for name in filter(None, varnames): + + varnames = ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG') + if category: + if isinstance(category, str): + varnames = (category, *varnames) + elif isinstance(category, (list, tuple)): + varnames = (*category, *varnames) + else: + raise TypeError(f"Invalid type for category: {category!r}") + + for name in varnames: + if not name: + continue locale = os.getenv(name) if locale: if name == 'LANGUAGE' and ':' in locale: diff --git a/tests/test_core.py b/tests/test_core.py index b6c55626c..8770cf8d9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -265,7 +265,7 @@ def test_plural_form_property(self): def test_default_locale(monkeypatch): - for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: + for name in ['LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']: monkeypatch.setenv(name, '') monkeypatch.setenv('LANG', 'fr_FR.UTF-8') assert default_locale('LC_MESSAGES') == 'fr_FR' @@ -277,6 +277,23 @@ def test_default_locale(monkeypatch): assert default_locale() == 'en_US_POSIX' +def test_default_locale_multiple_args(monkeypatch): + for name in ['LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC']: + monkeypatch.setenv(name, '') + assert default_locale(["", 0, None]) is None + monkeypatch.setenv('LANG', 'en_US') + assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'en_US' # No LC_MONETARY or LC_NUMERIC set + monkeypatch.setenv('LC_NUMERIC', 'fr_FR.UTF-8') + assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'fr_FR' # LC_NUMERIC set + monkeypatch.setenv('LC_MONETARY', 'fi_FI.UTF-8') + assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'fi_FI' # LC_MONETARY set, it takes precedence + + +def test_default_locale_bad_arg(): + with pytest.raises(TypeError): + default_locale(42) + + def test_negotiate_locale(): assert (core.negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) == 'de_DE') From 56ef7c7f578a904917464c187e399abb762bd5e3 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 22 Dec 2023 00:42:55 +0100 Subject: [PATCH 2/2] Prefer LC_MONETARY when formatting currency Co-authored-by: Alexis Hildebrandt --- babel/numbers.py | 29 ++++++++++++++++++----------- tests/test_numbers.py | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index a2b89cc31..8dd4d9eae 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -7,7 +7,8 @@ The default locale for the functions in this module is determined by the following environment variables, in that order: - * ``LC_NUMERIC``, + * ``LC_MONETARY`` for currency related functions, + * ``LC_NUMERIC``, and * ``LC_ALL``, and * ``LANG`` @@ -28,6 +29,7 @@ from babel.core import Locale, default_locale, get_global from babel.localedata import LocaleDataDict +LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC')) LC_NUMERIC = default_locale('LC_NUMERIC') @@ -117,9 +119,10 @@ def get_currency_name( :param currency: the currency code. :param count: the optional count. If provided the currency name will be pluralized to that number if possible. - :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. + :param locale: the `Locale` object or locale identifier. + Defaults to the system currency locale or numeric locale. """ - loc = Locale.parse(locale or LC_NUMERIC) + loc = Locale.parse(locale or LC_MONETARY) if count is not None: try: plural_form = loc.plural_form(count) @@ -142,9 +145,10 @@ def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> st u'$' :param currency: the currency code. - :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. + :param locale: the `Locale` object or locale identifier. + Defaults to the system currency locale or numeric locale. """ - return Locale.parse(locale or LC_NUMERIC).currency_symbols.get(currency, currency) + return Locale.parse(locale or LC_MONETARY).currency_symbols.get(currency, currency) def get_currency_precision(currency: str) -> int: @@ -181,9 +185,10 @@ def get_currency_unit_pattern( :param currency: the currency code. :param count: the optional count. If provided the unit pattern for that number will be returned. - :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. + :param locale: the `Locale` object or locale identifier. + Defaults to the system currency locale or numeric locale. """ - loc = Locale.parse(locale or LC_NUMERIC) + loc = Locale.parse(locale or LC_MONETARY) if count is not None: plural_form = loc.plural_form(count) try: @@ -760,7 +765,8 @@ def format_currency( :param number: the number to format :param currency: the currency code :param format: the format string to use - :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. + :param locale: the `Locale` object or locale identifier. + Defaults to the system currency locale or numeric locale. :param currency_digits: use the currency's natural number of decimal digits :param format_type: the currency format type to use :param decimal_quantization: Truncate and round high-precision numbers to @@ -771,7 +777,7 @@ def format_currency( The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ - locale = Locale.parse(locale or LC_NUMERIC) + locale = Locale.parse(locale or LC_MONETARY) if format_type == 'name': return _format_currency_long_name( @@ -860,13 +866,14 @@ def format_compact_currency( :param number: the number to format :param currency: the currency code :param format_type: the compact format type to use. Defaults to "short". - :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. + :param locale: the `Locale` object or locale identifier. + Defaults to the system currency locale or numeric locale. :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". The special value "default" will use the default numbering system of the locale. :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. """ - locale = Locale.parse(locale or LC_NUMERIC) + locale = Locale.parse(locale or LC_MONETARY) try: compact_format = locale.compact_currency_formats[format_type] except KeyError as error: diff --git a/tests/test_numbers.py b/tests/test_numbers.py index cf0e8d1ba..45892fe2c 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -485,19 +485,6 @@ def test_format_currency(): == 'US$0,00') # other -def test_format_currency_with_none_locale_with_default(monkeypatch): - """Test that the default locale is used when locale is None.""" - monkeypatch.setattr(numbers, "LC_NUMERIC", "fi_FI") - assert numbers.format_currency(0, "USD", locale=None) == "0,00\xa0$" - - -def test_format_currency_with_none_locale(monkeypatch): - """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too.""" - monkeypatch.setattr(numbers, "LC_NUMERIC", None) # Pretend we couldn't find any locale when importing the module - with pytest.raises(TypeError, match="Empty"): - numbers.format_currency(0, "USD", locale=None) - - def test_format_currency_format_type(): assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type="standard") @@ -867,3 +854,31 @@ def test_single_quotes_in_pattern(): assert numbers.format_decimal(123, "'$'''0", locale='en') == "$'123" assert numbers.format_decimal(12, "'#'0 o''clock", locale='en') == "#12 o'clock" + + +def test_format_currency_with_none_locale_with_default(monkeypatch): + """Test that the default locale is used when locale is None.""" + monkeypatch.setattr(numbers, "LC_MONETARY", "fi_FI") + monkeypatch.setattr(numbers, "LC_NUMERIC", None) + assert numbers.format_currency(0, "USD", locale=None) == "0,00\xa0$" + + +def test_format_currency_with_none_locale(monkeypatch): + """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too.""" + monkeypatch.setattr(numbers, "LC_MONETARY", None) # Pretend we couldn't find any locale when importing the module + with pytest.raises(TypeError, match="Empty"): + numbers.format_currency(0, "USD", locale=None) + + +def test_format_decimal_with_none_locale_with_default(monkeypatch): + """Test that the default locale is used when locale is None.""" + monkeypatch.setattr(numbers, "LC_NUMERIC", "fi_FI") + monkeypatch.setattr(numbers, "LC_MONETARY", None) + assert numbers.format_decimal("1.23", locale=None) == "1,23" + + +def test_format_decimal_with_none_locale(monkeypatch): + """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too.""" + monkeypatch.setattr(numbers, "LC_NUMERIC", None) # Pretend we couldn't find any locale when importing the module + with pytest.raises(TypeError, match="Empty"): + numbers.format_decimal(0, locale=None)