From 673028822fcdbc5aeffc8d671a22645667e47aff Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Fri, 20 Aug 2021 10:56:22 +0100 Subject: [PATCH 1/6] bpo-44547: Implement Fractions.__int__ --- Lib/fractions.py | 4 +++- Lib/test/test_fractions.py | 6 ++++++ .../next/Library/2021-08-20-10-52-40.bpo-44547.eu0iJq.rst | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2021-08-20-10-52-40.bpo-44547.eu0iJq.rst diff --git a/Lib/fractions.py b/Lib/fractions.py index 180cd94c2879cc..b7cd5ce99879e7 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -595,12 +595,14 @@ def __abs__(a): return Fraction(abs(a._numerator), a._denominator, _normalize=False) def __trunc__(a): - """trunc(a)""" + """math.trunc(a)""" if a._numerator < 0: return -(-a._numerator // a._denominator) else: return a._numerator // a._denominator + __int__ = __trunc__ + def __floor__(a): """math.floor(a)""" return a.numerator // a.denominator diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index bbf7709fe959be..a65c6457e1e948 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -8,6 +8,7 @@ import fractions import functools import sys +import typing import unittest from copy import copy, deepcopy import pickle @@ -385,6 +386,11 @@ def testConversions(self): self.assertTypedEquals(0.1+0j, complex(F(1,10))) + def testSupportsInt(self): + # See bpo-44547. + f = F(3, 2) + self.assertIsInstance(f, typing.SupportsInt) + def testBoolGuarateesBoolReturn(self): # Ensure that __bool__ is used on numerator which guarantees a bool # return. See also bpo-39274. diff --git a/Misc/NEWS.d/next/Library/2021-08-20-10-52-40.bpo-44547.eu0iJq.rst b/Misc/NEWS.d/next/Library/2021-08-20-10-52-40.bpo-44547.eu0iJq.rst new file mode 100644 index 00000000000000..a5f425e17cad23 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-08-20-10-52-40.bpo-44547.eu0iJq.rst @@ -0,0 +1,2 @@ +Implement ``Fraction.__int__``, so that a :class:`fractions.Fraction` +instance ``f`` passes an ``isinstance(f, typing.SupportsInt)`` check. From bfa14a8a6a5fa75d3a3ff0de9af70b36ac287c94 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sat, 21 Aug 2021 11:57:05 +0100 Subject: [PATCH 2/6] Test custom integer types --- Lib/test/test_fractions.py | 108 +++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index a65c6457e1e948..8c4d24f51df79d 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -84,6 +84,103 @@ class DummyFraction(fractions.Fraction): """Dummy Fraction subclass for copy and deepcopy testing.""" +class CustomInteger(numbers.Integral): + """int-like class that doesn't inherit from int""" + + def __new__(cls, value): + if not isinstance(value, int): + raise ValueError("value should be an 'int' instance") + self = object.__new__(CustomInteger) + # Ensure we have something of exact type 'int' internally. + self._value = int(value) + return self + + def __int__(self): + return self._value + + def __index__(self): + return self._value + + @property + def numerator(self): + return self + + @property + def denominator(self): + return CustomInteger(1) + + def _operator_wrappers(operator, wrap_return=True): + """ + Helper for defining binary operations. + """ + def forward(a, b): + if isinstance(b, CustomInteger): + b = b._value + elif not isinstance(b, int): + return NotImplemented + result = operator(a._value, b) + return CustomInteger(result) if wrap_return else result + + def reverse(a, b): + if isinstance(b, CustomInteger): + b = b._value + elif not isinstance(b, int): + return NotImplemented + result = operator(b, a._value) + return CustomInteger(result) if wrap_return else result + + return forward, reverse + + # Arithmetic operations + __add__, __radd__ = _operator_wrappers(operator.add) + __sub__, __rsub__ = _operator_wrappers(operator.sub) + __mul__, __rmul__ = _operator_wrappers(operator.mul) + __floordiv__, __rfloordiv__ = _operator_wrappers(operator.floordiv) + __mod__, __rmod__ = _operator_wrappers(operator.mod) + __or__, __ror__ = _operator_wrappers(operator.or_) + __and__, __rand__ = _operator_wrappers(operator.and_) + __xor__, __rxor__ = _operator_wrappers(operator.xor) + __lshift__, __rlshift__ = _operator_wrappers(operator.lshift) + __rshift__, __rrshift__ = _operator_wrappers(operator.rshift) + __truediv__, __rtruediv__ = _operator_wrappers( + operator.truediv, wrap_return=False) + + # Comparisons + __lt__, __gt__ = _operator_wrappers(operator.lt, wrap_return=False) + __le__, __ge__ = _operator_wrappers(operator.le, wrap_return=False) + __eq__, _unused = _operator_wrappers(operator.eq, wrap_return=False) + + # Unary operations + def __abs__(self): return CustomInteger(abs(self._value)) + def __neg__(self): return CustomInteger(-self._value) + def __pos__(self): return CustomInteger(+self._value) + def __invert__(self): return CustomInteger(~self._value) + def __bool__(self): return bool(self._value) + + # These should all return an int rather than a CustomInteger. + __floor__ = __ceil__ = __trunc__ = __int__ + + def __round__(self, ndigits=None): + if ndigits is None: # return int if no second arg + return round(self._value) + else: # return same type if second arg + return CustomInteger(round(self._value, ndigits)) + + # pow is messy. We don't attempt to do anything with 3-argument pow. + # 2-argument pow for ints has a range of different return types; we only + # want to re-wrap if the returned value is an int. + _pow , _rpow = _operator_wrappers(operator.pow, wrap_return=False) + + def __pow__(self, other): + result = self._pow(other) + return CustomInteger(result) if isinstance(result, int) else result + + def __rpow__(self, other): + result = self._rpow(other) + return CustomInteger(result) if isinstance(result, int) else result + + + def _components(r): return (r.numerator, r.denominator) @@ -390,6 +487,17 @@ def testSupportsInt(self): # See bpo-44547. f = F(3, 2) self.assertIsInstance(f, typing.SupportsInt) + self.assertEqual(int(f), 1) + self.assertEqual(type(int(f)), int) + + def testIntGuaranteesIntReturn(self): + # Check that int(some_fraction) gives a result of exact type `int` + # even if the fraction is using some other Integral type for its + # numerator and denominator. + f = F(CustomInteger(13), CustomInteger(5)) + self.assertIsInstance(f, typing.SupportsInt) + self.assertEqual(int(f), 2) + self.assertEqual(type(int(f)), int) def testBoolGuarateesBoolReturn(self): # Ensure that __bool__ is used on numerator which guarantees a bool From 23e8389a0ce59eafe9e45e2617eb9f37dbcb6e89 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sun, 22 Aug 2021 09:59:05 +0100 Subject: [PATCH 3/6] Fix __int__ to always return an int --- Lib/fractions.py | 12 +++- Lib/test/test_fractions.py | 124 ++++++++----------------------------- 2 files changed, 36 insertions(+), 100 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index b7cd5ce99879e7..d8cb1c5c79305e 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -594,15 +594,23 @@ def __abs__(a): """abs(a)""" return Fraction(abs(a._numerator), a._denominator, _normalize=False) + def __int__(a): + """int(a)""" + if a._numerator < 0: + return int(-(-a._numerator // a._denominator)) + else: + return int(a._numerator // a._denominator) + def __trunc__(a): """math.trunc(a)""" + # Note: this differs from __int__ - __int__ must return an int, + # while __trunc__ may return a non-int numbers.Integral object + # if that's more convenient or efficient. if a._numerator < 0: return -(-a._numerator // a._denominator) else: return a._numerator // a._denominator - __int__ = __trunc__ - def __floor__(a): """math.floor(a)""" return a.numerator // a.denominator diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 8c4d24f51df79d..fc46e8674fc46e 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -84,103 +84,6 @@ class DummyFraction(fractions.Fraction): """Dummy Fraction subclass for copy and deepcopy testing.""" -class CustomInteger(numbers.Integral): - """int-like class that doesn't inherit from int""" - - def __new__(cls, value): - if not isinstance(value, int): - raise ValueError("value should be an 'int' instance") - self = object.__new__(CustomInteger) - # Ensure we have something of exact type 'int' internally. - self._value = int(value) - return self - - def __int__(self): - return self._value - - def __index__(self): - return self._value - - @property - def numerator(self): - return self - - @property - def denominator(self): - return CustomInteger(1) - - def _operator_wrappers(operator, wrap_return=True): - """ - Helper for defining binary operations. - """ - def forward(a, b): - if isinstance(b, CustomInteger): - b = b._value - elif not isinstance(b, int): - return NotImplemented - result = operator(a._value, b) - return CustomInteger(result) if wrap_return else result - - def reverse(a, b): - if isinstance(b, CustomInteger): - b = b._value - elif not isinstance(b, int): - return NotImplemented - result = operator(b, a._value) - return CustomInteger(result) if wrap_return else result - - return forward, reverse - - # Arithmetic operations - __add__, __radd__ = _operator_wrappers(operator.add) - __sub__, __rsub__ = _operator_wrappers(operator.sub) - __mul__, __rmul__ = _operator_wrappers(operator.mul) - __floordiv__, __rfloordiv__ = _operator_wrappers(operator.floordiv) - __mod__, __rmod__ = _operator_wrappers(operator.mod) - __or__, __ror__ = _operator_wrappers(operator.or_) - __and__, __rand__ = _operator_wrappers(operator.and_) - __xor__, __rxor__ = _operator_wrappers(operator.xor) - __lshift__, __rlshift__ = _operator_wrappers(operator.lshift) - __rshift__, __rrshift__ = _operator_wrappers(operator.rshift) - __truediv__, __rtruediv__ = _operator_wrappers( - operator.truediv, wrap_return=False) - - # Comparisons - __lt__, __gt__ = _operator_wrappers(operator.lt, wrap_return=False) - __le__, __ge__ = _operator_wrappers(operator.le, wrap_return=False) - __eq__, _unused = _operator_wrappers(operator.eq, wrap_return=False) - - # Unary operations - def __abs__(self): return CustomInteger(abs(self._value)) - def __neg__(self): return CustomInteger(-self._value) - def __pos__(self): return CustomInteger(+self._value) - def __invert__(self): return CustomInteger(~self._value) - def __bool__(self): return bool(self._value) - - # These should all return an int rather than a CustomInteger. - __floor__ = __ceil__ = __trunc__ = __int__ - - def __round__(self, ndigits=None): - if ndigits is None: # return int if no second arg - return round(self._value) - else: # return same type if second arg - return CustomInteger(round(self._value, ndigits)) - - # pow is messy. We don't attempt to do anything with 3-argument pow. - # 2-argument pow for ints has a range of different return types; we only - # want to re-wrap if the returned value is an int. - _pow , _rpow = _operator_wrappers(operator.pow, wrap_return=False) - - def __pow__(self, other): - result = self._pow(other) - return CustomInteger(result) if isinstance(result, int) else result - - def __rpow__(self, other): - result = self._rpow(other) - return CustomInteger(result) if isinstance(result, int) else result - - - def _components(r): return (r.numerator, r.denominator) @@ -494,7 +397,32 @@ def testIntGuaranteesIntReturn(self): # Check that int(some_fraction) gives a result of exact type `int` # even if the fraction is using some other Integral type for its # numerator and denominator. - f = F(CustomInteger(13), CustomInteger(5)) + + class CustomInt(int): + """ + Subclass of int with just enough machinery to convince the Fraction + constructor to produce something with CustomInt numerator and + denominator. + """ + + @property + def numerator(self): + return self + + @property + def denominator(self): + return CustomInt(1) + + def __mul__(self, other): + return CustomInt(int(self) * int(other)) + + def __floordiv__(self, other): + return CustomInt(int(self) // int(other)) + + f = F(CustomInt(13), CustomInt(5)) + + self.assertIsInstance(f.numerator, CustomInt) + self.assertIsInstance(f.denominator, CustomInt) self.assertIsInstance(f, typing.SupportsInt) self.assertEqual(int(f), 2) self.assertEqual(type(int(f)), int) From 5cc96f782c9c7c2d16e0ed69ca4e32aa1bf7b995 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sun, 22 Aug 2021 10:40:48 +0100 Subject: [PATCH 4/6] Update to use operator.index instead of int for the final conversion --- Lib/fractions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index d8cb1c5c79305e..0fca3dd15745f0 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -594,12 +594,12 @@ def __abs__(a): """abs(a)""" return Fraction(abs(a._numerator), a._denominator, _normalize=False) - def __int__(a): + def __int__(a, _index=operator.index): """int(a)""" if a._numerator < 0: - return int(-(-a._numerator // a._denominator)) + return _index(-(-a._numerator // a._denominator)) else: - return int(a._numerator // a._denominator) + return _index(a._numerator // a._denominator) def __trunc__(a): """math.trunc(a)""" From 12be4a9c23d21a3b97b47d788347218a444b4985 Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sun, 22 Aug 2021 10:42:43 +0100 Subject: [PATCH 5/6] Remove misleading comment --- Lib/fractions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index 0fca3dd15745f0..f9ac882ec002fa 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -603,9 +603,6 @@ def __int__(a, _index=operator.index): def __trunc__(a): """math.trunc(a)""" - # Note: this differs from __int__ - __int__ must return an int, - # while __trunc__ may return a non-int numbers.Integral object - # if that's more convenient or efficient. if a._numerator < 0: return -(-a._numerator // a._denominator) else: From e568382e0a2dc45eb7d0ff50572883d8e2d77fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Thu, 21 Oct 2021 23:17:32 +0200 Subject: [PATCH 6/6] Document the change --- Doc/library/fractions.rst | 4 ++++ Doc/whatsnew/3.11.rst | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index d04de8f8e95a61..c893f2d5389d57 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -94,6 +94,10 @@ another rational number, or from a string. Underscores are now permitted when creating a :class:`Fraction` instance from a string, following :PEP:`515` rules. + .. versionchanged:: 3.11 + :class:`Fraction` implements ``__int__`` now to satisfy + ``typing.SupportsInt`` instance checks. + .. attribute:: numerator Numerator of the Fraction in lowest term. diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 49b4364be9bd7f..c99adf3ea4d770 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -181,8 +181,12 @@ Improved Modules fractions --------- -Support :PEP:`515`-style initialization of :class:`~fractions.Fraction` from -string. (Contributed by Sergey B Kirpichev in :issue:`44258`.) +* Support :PEP:`515`-style initialization of :class:`~fractions.Fraction` from + string. (Contributed by Sergey B Kirpichev in :issue:`44258`.) + +* :class:`~fractions.Fraction` now implements an ``__int__`` method, so + that an ``isinstance(some_fraction, typing.SupportsInt)`` check passes. + (Contributed by Mark Dickinson in :issue:`44547`.) math