From 85192cb94a9211a524e469c4494912433f463a8a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Feb 2025 11:50:35 +0200 Subject: [PATCH 1/3] gh-130104: Call __rpow__ in ternary pow() if necessary Previously it was only called in binary pow() and the binary power operator. --- Doc/reference/datamodel.rst | 11 +++-- Doc/whatsnew/3.14.rst | 5 +++ Lib/_pydecimal.py | 4 +- Lib/fractions.py | 4 +- Lib/test/test_capi/test_number.py | 5 +-- Lib/test/test_decimal.py | 4 +- Lib/test/test_descr.py | 4 ++ Lib/test/test_fractions.py | 6 +++ ...-02-18-11-42-58.gh-issue-130104.BOicVZ.rst | 4 ++ Objects/typeobject.c | 45 ++++++++++++++++--- 10 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 66b836eaf0008a..6ef8932f0c8c3f 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -3356,10 +3356,15 @@ left undefined. is called if ``type(x).__sub__(x, y)`` returns :data:`NotImplemented` or ``type(y)`` is a subclass of ``type(x)``. [#]_ - .. index:: pair: built-in function; pow + Note that :meth:`__rpow__` should be defined to accept an optional third + argument if the ternary version of the built-in :func:`pow` function + is to be supported. - Note that ternary :func:`pow` will not try calling :meth:`__rpow__` (the - coercion rules would become too complicated). + .. versionchanged:: next + + Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. + Previously it was only called in binary :func:`!pow` and the binary + power operator. .. note:: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index ac0ae8cf0133e6..9802f9072182f7 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -307,6 +307,11 @@ Other language changes The testbed can also be used to run the test suite of projects other than CPython itself. (Contributed by Russell Keith-Magee in :gh:`127592`.) +* Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. + Previously it was only called in binary :func:`!pow` and the binary + power operator. + (Contributed by Serhiy Storchaka in :gh:`130104`.) + New modules =========== diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index ec036199331396..66fc06b95a5cf5 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -2440,12 +2440,12 @@ def __pow__(self, other, modulo=None, context=None): return ans - def __rpow__(self, other, context=None): + def __rpow__(self, other, modulo=None, context=None): """Swaps self/other and returns __pow__.""" other = _convert_other(other) if other is NotImplemented: return other - return other.__pow__(self, context=context) + return other.__pow__(self, modulo, context=context) def normalize(self, context=None): """Normalize- strip trailing 0s, change anything equal to 0 to 0e0""" diff --git a/Lib/fractions.py b/Lib/fractions.py index f0cbc8c2e6c012..fa722589fb4f67 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -905,8 +905,10 @@ def __pow__(a, b, modulo=None): else: return NotImplemented - def __rpow__(b, a): + def __rpow__(b, a, modulo=None): """a ** b""" + if modulo is not None: + return NotImplemented if b._denominator == 1 and b._numerator >= 0: # If a is an int, keep it that way if possible. return a ** b._numerator diff --git a/Lib/test/test_capi/test_number.py b/Lib/test/test_capi/test_number.py index b915dee37c7ca6..9a0b6649f89889 100644 --- a/Lib/test/test_capi/test_number.py +++ b/Lib/test/test_capi/test_number.py @@ -238,9 +238,8 @@ def __rpow__(*args): x = X() self.assertEqual(power(4, x), (x, 4)) self.assertEqual(inplacepower(4, x), (x, 4)) - # XXX: Three-arg power doesn't use __rpow__. - self.assertRaises(TypeError, power, 4, x, 5) - self.assertRaises(TypeError, inplacepower, 4, x, 5) + self.assertEqual(power(4, x, 5), (x, 4, 5)) + self.assertEqual(inplacepower(4, x, 5), (x, 4, 5)) class X: def __ipow__(*args): diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 0e15eb3693888d..e8bbe1135cb693 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -4481,12 +4481,10 @@ def test_implicit_context(self): self.assertIs(Decimal("NaN").fma(7, 1).is_nan(), True) # three arg power self.assertEqual(pow(Decimal(10), 2, 7), 2) + self.assertEqual(pow(10, Decimal(2), 7), 2) if self.decimal == C: - self.assertEqual(pow(10, Decimal(2), 7), 2) self.assertEqual(pow(10, 2, Decimal(7)), 2) else: - # XXX: Three-arg power doesn't use __rpow__. - self.assertRaises(TypeError, pow, 10, Decimal(2), 7) # XXX: There is no special method to dispatch on the # third arg of three-arg power. self.assertRaises(TypeError, pow, 10, 2, Decimal(7)) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index f2f3d9469f8bab..348b3676cab7e7 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -3515,6 +3515,10 @@ def __rpow__(self, other, mod=None): self.assertEqual(repr(2 ** I(3)), "I(8)") self.assertEqual(repr(I(2) ** 3), "I(8)") self.assertEqual(repr(pow(I(2), I(3), I(5))), "I(3)") + self.assertEqual(repr(pow(I(2), I(3), 5)), "I(3)") + self.assertEqual(repr(pow(I(2), 3, 5)), "I(3)") + self.assertEqual(repr(pow(2, I(3), 5)), "I(3)") + self.assertEqual(repr(pow(2, 3, I(5))), "3") class S(str): def __eq__(self, other): return self.lower() == other.lower() diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 98dccbec9566ac..84faa63606439e 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -1707,6 +1707,12 @@ def test_three_argument_pow(self): self.assertRaisesMessage(TypeError, message % ("Fraction", "int", "int"), pow, F(3), 4, 5) + self.assertRaisesMessage(TypeError, + message % ("int", "Fraction", "int"), + pow, 3, F(4), 5) + self.assertRaisesMessage(TypeError, + message % ("int", "int", "Fraction"), + pow, 3, 4, F(5)) if __name__ == '__main__': diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst new file mode 100644 index 00000000000000..70b67f678ce4da --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst @@ -0,0 +1,4 @@ +Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if +necessary. +Previously it was only called in binary :func:`!pow` and the binary +power operator. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1fa1220aeec648..17d055e7ce4fa3 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -9628,7 +9628,7 @@ FUNCNAME(PyObject *self, PyObject *other) \ { \ PyObject* stack[2]; \ PyThreadState *tstate = _PyThreadState_GET(); \ - int do_other = !Py_IS_TYPE(self, Py_TYPE(other)) && \ + int do_other = /*(void*)TESTFUNC != (void*)slot_nb_power &&*/ !Py_IS_TYPE(self, Py_TYPE(other)) && \ Py_TYPE(other)->tp_as_number != NULL && \ Py_TYPE(other)->tp_as_number->SLOTNAME == TESTFUNC; \ if (Py_TYPE(self)->tp_as_number != NULL && \ @@ -9813,13 +9813,46 @@ slot_nb_power(PyObject *self, PyObject *other, PyObject *modulus) { if (modulus == Py_None) return slot_nb_power_binary(self, other); - /* Three-arg power doesn't use __rpow__. But ternary_op - can call this when the second argument's type uses - slot_nb_power, so check before calling self.__pow__. */ + + /* The following code is a copy of SLOT1BINFULL, but for three arguments. */ + PyObject* stack[3]; + PyThreadState *tstate = _PyThreadState_GET(); + int do_other = !Py_IS_TYPE(self, Py_TYPE(other)) && + Py_TYPE(other)->tp_as_number != NULL && + Py_TYPE(other)->tp_as_number->nb_power == slot_nb_power; if (Py_TYPE(self)->tp_as_number != NULL && Py_TYPE(self)->tp_as_number->nb_power == slot_nb_power) { - PyObject* stack[3] = {self, other, modulus}; - return vectorcall_method(&_Py_ID(__pow__), stack, 3); + PyObject *r; + if (do_other && PyType_IsSubtype(Py_TYPE(other), Py_TYPE(self))) { + int ok = method_is_overloaded(self, other, &_Py_ID(__rpow__)); + if (ok < 0) { + return NULL; + } + if (ok) { + stack[0] = other; + stack[1] = self; + stack[2] = modulus; + r = vectorcall_maybe(tstate, &_Py_ID(__rpow__), stack, 3); + if (r != Py_NotImplemented) + return r; + Py_DECREF(r); + do_other = 0; + } + } + stack[0] = self; + stack[1] = other; + stack[2] = modulus; + r = vectorcall_maybe(tstate, &_Py_ID(__pow__), stack, 3); + if (r != Py_NotImplemented || + Py_IS_TYPE(other, Py_TYPE(self))) + return r; + Py_DECREF(r); + } + if (do_other) { + stack[0] = other; + stack[1] = self; + stack[2] = modulus; + return vectorcall_maybe(tstate, &_Py_ID(__rpow__), stack, 3); } Py_RETURN_NOTIMPLEMENTED; } From b563d1cdcb55ff3387448ea931357bd52e7d18c3 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Feb 2025 12:45:59 +0200 Subject: [PATCH 2/3] Get rid of remnants of debugging. --- Objects/typeobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 17d055e7ce4fa3..d213afbd694bde 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -9628,7 +9628,7 @@ FUNCNAME(PyObject *self, PyObject *other) \ { \ PyObject* stack[2]; \ PyThreadState *tstate = _PyThreadState_GET(); \ - int do_other = /*(void*)TESTFUNC != (void*)slot_nb_power &&*/ !Py_IS_TYPE(self, Py_TYPE(other)) && \ + int do_other = !Py_IS_TYPE(self, Py_TYPE(other)) && \ Py_TYPE(other)->tp_as_number != NULL && \ Py_TYPE(other)->tp_as_number->SLOTNAME == TESTFUNC; \ if (Py_TYPE(self)->tp_as_number != NULL && \ From 56daac822ab2e1255a2334cc978e46c4f9cd3933 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 20 Feb 2025 12:23:10 +0200 Subject: [PATCH 3/3] "ternary" -> "three-argument" --- Doc/reference/datamodel.rst | 8 ++++---- Doc/whatsnew/3.14.rst | 4 ++-- .../2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 6ef8932f0c8c3f..f1b7d33655c591 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -3319,7 +3319,7 @@ left undefined. :meth:`__divmod__` method should be the equivalent to using :meth:`__floordiv__` and :meth:`__mod__`; it should not be related to :meth:`__truediv__`. Note that :meth:`__pow__` should be defined to accept - an optional third argument if the ternary version of the built-in :func:`pow` + an optional third argument if the three-argument version of the built-in :func:`pow` function is to be supported. If one of those methods does not support the operation with the supplied @@ -3357,13 +3357,13 @@ left undefined. is a subclass of ``type(x)``. [#]_ Note that :meth:`__rpow__` should be defined to accept an optional third - argument if the ternary version of the built-in :func:`pow` function + argument if the three-argument version of the built-in :func:`pow` function is to be supported. .. versionchanged:: next - Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. - Previously it was only called in binary :func:`!pow` and the binary + Three-argument :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. + Previously it was only called in two-argument :func:`!pow` and the binary power operator. .. note:: diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 9802f9072182f7..5fa633adc07c62 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -307,8 +307,8 @@ Other language changes The testbed can also be used to run the test suite of projects other than CPython itself. (Contributed by Russell Keith-Magee in :gh:`127592`.) -* Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. - Previously it was only called in binary :func:`!pow` and the binary +* Three-argument :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. + Previously it was only called in two-argument :func:`!pow` and the binary power operator. (Contributed by Serhiy Storchaka in :gh:`130104`.) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst index 70b67f678ce4da..9539ad3d79d600 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-18-11-42-58.gh-issue-130104.BOicVZ.rst @@ -1,4 +1,4 @@ -Ternary :func:`pow` now try calling :meth:`~object.__rpow__` if +Three-argument :func:`pow` now try calling :meth:`~object.__rpow__` if necessary. -Previously it was only called in binary :func:`!pow` and the binary +Previously it was only called in two-argument :func:`!pow` and the binary power operator.