Skip to content

Commit 89e50ab

Browse files
authored
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:...
1 parent afb2eed commit 89e50ab

File tree

5 files changed

+85
-11
lines changed

5 files changed

+85
-11
lines changed

Doc/library/fractions.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ another rational number, or from a string.
4242

4343
where the optional ``sign`` may be either '+' or '-' and
4444
``numerator`` and ``denominator`` (if present) are strings of
45-
decimal digits. In addition, any string that represents a finite
45+
decimal digits (underscores may be used to delimit digits as with
46+
integral literals in code). In addition, any string that represents a finite
4647
value and is accepted by the :class:`float` constructor is also
4748
accepted by the :class:`Fraction` constructor. In either form the
4849
input string may also have leading and/or trailing whitespace.
@@ -89,6 +90,10 @@ another rational number, or from a string.
8990
and *denominator*. :func:`math.gcd` always return a :class:`int` type.
9091
Previously, the GCD type depended on *numerator* and *denominator*.
9192

93+
.. versionchanged:: 3.11
94+
Underscores are now permitted when creating a :class:`Fraction` instance
95+
from a string, following :PEP:`515` rules.
96+
9297
.. attribute:: numerator
9398

9499
Numerator of the Fraction in lowest term.

Doc/whatsnew/3.11.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ New Modules
8686
Improved Modules
8787
================
8888

89+
fractions
90+
---------
91+
92+
Support :PEP:`515`-style initialization of :class:`~fractions.Fraction` from
93+
string. (Contributed by Sergey B Kirpichev in :issue:`44258`.)
8994

9095
Optimizations
9196
=============

Lib/fractions.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@
2121
_PyHASH_INF = sys.hash_info.inf
2222

2323
_RATIONAL_FORMAT = re.compile(r"""
24-
\A\s* # optional whitespace at the start, then
25-
(?P<sign>[-+]?) # an optional sign, then
26-
(?=\d|\.\d) # lookahead for digit or .digit
27-
(?P<num>\d*) # numerator (possibly empty)
28-
(?: # followed by
29-
(?:/(?P<denom>\d+))? # an optional denominator
30-
| # or
31-
(?:\.(?P<decimal>\d*))? # an optional fractional part
32-
(?:E(?P<exp>[-+]?\d+))? # and optional exponent
24+
\A\s* # optional whitespace at the start,
25+
(?P<sign>[-+]?) # an optional sign, then
26+
(?=\d|\.\d) # lookahead for digit or .digit
27+
(?P<num>\d*|\d+(_\d+)*) # numerator (possibly empty)
28+
(?: # followed by
29+
(?:/(?P<denom>\d+(_\d+)*))? # an optional denominator
30+
| # or
31+
(?:\.(?P<decimal>d*|\d+(_\d+)*))? # an optional fractional part
32+
(?:E(?P<exp>[-+]?\d+(_\d+)*))? # and optional exponent
3333
)
34-
\s*\Z # and optional whitespace to finish
34+
\s*\Z # and optional whitespace to finish
3535
""", re.VERBOSE | re.IGNORECASE)
3636

3737

@@ -122,6 +122,7 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True):
122122
denominator = 1
123123
decimal = m.group('decimal')
124124
if decimal:
125+
decimal = decimal.replace('_', '')
125126
scale = 10**len(decimal)
126127
numerator = numerator * scale + int(decimal)
127128
denominator *= scale

Lib/test/test_fractions.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def testFromString(self):
173173
self.assertEqual((-12300, 1), _components(F("-1.23e4")))
174174
self.assertEqual((0, 1), _components(F(" .0e+0\t")))
175175
self.assertEqual((0, 1), _components(F("-0.000e0")))
176+
self.assertEqual((123, 1), _components(F("1_2_3")))
177+
self.assertEqual((41, 107), _components(F("1_2_3/3_2_1")))
178+
self.assertEqual((6283, 2000), _components(F("3.14_15")))
179+
self.assertEqual((6283, 2*10**13), _components(F("3.14_15e-1_0")))
180+
self.assertEqual((101, 100), _components(F("1.01")))
181+
self.assertEqual((101, 100), _components(F("1.0_1")))
176182

177183
self.assertRaisesMessage(
178184
ZeroDivisionError, "Fraction(3, 0)",
@@ -210,6 +216,62 @@ def testFromString(self):
210216
# Allow 3. and .3, but not .
211217
ValueError, "Invalid literal for Fraction: '.'",
212218
F, ".")
219+
self.assertRaisesMessage(
220+
ValueError, "Invalid literal for Fraction: '_'",
221+
F, "_")
222+
self.assertRaisesMessage(
223+
ValueError, "Invalid literal for Fraction: '_1'",
224+
F, "_1")
225+
self.assertRaisesMessage(
226+
ValueError, "Invalid literal for Fraction: '1__2'",
227+
F, "1__2")
228+
self.assertRaisesMessage(
229+
ValueError, "Invalid literal for Fraction: '/_'",
230+
F, "/_")
231+
self.assertRaisesMessage(
232+
ValueError, "Invalid literal for Fraction: '1_/'",
233+
F, "1_/")
234+
self.assertRaisesMessage(
235+
ValueError, "Invalid literal for Fraction: '_1/'",
236+
F, "_1/")
237+
self.assertRaisesMessage(
238+
ValueError, "Invalid literal for Fraction: '1__2/'",
239+
F, "1__2/")
240+
self.assertRaisesMessage(
241+
ValueError, "Invalid literal for Fraction: '1/_'",
242+
F, "1/_")
243+
self.assertRaisesMessage(
244+
ValueError, "Invalid literal for Fraction: '1/_1'",
245+
F, "1/_1")
246+
self.assertRaisesMessage(
247+
ValueError, "Invalid literal for Fraction: '1/1__2'",
248+
F, "1/1__2")
249+
self.assertRaisesMessage(
250+
ValueError, "Invalid literal for Fraction: '1._111'",
251+
F, "1._111")
252+
self.assertRaisesMessage(
253+
ValueError, "Invalid literal for Fraction: '1.1__1'",
254+
F, "1.1__1")
255+
self.assertRaisesMessage(
256+
ValueError, "Invalid literal for Fraction: '1.1e+_1'",
257+
F, "1.1e+_1")
258+
self.assertRaisesMessage(
259+
ValueError, "Invalid literal for Fraction: '1.1e+1__1'",
260+
F, "1.1e+1__1")
261+
# Test catastrophic backtracking.
262+
val = "9"*50 + "_"
263+
self.assertRaisesMessage(
264+
ValueError, "Invalid literal for Fraction: '" + val + "'",
265+
F, val)
266+
self.assertRaisesMessage(
267+
ValueError, "Invalid literal for Fraction: '1/" + val + "'",
268+
F, "1/" + val)
269+
self.assertRaisesMessage(
270+
ValueError, "Invalid literal for Fraction: '1." + val + "'",
271+
F, "1." + val)
272+
self.assertRaisesMessage(
273+
ValueError, "Invalid literal for Fraction: '1.1+e" + val + "'",
274+
F, "1.1+e" + val)
213275

214276
def testImmutable(self):
215277
r = F(7, 3)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support PEP 515 for Fraction's initialization from string.

0 commit comments

Comments
 (0)