summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorSergey B Kirpichev <skirpichev@gmail.com>2021-06-07 07:06:33 (GMT)
committerGitHub <noreply@github.com>2021-06-07 07:06:33 (GMT)
commit89e50ab36fac6a0e7f1998501f36fcd2872a6604 (patch)
tree8f708926c1f7f64b60d7da505fd0dbe8e1d2d4a4 /Lib
parentafb2eed72b32a35b4726ff35f92e4fbf54926046 (diff)
downloadcpython-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:...
Diffstat (limited to 'Lib')
-rw-r--r--Lib/fractions.py21
-rw-r--r--Lib/test/test_fractions.py62
2 files changed, 73 insertions, 10 deletions
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)