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:...
This commit is contained in:
Sergey B Kirpichev 2021-06-07 10:06:33 +03:00 committed by GitHub
parent afb2eed72b
commit 89e50ab36f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 85 additions and 11 deletions

View File

@ -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.

View File

@ -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
=============

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1 @@
Support PEP 515 for Fraction's initialization from string.