https://github.com/python/cpython/commit/c8d2630995fc234f8276e35643a4a43e62224510
commit: c8d2630995fc234f8276e35643a4a43e62224510
branch: main
author: Serhiy Storchaka <storch...@gmail.com>
committer: serhiy-storchaka <storch...@gmail.com>
date: 2024-07-19T08:06:53+03:00
summary:

gh-82017: Support as_integer_ratio() in the Fraction constructor (GH-120271)

Any objects that have the as_integer_ratio() method (e.g. numpy.float128)
can now be converted to a fraction.

files:
A Misc/NEWS.d/next/Library/2024-06-08-17-41-11.gh-issue-82017.WpSTGi.rst
M Doc/library/fractions.rst
M Doc/whatsnew/3.14.rst
M Lib/fractions.py
M Lib/test/test_fractions.py

diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst
index 552d6030b1ceda..410b176c5d5084 100644
--- a/Doc/library/fractions.rst
+++ b/Doc/library/fractions.rst
@@ -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.
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 8f7b6ebd0af316..777faafe59b4f5 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -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
 --
 
diff --git a/Lib/fractions.py b/Lib/fractions.py
index 565503911bbe97..34fd0803d1b1ab 100644
--- a/Lib/fractions.py
+++ b/Lib/fractions.py
@@ -3,7 +3,6 @@
 
 """Fraction, infinite-precision, rational numbers."""
 
-from decimal import Decimal
 import functools
 import math
 import numbers
@@ -244,7 +243,9 @@ def __new__(cls, numerator=0, denominator=None):
                 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 @@ def __new__(cls, numerator=0, denominator=None):
                     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
diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py
index 589669298e22e2..12c42126301265 100644
--- a/Lib/test/test_fractions.py
+++ b/Lib/test/test_fractions.py
@@ -354,6 +354,41 @@ def testInitFromDecimal(self):
         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
index 00000000000000..7decee7ff3384e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-08-17-41-11.gh-issue-82017.WpSTGi.rst
@@ -0,0 +1,2 @@
+Added support for converting any objects that have the
+:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to