Skip to content

gh-130104: Call __rpow__ in ternary pow() if necessary #130251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you should use object.__rpow__(self, other, modulo=None) signature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand you. Were and how can it be used?

This sentence is a copy of the corresponding sentence for __pow__.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant L3329-3342 above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. There are other similar cases (for example __round__), so I will left this for other issue. Actually, __pow__() and __round__() are never called with None, so it is not necessary that they support None.

argument if the three-argument 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

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::

Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,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`.)

* 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`.)

Comment on lines +461 to +465
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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`.)
* 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`.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference?

* Add a built-in implementation for HMAC (:rfc:`2104`) using formally verified
code from the `HACL* <https://github.com/hacl-star/hacl-star/>`__ project.
This implementation is used as a fallback when the OpenSSL implementation
Expand Down
4 changes: 2 additions & 2 deletions Lib/_pydecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
4 changes: 3 additions & 1 deletion Lib/fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions Lib/test/test_capi/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,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):
Expand Down
4 changes: 1 addition & 3 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4493,12 +4493,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))
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__':
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
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.
43 changes: 38 additions & 5 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -9992,13 +9992,46 @@ slot_nb_power(PyObject *self, PyObject *other, PyObject *modulus)
{
if (modulus == Py_None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's insert some PEP-7 here 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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PyObject* stack[3];
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;
Comment on lines +9999 to +10001
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use a lot of Py_TYPE(other), so I think we can have a variable holding it just for readability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is a copy of SLOT1BINFULL. I do not want to introduce more difference than necessary.

We can make SLOT1BINFULL supporting the third argument, but I am not sure that it is worth.

Comment on lines +9999 to +10001
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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;
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;
Comment on lines +10015 to +10016
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (r != Py_NotImplemented)
return r;
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;
Comment on lines +10025 to +10027
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (r != Py_NotImplemented ||
Py_IS_TYPE(other, Py_TYPE(self)))
return r;
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;
}
Expand Down
Loading