Skip to content

Commit 03b57a5

Browse files
committed
Add classmethod ".from_number()".
See python/cpython#121800
1 parent 6d8844f commit 03b57a5

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ ChangeLog
99
* Generally use ``.as_integer_ratio()`` in the constructor if available.
1010
https://github.com/python/cpython/pull/120271
1111

12+
* Add a classmethod ``.from_number()`` that requires a number argument, not a string.
13+
https://github.com/python/cpython/pull/121800
14+
1215
* Mixed calculations with other ``Rational`` classes could return the wrong type.
1316
https://github.com/python/cpython/issues/119189
1417

src/quicktions.pyx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,8 @@ cdef class Fraction:
482482
return
483483

484484
else:
485-
raise TypeError("argument should be a string or a number")
485+
raise TypeError("argument should be a string or a Rational "
486+
"instance or have the as_integer_ratio() method")
486487

487488
elif type(numerator) is int is type(denominator):
488489
pass # *very* normal case
@@ -526,6 +527,34 @@ cdef class Fraction:
526527
self._numerator = numerator
527528
self._denominator = denominator
528529

530+
@classmethod
531+
def from_number(cls, number):
532+
"""Converts a finite real number to a rational number, exactly.
533+
534+
Beware that Fraction.from_number(0.3) != Fraction(3, 10).
535+
536+
"""
537+
if type(number) is int:
538+
return _fraction_from_coprime_ints(number, 1, cls)
539+
540+
elif type(number) is Fraction:
541+
return _fraction_from_coprime_ints((<Fraction> number)._numerator, (<Fraction> number)._denominator, cls)
542+
543+
elif isinstance(number, float):
544+
n, d = number.as_integer_ratio()
545+
return _fraction_from_coprime_ints(n, d, cls)
546+
547+
elif isinstance(number, Rational):
548+
return _fraction_from_coprime_ints(number.numerator, number.denominator, cls)
549+
550+
elif not isinstance(number, type) and hasattr(number, 'as_integer_ratio'):
551+
n, d = number.as_integer_ratio()
552+
return _fraction_from_coprime_ints(n, d, cls)
553+
554+
else:
555+
raise TypeError("argument should be a Rational instance or "
556+
"have the as_integer_ratio() method")
557+
529558
@classmethod
530559
def from_float(cls, f):
531560
"""Converts a finite float to a rational number, exactly.

src/test_fractions.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,13 @@ def __repr__(self):
392392
class RectComplex(Rect, complex):
393393
pass
394394

395+
class Ratio:
396+
def __init__(self, ratio):
397+
self._ratio = ratio
398+
def as_integer_ratio(self):
399+
return self._ratio
400+
401+
395402
class FractionTest(unittest.TestCase):
396403

397404
def assertTypedEquals(self, expected, actual):
@@ -474,14 +481,9 @@ def testInitFromDecimal(self):
474481
self.assertRaises(OverflowError, F, Decimal('-inf'))
475482

476483
def testInitFromIntegerRatio(self):
477-
class Ratio:
478-
def __init__(self, ratio):
479-
self._ratio = ratio
480-
def as_integer_ratio(self):
481-
return self._ratio
482-
483484
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
484-
errmsg = "argument should be a string or a number"
485+
errmsg = (r"argument should be a string or a Rational instance or "
486+
r"have the as_integer_ratio\(\) method")
485487
# the type also has an "as_integer_ratio" attribute.
486488
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
487489
# bad ratio
@@ -507,6 +509,8 @@ class B(metaclass=M):
507509
pass
508510
self.assertRaisesRegex(TypeError, errmsg, F, B)
509511
self.assertRaisesRegex(TypeError, errmsg, F, B())
512+
self.assertRaises(TypeError, F.from_number, B)
513+
self.assertRaises(TypeError, F.from_number, B())
510514

511515
def testFromString(self):
512516
self.assertEqual((5, 1), _components(F("5")))
@@ -746,6 +750,36 @@ def testFromDecimal(self):
746750
ValueError, "Cannot convert sNaN to Fraction.",
747751
F.from_decimal, Decimal("snan"))
748752

753+
def testFromNumber(self, cls=F):
754+
def check(arg, numerator, denominator):
755+
f = cls.from_number(arg)
756+
self.assertIs(type(f), cls)
757+
self.assertEqual(f.numerator, numerator)
758+
self.assertEqual(f.denominator, denominator)
759+
760+
check(10, 10, 1)
761+
check(2.5, 5, 2)
762+
check(Decimal('2.5'), 5, 2)
763+
check(F(22, 7), 22, 7)
764+
check(DummyFraction(22, 7), 22, 7)
765+
check(Rat(22, 7), 22, 7)
766+
check(Ratio((22, 7)), 22, 7)
767+
self.assertRaises(TypeError, cls.from_number, 3+4j)
768+
self.assertRaises(TypeError, cls.from_number, '5/2')
769+
self.assertRaises(TypeError, cls.from_number, [])
770+
self.assertRaises(OverflowError, cls.from_number, float('inf'))
771+
self.assertRaises(OverflowError, cls.from_number, Decimal('inf'))
772+
773+
# as_integer_ratio not defined in a class
774+
class A:
775+
pass
776+
a = A()
777+
a.as_integer_ratio = lambda: (9, 5)
778+
check(a, 9, 5)
779+
780+
def testFromNumber_subclass(self):
781+
self.testFromNumber(DummyFraction)
782+
749783
def test_is_integer(self):
750784
self.assertTrue(F(1, 1).is_integer())
751785
self.assertTrue(F(-1, 1).is_integer())

0 commit comments

Comments
 (0)