diff --git a/babel/core.py b/babel/core.py index 39a2cf6ed..983fccc8d 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,20 @@ 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: locale = os.getenv(name) if locale: if name == 'LANGUAGE' and ':' in locale: diff --git a/babel/numbers.py b/babel/numbers.py index 6d334918a..597bdd882 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`` @@ -31,6 +32,7 @@ if TYPE_CHECKING: from typing_extensions import Literal +LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC')) LC_NUMERIC = default_locale('LC_NUMERIC') @@ -120,9 +122,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) @@ -145,9 +148,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: @@ -184,9 +188,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: @@ -763,7 +768,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 @@ -774,7 +780,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( @@ -863,13 +869,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_core.py b/tests/test_core.py index b6c55626c..5ea4eb9d6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -277,6 +277,22 @@ def test_default_locale(monkeypatch): assert default_locale() == 'en_US_POSIX' +def test_default_locale_multiple_args(monkeypatch): + for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC']: + monkeypatch.setenv(name, '') + 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') 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)