Repository: python-babel/babel (python)
Issue: test_extract_operands never fails
py.test warns me about this:

> babel/tests/test_plural.py:257: SyntaxWarning: assertion is always true, perhaps remove parentheses?

``` python
    assert (plural.extract_operands(source) ==
            Decimal(n), i, v, w, f, t)
```

Just removing the parens (and rewrapping the statement) makes the test fail though, so it needs some work.

Pull request description: Fix float conversion in `extract_operands` (and the relevant test)
Fixes #421


Patch:
diff --git a/babel/plural.py b/babel/plural.py
index 0b8e425..99e9a65 100644
--- a/babel/plural.py
+++ b/babel/plural.py
@@ -9,7 +9,6 @@
     :license: BSD, see LICENSE for more details.
 """
 import re
-import sys
 
 from babel._compat import decimal
 
@@ -19,10 +18,27 @@ _fallback_tag = 'other'
 
 
 def extract_operands(source):
-    """Extract operands from a decimal, a float or an int, according to
-    `CLDR rules`_.
+    """Extract operands from a decimal, a float or an int, according to `CLDR rules`_.
+
+    The result is a 6-tuple (n, i, v, w, f, t), where those symbols are as follows:
+
+    ====== ===============================================================
+    Symbol Value
+    ------ ---------------------------------------------------------------
+    n      absolute value of the source number (integer and decimals).
+    i      integer digits of n.
+    v      number of visible fraction digits in n, with trailing zeros.
+    w      number of visible fraction digits in n, without trailing zeros.
+    f      visible fractional digits in n, with trailing zeros.
+    t      visible fractional digits in n, without trailing zeros.
+    ====== ===============================================================
 
     .. _`CLDR rules`: http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Operands
+
+    :param source: A real number
+    :type source: int|float|decimal.Decimal
+    :return: A n-i-v-w-f-t tuple
+    :rtype: tuple[decimal.Decimal, int, int, int, int, int]
     """
     n = abs(source)
     i = int(n)
@@ -30,10 +46,17 @@ def extract_operands(source):
         if i == n:
             n = i
         else:
-            # 2.6's Decimal cannot convert from float directly
-            if sys.version_info < (2, 7):
-                n = str(n)
-            n = decimal.Decimal(n)
+            # Cast the `float` to a number via the string representation.
+            # This is required for Python 2.6 anyway (it will straight out fail to
+            # do the conversion otherwise), and it's highly unlikely that the user
+            # actually wants the lossless conversion behavior (quoting the Python
+            # documentation):
+            # > If value is a float, the binary floating point value is losslessly
+            # > converted to its exact decimal equivalent.
+            # > This conversion can often require 53 or more digits of precision.
+            # Should the user want that behavior, they can simply pass in a pre-
+            # converted `Decimal` instance of desired accuracy.
+            n = decimal.Decimal(str(n))
 
     if isinstance(n, decimal.Decimal):
         dec_tuple = n.as_tuple()


Test patch:
diff --git a/tests/test_plural.py b/tests/test_plural.py
index 6406af8..be74149 100644
--- a/tests/test_plural.py
+++ b/tests/test_plural.py
@@ -16,6 +16,8 @@ import pytest
 from babel import plural, localedata
 from babel._compat import decimal
 
+EPSILON = decimal.Decimal("0.0001")
+
 
 def test_plural_rule():
     rule = plural.PluralRule({'one': 'n is 1'})
@@ -240,12 +242,12 @@ class PluralRuleParserTestCase(unittest.TestCase):
 
 EXTRACT_OPERANDS_TESTS = (
     (1, 1, 1, 0, 0, 0, 0),
-    ('1.0', '1.0', 1, 1, 0, 0, 0),
-    ('1.00', '1.00', 1, 2, 0, 0, 0),
-    ('1.3', '1.3', 1, 1, 1, 3, 3),
-    ('1.30', '1.30', 1, 2, 1, 30, 3),
-    ('1.03', '1.03', 1, 2, 2, 3, 3),
-    ('1.230', '1.230', 1, 3, 2, 230, 23),
+    (decimal.Decimal('1.0'), '1.0', 1, 1, 0, 0, 0),
+    (decimal.Decimal('1.00'), '1.00', 1, 2, 0, 0, 0),
+    (decimal.Decimal('1.3'), '1.3', 1, 1, 1, 3, 3),
+    (decimal.Decimal('1.30'), '1.30', 1, 2, 1, 30, 3),
+    (decimal.Decimal('1.03'), '1.03', 1, 2, 2, 3, 3),
+    (decimal.Decimal('1.230'), '1.230', 1, 3, 2, 230, 23),
     (-1, 1, 1, 0, 0, 0, 0),
     (1.3, '1.3', 1, 1, 1, 3, 3),
 )
@@ -253,9 +255,13 @@ EXTRACT_OPERANDS_TESTS = (
 
 @pytest.mark.parametrize('source,n,i,v,w,f,t', EXTRACT_OPERANDS_TESTS)
 def test_extract_operands(source, n, i, v, w, f, t):
-    source = decimal.Decimal(source) if isinstance(source, str) else source
-    assert (plural.extract_operands(source) ==
-            decimal.Decimal(n), i, v, w, f, t)
+    e_n, e_i, e_v, e_w, e_f, e_t = plural.extract_operands(source)
+    assert abs(e_n - decimal.Decimal(n)) <= EPSILON  # float-decimal conversion inaccuracy
+    assert e_i == i
+    assert e_v == v
+    assert e_w == w
+    assert e_f == f
+    assert e_t == t
 
 
 @pytest.mark.parametrize('locale', ('ru', 'pl'))