summaryrefslogtreecommitdiffstats
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
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:...
-rw-r--r--Doc/library/fractions.rst7
-rw-r--r--Doc/whatsnew/3.11.rst5
-rw-r--r--Lib/fractions.py21
-rw-r--r--Lib/test/test_fractions.py62
-rw-r--r--Misc/NEWS.d/next/Library/2021-05-28-09-43-33.bpo-44258.nh5F7R.rst1
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.