Repository: python-babel/babel
Pull request #724 (2020-07-31 14:23:54)

LazyProxy: Handle AttributeError in specified func
Fixes https://github.com/python-babel/babel/issues/723

codecov-commenter: # [Codecov](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=h1) Report
> Merging [#724](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=desc) into [master](https://codecov.io/gh/python-babel/babel/commit/e7e4265d9a037ac38bba99f8513fb9e48a1081ba&el=desc) will **decrease** coverage by `0.09%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/python-babel/babel/pull/724/graphs/tree.svg?width=650&height=150&src=pr&token=lEYaWCjCnu)](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #724      +/-   ##
==========================================
- Coverage   90.97%   90.87%   -0.10%     
==========================================
  Files          24       24              
  Lines        4176     4131      -45     
==========================================
- Hits         3799     3754      -45     
  Misses        377      377              
```


| [Impacted Files](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [babel/support.py](https://codecov.io/gh/python-babel/babel/pull/724/diff?src=pr&el=tree#diff-YmFiZWwvc3VwcG9ydC5weQ==) | `83.14% <100.00%> (+0.39%)` | :arrow_up: |
| [babel/core.py](https://codecov.io/gh/python-babel/babel/pull/724/diff?src=pr&el=tree#diff-YmFiZWwvY29yZS5weQ==) | `95.51% <0.00%> (-0.54%)` | :arrow_down: |
| [babel/messages/catalog.py](https://codecov.io/gh/python-babel/babel/pull/724/diff?src=pr&el=tree#diff-YmFiZWwvbWVzc2FnZXMvY2F0YWxvZy5weQ==) | `95.56% <0.00%> (-0.08%)` | :arrow_down: |
| [babel/dates.py](https://codecov.io/gh/python-babel/babel/pull/724/diff?src=pr&el=tree#diff-YmFiZWwvZGF0ZXMucHk=) | `91.55% <0.00%> (-0.05%)` | :arrow_down: |
| [babel/plural.py](https://codecov.io/gh/python-babel/babel/pull/724/diff?src=pr&el=tree#diff-YmFiZWwvcGx1cmFsLnB5) | `96.04% <0.00%> (-0.03%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=footer). Last update [e7e4265...24fbb2b](https://codecov.io/gh/python-babel/babel/pull/724?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/babel/numbers.py b/babel/numbers.py
index cf819fc..bbc5e21 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -373,7 +373,7 @@ def get_decimal_quantum(precision):
 
 
 def format_decimal(
-        number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
+        number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
     u"""Return the given decimal number formatted for a specific locale.
 
     >>> format_decimal(1.2345, locale='en_US')
@@ -401,19 +401,25 @@ def format_decimal(
     u'1.235'
     >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
     u'1.2346'
+    >>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
+    u'12345,67'
+    >>> format_decimal(12345.67, locale='en_US', group_separator=True)
+    u'12,345.67'
 
     :param number: the number to format
     :param format:
     :param locale: the `Locale` object or locale identifier
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
+    :param group_separator: Boolean to switch group separator on/off in a locale's
+                            number format.
     """
     locale = Locale.parse(locale)
     if not format:
         format = locale.decimal_formats.get(format)
     pattern = parse_pattern(format)
     return pattern.apply(
-        number, locale, decimal_quantization=decimal_quantization)
+        number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
 
 
 class UnknownCurrencyFormatError(KeyError):
@@ -422,7 +428,7 @@ class UnknownCurrencyFormatError(KeyError):
 
 def format_currency(
         number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
-        format_type='standard', decimal_quantization=True):
+        format_type='standard', decimal_quantization=True, group_separator=True):
     u"""Return formatted currency value.
 
     >>> format_currency(1099.98, 'USD', locale='en_US')
@@ -472,6 +478,12 @@ def format_currency(
         ...
     UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
 
+    >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
+    u'$101299.98'
+
+    >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
+    u'$101,299.98'
+
     You can also pass format_type='name' to use long display names. The order of
     the number and currency name, along with the correct localized plural form
     of the currency name, is chosen according to locale:
@@ -500,12 +512,14 @@ def format_currency(
     :param format_type: the currency format type to use
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
+    :param group_separator: Boolean to switch group separator on/off in a locale's
+                            number format.
 
     """
     if format_type == 'name':
         return _format_currency_long_name(number, currency, format=format,
                                           locale=locale, currency_digits=currency_digits,
-                                          decimal_quantization=decimal_quantization)
+                                          decimal_quantization=decimal_quantization, group_separator=group_separator)
     locale = Locale.parse(locale)
     if format:
         pattern = parse_pattern(format)
@@ -518,12 +532,12 @@ def format_currency(
 
     return pattern.apply(
         number, locale, currency=currency, currency_digits=currency_digits,
-        decimal_quantization=decimal_quantization)
+        decimal_quantization=decimal_quantization, group_separator=group_separator)
 
 
 def _format_currency_long_name(
         number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
-        format_type='standard', decimal_quantization=True):
+        format_type='standard', decimal_quantization=True, group_separator=True):
     # Algorithm described here:
     # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
     locale = Locale.parse(locale)
@@ -552,13 +566,13 @@ def _format_currency_long_name(
 
     number_part = pattern.apply(
         number, locale, currency=currency, currency_digits=currency_digits,
-        decimal_quantization=decimal_quantization)
+        decimal_quantization=decimal_quantization, group_separator=group_separator)
 
     return unit_pattern.format(number_part, display_name)
 
 
 def format_percent(
-        number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
+        number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
     """Return formatted percent value for a specific locale.
 
     >>> format_percent(0.34, locale='en_US')
@@ -582,18 +596,26 @@ def format_percent(
     >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
     u'2,398.76%'
 
+    >>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
+    u'22929112%'
+
+    >>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
+    u'22.929.112%'
+
     :param number: the percent number to format
     :param format:
     :param locale: the `Locale` object or locale identifier
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
+    :param group_separator: Boolean to switch group separator on/off in a locale's
+                            number format.
     """
     locale = Locale.parse(locale)
     if not format:
         format = locale.percent_formats.get(format)
     pattern = parse_pattern(format)
     return pattern.apply(
-        number, locale, decimal_quantization=decimal_quantization)
+        number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
 
 
 def format_scientific(
@@ -913,6 +935,7 @@ class NumberPattern(object):
         currency_digits=True,
         decimal_quantization=True,
         force_frac=None,
+        group_separator=True,
     ):
         """Renders into a string a number following the defined pattern.
 
@@ -952,8 +975,8 @@ class NumberPattern(object):
         if self.exp_prec:
             value, exp, exp_sign = self.scientific_notation_elements(value, locale)
 
-        # Adjust the precision of the fractionnal part and force it to the
-        # currency's if neccessary.
+        # Adjust the precision of the fractional part and force it to the
+        # currency's if necessary.
         if force_frac:
             # TODO (3.x?): Remove this parameter
             warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning)
@@ -975,7 +998,7 @@ class NumberPattern(object):
         # Render scientific notation.
         if self.exp_prec:
             number = ''.join([
-                self._quantize_value(value, locale, frac_prec),
+                self._quantize_value(value, locale, frac_prec, group_separator),
                 get_exponential_symbol(locale),
                 exp_sign,
                 self._format_int(
@@ -993,7 +1016,7 @@ class NumberPattern(object):
 
         # A normal number pattern.
         else:
-            number = self._quantize_value(value, locale, frac_prec)
+            number = self._quantize_value(value, locale, frac_prec, group_separator)
 
         retval = ''.join([
             self.prefix[is_negative],
@@ -1060,13 +1083,14 @@ class NumberPattern(object):
             gsize = self.grouping[1]
         return value + ret
 
-    def _quantize_value(self, value, locale, frac_prec):
+    def _quantize_value(self, value, locale, frac_prec, group_separator):
         quantum = get_decimal_quantum(frac_prec[1])
         rounded = value.quantize(quantum)
         a, sep, b = "{:f}".format(rounded).partition(".")
-        number = (self._format_int(a, self.int_prec[0],
-                                   self.int_prec[1], locale) +
-                  self._format_frac(b or '0', locale, frac_prec))
+        integer_part = a
+        if group_separator:
+            integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale)
+        number = integer_part + self._format_frac(b or '0', locale, frac_prec)
         return number
 
     def _format_frac(self, value, locale, force_frac=None):
diff --git a/babel/support.py b/babel/support.py
index efe41d5..47f812d 100644
--- a/babel/support.py
+++ b/babel/support.py
@@ -165,7 +165,7 @@ class LazyProxy(object):
     Hello, universe!
     Hello, world!
     """
-    __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled']
+    __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error']
 
     def __init__(self, func, *args, **kwargs):
         is_cache_enabled = kwargs.pop('enable_cache', True)
@@ -175,11 +175,17 @@ class LazyProxy(object):
         object.__setattr__(self, '_kwargs', kwargs)
         object.__setattr__(self, '_is_cache_enabled', is_cache_enabled)
         object.__setattr__(self, '_value', None)
+        object.__setattr__(self, '_attribute_error', None)
 
     @property
     def value(self):
         if self._value is None:
-            value = self._func(*self._args, **self._kwargs)
+            try:
+                value = self._func(*self._args, **self._kwargs)
+            except AttributeError as error:
+                object.__setattr__(self, '_attribute_error', error)
+                raise
+
             if not self._is_cache_enabled:
                 return value
             object.__setattr__(self, '_value', value)
@@ -249,6 +255,8 @@ class LazyProxy(object):
         delattr(self.value, name)
 
     def __getattr__(self, name):
+        if self._attribute_error is not None:
+            raise self._attribute_error
         return getattr(self.value, name)
 
     def __setattr__(self, name, value):


Test patch:
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index a980a66..3db5f33 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -153,6 +153,36 @@ class FormatDecimalTestCase(unittest.TestCase):
         fmt = numbers.format_decimal(number, format="@@@", locale='en_US')
         self.assertEqual('0.000000700', fmt)
 
+    def test_group_separator(self):
+        self.assertEqual('29567.12', numbers.format_decimal(29567.12,
+                                                                     locale='en_US', group_separator=False))
+        self.assertEqual('29567,12', numbers.format_decimal(29567.12,
+                                                                     locale='fr_CA', group_separator=False))
+        self.assertEqual('29567,12', numbers.format_decimal(29567.12,
+                                                                     locale='pt_BR', group_separator=False))
+        self.assertEqual(u'$1099.98', numbers.format_currency(1099.98, 'USD',
+                                                             locale='en_US', group_separator=False))
+        self.assertEqual(u'101299,98\xa0€', numbers.format_currency(101299.98, 'EUR',
+                                                            locale='fr_CA', group_separator=False))
+        self.assertEqual('101299.98 euros', numbers.format_currency(101299.98, 'EUR',
+                                                            locale='en_US', group_separator=False, format_type='name'))
+        self.assertEqual(u'25123412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False))
+
+        self.assertEqual(u'29,567.12', numbers.format_decimal(29567.12,
+                                                            locale='en_US', group_separator=True))
+        self.assertEqual(u'29\u202f567,12', numbers.format_decimal(29567.12,
+                                                            locale='fr_CA', group_separator=True))
+        self.assertEqual(u'29.567,12', numbers.format_decimal(29567.12,
+                                                            locale='pt_BR', group_separator=True))
+        self.assertEqual(u'$1,099.98', numbers.format_currency(1099.98, 'USD',
+                                                              locale='en_US', group_separator=True))
+        self.assertEqual(u'101\u202f299,98\xa0\u20ac', numbers.format_currency(101299.98, 'EUR',
+                                                                    locale='fr_CA', group_separator=True))
+        self.assertEqual(u'101,299.98 euros', numbers.format_currency(101299.98, 'EUR',
+                                                                    locale='en_US', group_separator=True,
+                                                                    format_type='name'))
+        self.assertEqual(u'25\xa0123\xa0412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True))
+
 
 class NumberParsingTestCase(unittest.TestCase):
 
diff --git a/tests/test_support.py b/tests/test_support.py
index b4dd823..1b74ae8 100644
--- a/tests/test_support.py
+++ b/tests/test_support.py
@@ -279,6 +279,17 @@ class LazyProxyTestCase(unittest.TestCase):
         self.assertEqual(2, proxy.value)
         self.assertEqual(1, proxy_deepcopy.value)
 
+    def test_handle_attribute_error(self):
+
+        def raise_attribute_error():
+            raise AttributeError('message')
+
+        proxy = support.LazyProxy(raise_attribute_error)
+        with pytest.raises(AttributeError) as exception:
+            proxy.value
+
+        self.assertEqual('message', str(exception.value))
+
 
 def test_format_date():
     fmt = support.Format('en_US')