diff options
author | Sergey B Kirpichev <skirpichev@gmail.com> | 2021-06-07 07:06:33 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-07 07:06:33 (GMT) |
commit | 89e50ab36fac6a0e7f1998501f36fcd2872a6604 (patch) | |
tree | 8f708926c1f7f64b60d7da505fd0dbe8e1d2d4a4 | |
parent | afb2eed72b32a35b4726ff35f92e4fbf54926046 (diff) | |
download | cpython-89e50ab36fac6a0e7f1998501f36fcd2872a6604.zip cpython-89e50ab36fac6a0e7f1998501f36fcd2872a6604.tar.gz cpython-89e50ab36fac6a0e7f1998501f36fcd2872a6604.tar.bz2 |
bpo-44258: support PEP 515 for Fraction's initialization from string (GH-26422)
* bpo-44258: support PEP 515 for Fraction's initialization from string
* regexps's version
* A different regexps version, which doesn't suffer from catastrophic backtracking
* revert denom -> den
* strip "_" from the decimal str, add few tests
* drop redundant tests
* Add versionchanged & whatsnew entry
* Amend Fraction constructor docs
* Change .. versionchanged:...
-rw-r--r-- | Doc/library/fractions.rst | 7 | ||||
-rw-r--r-- | Doc/whatsnew/3.11.rst | 5 | ||||
-rw-r--r-- | Lib/fractions.py | 21 | ||||
-rw-r--r-- | Lib/test/test_fractions.py | 62 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2021-05-28-09-43-33.bpo-44258.nh5F7R.rst | 1 |
5 files changed, 85 insertions, 11 deletions
diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index a4d006e..d04de8f 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -42,7 +42,8 @@ another rational number, or from a string. where the optional ``sign`` may be either '+' or '-' and ``numerator`` and ``denominator`` (if present) are strings of - decimal digits. In addition, any string that represents a finite + decimal digits (underscores may be used to delimit digits as with + integral literals in code). In addition, any string that represents a finite value and is accepted by the :class:`float` constructor is also accepted by the :class:`Fraction` constructor. In either form the input string may also have leading and/or trailing whitespace. @@ -89,6 +90,10 @@ another rational number, or from a string. and *denominator*. :func:`math.gcd` always return a :class:`int` type. Previously, the GCD type depended on *numerator* and *denominator*. + .. versionchanged:: 3.11 + Underscores are now permitted when creating a :class:`Fraction` instance + from a string, following :PEP:`515` rules. + .. attribute:: numerator Numerator of the Fraction in lowest term. diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 1ea8cba..8c81b08 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -86,6 +86,11 @@ New Modules Improved Modules ================ +fractions +--------- + +Support :PEP:`515`-style initialization of :class:`~fractions.Fraction` from +string. (Contributed by Sergey B Kirpichev in :issue:`44258`.) Optimizations ============= diff --git a/Lib/fractions.py b/Lib/fractions.py index 66e6831..180cd94 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -21,17 +21,17 @@ _PyHASH_MODULUS = sys.hash_info.modulus _PyHASH_INF = sys.hash_info.inf _RATIONAL_FORMAT = re.compile(r""" - \A\s* # optional whitespace at the start, then - (?P<sign>[-+]?) # an optional sign, then - (?=\d|\.\d) # lookahead for digit or .digit - (?P<num>\d*) # numerator (possibly empty) - (?: # followed by - (?:/(?P<denom>\d+))? # an optional denominator - | # or - (?:\.(?P<decimal>\d*))? # an optional fractional part - (?:E(?P<exp>[-+]?\d+))? # and optional exponent + \A\s* # optional whitespace at the start, + (?P<sign>[-+]?) # an optional sign, then + (?=\d|\.\d) # lookahead for digit or .digit + (?P<num>\d*|\d+(_\d+)*) # numerator (possibly empty) + (?: # followed by + (?:/(?P<denom>\d+(_\d+)*))? # an optional denominator + | # or + (?:\.(?P<decimal>d*|\d+(_\d+)*))? # an optional fractional part + (?:E(?P<exp>[-+]?\d+(_\d+)*))? # and optional exponent ) - \s*\Z # and optional whitespace to finish + \s*\Z # and optional whitespace to finish """, re.VERBOSE | re.IGNORECASE) @@ -122,6 +122,7 @@ class Fraction(numbers.Rational): denominator = 1 decimal = m.group('decimal') if decimal: + decimal = decimal.replace('_', '') scale = 10**len(decimal) numerator = numerator * scale + int(decimal) denominator *= scale diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 949ddd9..bbf7709 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -173,6 +173,12 @@ class FractionTest(unittest.TestCase): self.assertEqual((-12300, 1), _components(F("-1.23e4"))) self.assertEqual((0, 1), _components(F(" .0e+0\t"))) self.assertEqual((0, 1), _components(F("-0.000e0"))) + self.assertEqual((123, 1), _components(F("1_2_3"))) + self.assertEqual((41, 107), _components(F("1_2_3/3_2_1"))) + self.assertEqual((6283, 2000), _components(F("3.14_15"))) + self.assertEqual((6283, 2*10**13), _components(F("3.14_15e-1_0"))) + self.assertEqual((101, 100), _components(F("1.01"))) + self.assertEqual((101, 100), _components(F("1.0_1"))) self.assertRaisesMessage( ZeroDivisionError, "Fraction(3, 0)", @@ -210,6 +216,62 @@ class FractionTest(unittest.TestCase): # Allow 3. and .3, but not . ValueError, "Invalid literal for Fraction: '.'", F, ".") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '_'", + F, "_") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '_1'", + F, "_1") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1__2'", + F, "1__2") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '/_'", + F, "/_") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1_/'", + F, "1_/") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '_1/'", + F, "_1/") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1__2/'", + F, "1__2/") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1/_'", + F, "1/_") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1/_1'", + F, "1/_1") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1/1__2'", + F, "1/1__2") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1._111'", + F, "1._111") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1.1__1'", + F, "1.1__1") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1.1e+_1'", + F, "1.1e+_1") + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1.1e+1__1'", + F, "1.1e+1__1") + # Test catastrophic backtracking. + val = "9"*50 + "_" + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '" + val + "'", + F, val) + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1/" + val + "'", + F, "1/" + val) + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1." + val + "'", + F, "1." + val) + self.assertRaisesMessage( + ValueError, "Invalid literal for Fraction: '1.1+e" + val + "'", + F, "1.1+e" + val) def testImmutable(self): r = F(7, 3) diff --git a/Misc/NEWS.d/next/Library/2021-05-28-09-43-33.bpo-44258.nh5F7R.rst b/Misc/NEWS.d/next/Library/2021-05-28-09-43-33.bpo-44258.nh5F7R.rst new file mode 100644 index 0000000..b963689 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-05-28-09-43-33.bpo-44258.nh5F7R.rst @@ -0,0 +1 @@ +Support PEP 515 for Fraction's initialization from string. |