mirror of https://github.com/python/cpython
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:
parent
afb2eed72b
commit
89e50ab36f
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
=============
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Support PEP 515 for Fraction's initialization from string.
|
Loading…
Reference in New Issue