]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-82017: Support as_integer_ratio() in the Fraction constructor (GH-120271)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 19 Jul 2024 05:06:53 +0000 (08:06 +0300)
committerGitHub <noreply@github.com>
Fri, 19 Jul 2024 05:06:53 +0000 (08:06 +0300)
Any objects that have the as_integer_ratio() method (e.g. numpy.float128)
can now be converted to a fraction.

Doc/library/fractions.rst
Doc/whatsnew/3.14.rst
Lib/fractions.py
Lib/test/test_fractions.py
Misc/NEWS.d/next/Library/2024-06-08-17-41-11.gh-issue-82017.WpSTGi.rst [new file with mode: 0644]

index 552d6030b1ceda14a79b3132252ac9a11e550e9a..410b176c5d50849a5204d08b3926bc9b608bea95 100644 (file)
@@ -17,25 +17,30 @@ The :mod:`fractions` module provides support for rational number arithmetic.
 A Fraction instance can be constructed from a pair of integers, from
 another rational number, or from a string.
 
+.. index:: single: as_integer_ratio()
+
 .. class:: Fraction(numerator=0, denominator=1)
-           Fraction(other_fraction)
-           Fraction(float)
-           Fraction(decimal)
+           Fraction(number)
            Fraction(string)
 
    The first version requires that *numerator* and *denominator* are instances
    of :class:`numbers.Rational` and returns a new :class:`Fraction` instance
    with value ``numerator/denominator``. If *denominator* is ``0``, it
-   raises a :exc:`ZeroDivisionError`. The second version requires that
-   *other_fraction* is an instance of :class:`numbers.Rational` and returns a
-   :class:`Fraction` instance with the same value.  The next two versions accept
-   either a :class:`float` or a :class:`decimal.Decimal` instance, and return a
-   :class:`Fraction` instance with exactly the same value.  Note that due to the
+   raises a :exc:`ZeroDivisionError`.
+
+   The second version requires that *number* is an instance of
+   :class:`numbers.Rational` or has the :meth:`!as_integer_ratio` method
+   (this includes :class:`float` and :class:`decimal.Decimal`).
+   It returns a :class:`Fraction` instance with exactly the same value.
+   Assumed, that the :meth:`!as_integer_ratio` method returns a pair
+   of coprime integers and last one is positive.
+   Note that due to the
    usual issues with binary floating-point (see :ref:`tut-fp-issues`), the
    argument to ``Fraction(1.1)`` is not exactly equal to 11/10, and so
    ``Fraction(1.1)`` does *not* return ``Fraction(11, 10)`` as one might expect.
    (But see the documentation for the :meth:`limit_denominator` method below.)
-   The last version of the constructor expects a string or unicode instance.
+
+   The last version of the constructor expects a string.
    The usual form for this instance is::
 
       [sign] numerator ['/' denominator]
@@ -110,6 +115,10 @@ another rational number, or from a string.
       Formatting of :class:`Fraction` instances without a presentation type
       now supports fill, alignment, sign handling, minimum width and grouping.
 
+   .. versionchanged:: 3.14
+      The :class:`Fraction` constructor now accepts any objects with the
+      :meth:`!as_integer_ratio` method.
+
    .. attribute:: numerator
 
       Numerator of the Fraction in lowest term.
index 8f7b6ebd0af31662c1d8b1f0093204b8f39a65b3..777faafe59b4f5fb501a2661b5739a3c23b440ad 100644 (file)
@@ -100,6 +100,13 @@ ast
 
   (Contributed by Bénédikt Tran in :gh:`121141`.)
 
+fractions
+---------
+
+Added support for converting any objects that have the
+:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
+(Contributed by Serhiy Storchaka in :gh:`82017`.)
+
 os
 --
 
index 565503911bbe97f482282fda4003a95ac6727e84..34fd0803d1b1ab6c3c9c044b735143504e501c61 100644 (file)
@@ -3,7 +3,6 @@
 
 """Fraction, infinite-precision, rational numbers."""
 
-from decimal import Decimal
 import functools
 import math
 import numbers
@@ -244,7 +243,9 @@ class Fraction(numbers.Rational):
                 self._denominator = numerator.denominator
                 return self
 
-            elif isinstance(numerator, (float, Decimal)):
+            elif (isinstance(numerator, float) or
+                  (not isinstance(numerator, type) and
+                   hasattr(numerator, 'as_integer_ratio'))):
                 # Exact conversion
                 self._numerator, self._denominator = numerator.as_integer_ratio()
                 return self
@@ -278,8 +279,7 @@ class Fraction(numbers.Rational):
                     numerator = -numerator
 
             else:
-                raise TypeError("argument should be a string "
-                                "or a Rational instance")
+                raise TypeError("argument should be a string or a number")
 
         elif type(numerator) is int is type(denominator):
             pass # *very* normal case
index 589669298e22e263d9f1cefbf2d3033a1de4c97e..12c42126301265fab5c9af49e2c286effd634786 100644 (file)
@@ -354,6 +354,41 @@ class FractionTest(unittest.TestCase):
         self.assertRaises(OverflowError, F, Decimal('inf'))
         self.assertRaises(OverflowError, F, Decimal('-inf'))
 
+    def testInitFromIntegerRatio(self):
+        class Ratio:
+            def __init__(self, ratio):
+                self._ratio = ratio
+            def as_integer_ratio(self):
+                return self._ratio
+
+        self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
+        errmsg = "argument should be a string or a number"
+        # the type also has an "as_integer_ratio" attribute.
+        self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
+        # bad ratio
+        self.assertRaises(TypeError, F, Ratio(7))
+        self.assertRaises(ValueError, F, Ratio((7,)))
+        self.assertRaises(ValueError, F, Ratio((7, 3, 1)))
+        # only single-argument form
+        self.assertRaises(TypeError, F, Ratio((3, 7)), 11)
+        self.assertRaises(TypeError, F, 2, Ratio((-10, 9)))
+
+        # as_integer_ratio not defined in a class
+        class A:
+            pass
+        a = A()
+        a.as_integer_ratio = lambda: (9, 5)
+        self.assertEqual((9, 5), _components(F(a)))
+
+        # as_integer_ratio defined in a metaclass
+        class M(type):
+            def as_integer_ratio(self):
+                return (11, 9)
+        class B(metaclass=M):
+            pass
+        self.assertRaisesRegex(TypeError, errmsg, F, B)
+        self.assertRaisesRegex(TypeError, errmsg, F, B())
+
     def testFromString(self):
         self.assertEqual((5, 1), _components(F("5")))
         self.assertEqual((3, 2), _components(F("3/2")))
diff --git a/Misc/NEWS.d/next/Library/2024-06-08-17-41-11.gh-issue-82017.WpSTGi.rst b/Misc/NEWS.d/next/Library/2024-06-08-17-41-11.gh-issue-82017.WpSTGi.rst
new file mode 100644 (file)
index 0000000..7decee7
--- /dev/null
@@ -0,0 +1,2 @@
+Added support for converting any objects that have the
+:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.