Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer LC_MONETARY when formatting currencies #1173

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should calls like default_locale([None]) work without raising? (os.getenv raises for None)

else:
raise TypeError(f"Invalid type for category: {category!r}")

for name in varnames:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will technically try to look up an empty string now when using default_locale('') or default_locale(['']) but I guess that's a rare-enough case?

locale = os.getenv(name)
if locale:
if name == 'LANGUAGE' and ':' in locale:
Expand Down
29 changes: 18 additions & 11 deletions babel/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``

Expand All @@ -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')


Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
41 changes: 28 additions & 13 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Loading