Repository: python-babel/babel
Pull request #952 (2023-01-20 15:46:54)

Clean up babel.localtime

codecov[bot]: # [Codecov](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) Report
> Merging [#952](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) (a3e92dd) into [master](https://codecov.io/gh/python-babel/babel/commit/61be9dc5d6c9ed7d1fb9116ae81a205a5fbee473?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) (61be9dc) will **decrease** coverage by `0.69%`.
> The diff coverage is `50.00%`.

```diff
@@            Coverage Diff             @@
##           master     #952      +/-   ##
==========================================
- Coverage   90.48%   89.80%   -0.69%     
==========================================
  Files          24       25       +1     
  Lines        4393     4393              
==========================================
- Hits         3975     3945      -30     
- Misses        418      448      +30     
```


| [Impacted Files](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | Coverage Δ | |
|---|---|---|
| [babel/util.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvdXRpbC5weQ==) | `97.32% <ø> (ø)` | |
| [babel/localtime/\_\_init\_\_.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbG9jYWx0aW1lL19faW5pdF9fLnB5) | `69.23% <50.00%> (+7.06%)` | :arrow_up: |
| [babel/localtime/\_fallback.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbG9jYWx0aW1lL19mYWxsYmFjay5weQ==) | `50.00% <50.00%> (ø)` | |
| [babel/localtime/\_win32.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbG9jYWx0aW1lL193aW4zMi5weQ==) | `25.92% <0.00%> (-37.04%)` | :arrow_down: |
| [babel/localtime/\_helpers.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbG9jYWx0aW1lL19oZWxwZXJzLnB5) | `50.00% <0.00%> (-11.54%)` | :arrow_down: |
| [babel/localedata.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbG9jYWxlZGF0YS5weQ==) | `95.16% <0.00%> (-0.81%)` | :arrow_down: |
| [babel/messages/frontend.py](https://codecov.io/gh/python-babel/babel/pull/952?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#diff-YmFiZWwvbWVzc2FnZXMvZnJvbnRlbmQucHk=) | `86.78% <0.00%> (-0.74%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel)


Patch:
diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py
index 1d65fb2..29577fa 100644
--- a/babel/localtime/__init__.py
+++ b/babel/localtime/__init__.py
@@ -11,8 +11,6 @@
 
 import datetime
 import sys
-import time
-from threading import RLock
 
 if sys.platform == 'win32':
     from babel.localtime._win32 import _get_localzone
@@ -20,43 +18,15 @@ else:
     from babel.localtime._unix import _get_localzone
 
 
-_cached_tz = None
-_cache_lock = RLock()
-
-STDOFFSET = datetime.timedelta(seconds=-time.timezone)
-if time.daylight:
-    DSTOFFSET = datetime.timedelta(seconds=-time.altzone)
-else:
-    DSTOFFSET = STDOFFSET
-
-DSTDIFF = DSTOFFSET - STDOFFSET
-ZERO = datetime.timedelta(0)
-
-
-class _FallbackLocalTimezone(datetime.tzinfo):
-
-    def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta:
-        if self._isdst(dt):
-            return DSTOFFSET
-        else:
-            return STDOFFSET
-
-    def dst(self, dt: datetime.datetime) -> datetime.timedelta:
-        if self._isdst(dt):
-            return DSTDIFF
-        else:
-            return ZERO
-
-    def tzname(self, dt: datetime.datetime) -> str:
-        return time.tzname[self._isdst(dt)]
-
-    def _isdst(self, dt: datetime.datetime) -> bool:
-        tt = (dt.year, dt.month, dt.day,
-              dt.hour, dt.minute, dt.second,
-              dt.weekday(), 0, -1)
-        stamp = time.mktime(tt)
-        tt = time.localtime(stamp)
-        return tt.tm_isdst > 0
+# TODO(3.0): the offset constants are not part of the public API
+#            and should be removed
+from babel.localtime._fallback import (
+    DSTDIFF,  # noqa: F401
+    DSTOFFSET,  # noqa: F401
+    STDOFFSET,  # noqa: F401
+    ZERO,  # noqa: F401
+    _FallbackLocalTimezone,
+)
 
 
 def get_localzone() -> datetime.tzinfo:
diff --git a/babel/localtime/_fallback.py b/babel/localtime/_fallback.py
new file mode 100644
index 0000000..836f596
--- /dev/null
+++ b/babel/localtime/_fallback.py
@@ -0,0 +1,47 @@
+"""
+    babel.localtime._fallback
+    ~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    Emulated fallback local timezone when all else fails.
+
+    :copyright: (c) 2013-2023 by the Babel Team.
+    :license: BSD, see LICENSE for more details.
+"""
+
+import datetime
+import time
+
+STDOFFSET = datetime.timedelta(seconds=-time.timezone)
+if time.daylight:
+    DSTOFFSET = datetime.timedelta(seconds=-time.altzone)
+else:
+    DSTOFFSET = STDOFFSET
+
+DSTDIFF = DSTOFFSET - STDOFFSET
+ZERO = datetime.timedelta(0)
+
+
+class _FallbackLocalTimezone(datetime.tzinfo):
+
+    def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta:
+        if self._isdst(dt):
+            return DSTOFFSET
+        else:
+            return STDOFFSET
+
+    def dst(self, dt: datetime.datetime) -> datetime.timedelta:
+        if self._isdst(dt):
+            return DSTDIFF
+        else:
+            return ZERO
+
+    def tzname(self, dt: datetime.datetime) -> str:
+        return time.tzname[self._isdst(dt)]
+
+    def _isdst(self, dt: datetime.datetime) -> bool:
+        tt = (dt.year, dt.month, dt.day,
+              dt.hour, dt.minute, dt.second,
+              dt.weekday(), 0, -1)
+        stamp = time.mktime(tt)
+        tt = time.localtime(stamp)
+        return tt.tm_isdst > 0
diff --git a/babel/numbers.py b/babel/numbers.py
index d107046..ee9a133 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -126,7 +126,10 @@ def get_currency_name(
     """
     loc = Locale.parse(locale)
     if count is not None:
-        plural_form = loc.plural_form(count)
+        try:
+            plural_form = loc.plural_form(count)
+        except (OverflowError, ValueError):
+            plural_form = 'other'
         plural_names = loc._data['currency_names_plural']
         if currency in plural_names:
             currency_plural_names = plural_names[currency]
@@ -371,6 +374,17 @@ def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
     return Locale.parse(locale).number_symbols.get('group', ',')
 
 
+def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
+    """Return the symbol used by the locale to represent infinity.
+
+    >>> get_infinity_symbol('en_US')
+    u'∞'
+
+    :param locale: the `Locale` object or locale identifier
+    """
+    return Locale.parse(locale).number_symbols.get('infinity', '∞')
+
+
 def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
     """Return the given number formatted for a specific locale.
 
@@ -400,7 +414,8 @@ def get_decimal_precision(number: decimal.Decimal) -> int:
     # Copied from: https://github.com/mahmoud/boltons/pull/59
     assert isinstance(number, decimal.Decimal)
     decimal_tuple = number.normalize().as_tuple()
-    if decimal_tuple.exponent >= 0:
+    # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
+    if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
         return 0
     return abs(decimal_tuple.exponent)
 
@@ -515,6 +530,8 @@ def _get_compact_format(
     """
     if not isinstance(number, decimal.Decimal):
         number = decimal.Decimal(str(number))
+    if number.is_nan() or number.is_infinite():
+        return number, None
     format = None
     for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
         if abs(number) >= magnitude:
@@ -1287,6 +1304,9 @@ class NumberPattern:
         return value + ret
 
     def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str:
+        # If the number is +/-Infinity, we can't quantize it
+        if value.is_infinite():
+            return get_infinity_symbol(locale)
         quantum = get_decimal_quantum(frac_prec[1])
         rounded = value.quantize(quantum)
         a, sep, b = f"{rounded:f}".partition(".")
diff --git a/babel/util.py b/babel/util.py
index a5403e6..100e37d 100644
--- a/babel/util.py
+++ b/babel/util.py
@@ -255,10 +255,10 @@ class FixedOffsetTimezone(datetime.tzinfo):
 
 # Export the localtime functionality here because that's
 # where it was in the past.
+# TODO(3.0): remove these aliases
 UTC = dates.UTC
 LOCALTZ = dates.LOCALTZ
 get_localzone = localtime.get_localzone
-
 STDOFFSET = localtime.STDOFFSET
 DSTOFFSET = localtime.DSTOFFSET
 DSTDIFF = localtime.DSTDIFF


Test patch:
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index c8a04d6..d6028b3 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -111,6 +111,16 @@ class FormatDecimalTestCase(unittest.TestCase):
         number = decimal.Decimal("7E-7")
         assert numbers.format_decimal(number, format="@@@", locale='en_US') == '0.000000700'
 
+    def test_nan_and_infinity(self):
+        assert numbers.format_decimal(decimal.Decimal('Infinity'), locale='en_US') == '∞'
+        assert numbers.format_decimal(decimal.Decimal('-Infinity'), locale='en_US') == '-∞'
+        assert numbers.format_decimal(decimal.Decimal('NaN'), locale='en_US') == 'NaN'
+        assert numbers.format_compact_decimal(decimal.Decimal('Infinity'), locale='en_US', format_type="short") == '∞'
+        assert numbers.format_compact_decimal(decimal.Decimal('-Infinity'), locale='en_US', format_type="short") == '-∞'
+        assert numbers.format_compact_decimal(decimal.Decimal('NaN'), locale='en_US', format_type="short") == 'NaN'
+        assert numbers.format_currency(decimal.Decimal('Infinity'), 'USD', locale='en_US') == '$∞'
+        assert numbers.format_currency(decimal.Decimal('-Infinity'), 'USD', locale='en_US') == '-$∞'
+
     def test_group_separator(self):
         assert numbers.format_decimal(29567.12, locale='en_US', group_separator=False) == '29567.12'
         assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=False) == '29567,12'