Skip to content

bpo-38302. __rpow__ now called when __ipow__ returns NotImplemented #16459

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 9 commits into from
Feb 26, 2021

Conversation

ashkop
Copy link
Contributor

@ashkop ashkop commented Sep 28, 2019

Basically copied the behavior of binary_iop for the ternary operator. Also made sure that op_name is used in error message.

https://bugs.python.org/issue38302

Copy link
Member

@brandtbucher brandtbucher left a comment

Choose a reason for hiding this comment

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

Thanks, this looks good! Just noticed a couple of small things:

@@ -1216,7 +1235,7 @@ PyNumber_InPlacePower(PyObject *v, PyObject *w, PyObject *z)
{
if (v->ob_type->tp_as_number &&
v->ob_type->tp_as_number->nb_inplace_power != NULL) {
return ternary_op(v, w, z, NB_SLOT(nb_inplace_power), "**=");
return ternary_iop(v, w, z, NB_SLOT(nb_inplace_power), NB_SLOT(nb_power), "**=");
Copy link
Member

@brandtbucher brandtbucher Sep 28, 2019

Choose a reason for hiding this comment

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

PyNumber_InPlacePower can now just mirror the other PyNumber_InPlace* functions, right?

So this is the only line that's needed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, thank you.

@@ -3900,6 +3900,18 @@ def __ipow__(self, other):
a = C()
a **= 2

def test_ipow_returns_not_implemented(self):
class A:
def __ipow__(self, other):
Copy link
Member

Choose a reason for hiding this comment

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

It looks like we're also lacking a test for when A doesn't define __ipow__. Do you mind adding one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, added the test

Copy link
Member

Choose a reason for hiding this comment

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

Could you add a test for the two fallbacks and ensure the same class is returning NotImplemented and implementing the regular power methods i.e something like:

class A:
    def __ipow__(self, other):
        return NotImplemented

class B(A):
    def __pow__(self, other):
        return 1
class C(A):
    def __rpow__(self, other):
        return 2

The linked bug is only exhibited when a class has an ipow slot but when called it returns NotImplemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understood correctly what you meant, but I updated the test to also test the __pow__ fallback.

Copy link
Member

Choose a reason for hiding this comment

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

Thank you, what you have now looks good. What I meant was that to catch the original bug/properly be a regression test, the class has to look like:

class A:
    def __ipow__(self, _):
        return NotImplemented
    def __pow__(self, other):
        ...

if the class looks like

class A:
    def __pow__(self, other):
        ...

then the old logic returns a false here and correctly goes to the non-inplace version of power anyway:

cpython/Objects/abstract.c

Lines 1162 to 1163 in 8e19c8b

if (Py_TYPE(v)->tp_as_number &&
Py_TYPE(v)->tp_as_number->nb_inplace_power != NULL) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, you are right. Thanks!

@brandtbucher brandtbucher added the type-bug An unexpected behavior, bug, or error label Sep 28, 2019
@hongweipeng

This comment has been minimized.

Copy link
Member

@brandtbucher brandtbucher left a comment

Choose a reason for hiding this comment

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

@ashkop, one more small thing in the new test. Then it looks good!

def __rpow__(self, other):
return 1

a = A()
Copy link
Member

Choose a reason for hiding this comment

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

This can just be:

Suggested change
a = A()
a = object()

Then you don't need the class definition above!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

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

LGTM. 👍

@@ -3900,6 +3900,18 @@ def __ipow__(self, other):
a = C()
a **= 2

def test_ipow_returns_not_implemented(self):
class A:
def __ipow__(self, other):
Copy link
Member

Choose a reason for hiding this comment

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

Could you add a test for the two fallbacks and ensure the same class is returning NotImplemented and implementing the regular power methods i.e something like:

class A:
    def __ipow__(self, other):
        return NotImplemented

class B(A):
    def __pow__(self, other):
        return 1
class C(A):
    def __rpow__(self, other):
        return 2

The linked bug is only exhibited when a class has an ipow slot but when called it returns NotImplemented.

@@ -0,0 +1 @@
If :func:`object.__ipow__` returns :const:`NotImplemented` :func:`object.__rpow__` will be called now as expected.
Copy link
Member

@ammaraskar ammaraskar Aug 24, 2020

Choose a reason for hiding this comment

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

Just fyi, @brettcannon posted on the original bug pointing out that this would not call __pow__ either. I'd recommend changing this to

If :func:`object.__ipow__` returns :const:`NotImplemented`, the operator will correctly
fall back to :func:`object.__pow__` and :func:`object.__rpow__` as expected.

It might also be worth mentioning this in whatsnew, even though it's obscure it is potentially a change in behavior that people should be informed about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the news entry, and also added a line to What's New

@@ -965,8 +965,9 @@ ternary_op(PyObject *v,
if (z == Py_None)
PyErr_Format(
PyExc_TypeError,
"unsupported operand type(s) for ** or pow(): "
"unsupported operand type(s) for %.100s: "
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for fixing this! Really weird that op_name went unused so long. Would you mind adding a test for this like:

        x = None
        with self.assertRaises(TypeError) as cm:
            x **= 2
        self.assertIn('unsupported operand type(s) for **=', str(cm.exception))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, added a new test

@ashkop ashkop force-pushed the bpo-38302-ipow-fix branch from a2476e7 to 25f95b5 Compare August 25, 2020 15:39
@@ -3900,6 +3900,18 @@ def __ipow__(self, other):
a = C()
a **= 2

def test_ipow_returns_not_implemented(self):
class A:
def __ipow__(self, other):
Copy link
Member

Choose a reason for hiding this comment

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

Thank you, what you have now looks good. What I meant was that to catch the original bug/properly be a regression test, the class has to look like:

class A:
    def __ipow__(self, _):
        return NotImplemented
    def __pow__(self, other):
        ...

if the class looks like

class A:
    def __pow__(self, other):
        ...

then the old logic returns a false here and correctly goes to the non-inplace version of power anyway:

cpython/Objects/abstract.c

Lines 1162 to 1163 in 8e19c8b

if (Py_TYPE(v)->tp_as_number &&
Py_TYPE(v)->tp_as_number->nb_inplace_power != NULL) {

@ashkop
Copy link
Contributor Author

ashkop commented Feb 22, 2021

Merged upstream, added Py_TEST usage, removed TERNARY_OP definition since op_name is now used in the error message.

@brettcannon brettcannon self-requested a review February 23, 2021 01:15
@brettcannon
Copy link
Member

I also tested this PR against my own test suite that uncovered this issue and it passed!

@brettcannon brettcannon merged commit cc02b4f into python:master Feb 26, 2021
@bedevere-bot
Copy link

@brettcannon: Please replace # with GH- in the commit message next time. Thanks!

@brettcannon
Copy link
Member

@ashkop thanks for the PR!

adorilson pushed a commit to adorilson/cpython that referenced this pull request Mar 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants