Skip to content
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

bpo-41295: Allow overriding __setattr__ on multiple inheritance #21473

Closed
wants to merge 1 commit into from

Conversation

david-caro
Copy link

@david-caro david-caro commented Jul 14, 2020

The change introduced in bpo-39960, broke being able to override the
__setattr__ function unless it's done by the next class in the MRO.

For example:

class Meta(type):
    def __setattr__(cls, key, value):
        type.__setattr__(cls, key, value)

class OtherClass:
    pass

class DefaultMeta(OtherClass, Meta):
    pass

obj = DefaultMeta('A', (object,), {})
obj.test = True
print(obj.test)

Would break as it would first check if OtherClass has a custom
__setattr__ and if not would raise error right away.

This change goes through the whole MRO to find out if there's any class
that overrides it before throwing error.

https://bugs.python.org/issue41295

Objects/typeobject.c Outdated Show resolved Hide resolved
Objects/typeobject.c Outdated Show resolved Hide resolved
@david-caro
Copy link
Author

I would appreciate some guidance on where to put tests for this :)

Copy link
Member

@tiran tiran left a comment

Choose a reason for hiding this comment

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

Could you please add your regression as test case, too?

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@david-caro
Copy link
Author

Could you please add your regression as test case, too?

I added the test to test_desrc.py, I'm not sure if that's where it should go, let me know, thanks for the reviews! :)

@david-caro david-caro requested a review from tiran July 14, 2020 17:29
@david-caro
Copy link
Author

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@tiran: please review the changes made to this pull request.

Lib/test/test_descr.py Outdated Show resolved Hide resolved
Lib/test/test_descr.py Outdated Show resolved Hide resolved
@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@david-caro
Copy link
Author

Awesome, thanks a lot for the quick review :)

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@tiran: please review the changes made to this pull request.

@bedevere-bot bedevere-bot requested a review from tiran July 14, 2020 17:39
@tiran tiran requested a review from scoder July 14, 2020 18:13
@tiran
Copy link
Member

tiran commented Jul 14, 2020

Thanks! I'm leaving the final review to @scoder .

@scoder
Copy link
Contributor

scoder commented Jul 14, 2020

I'll have to take a deeper look at this, just a quick comment: should_break is not a good name because it does not explain what it means if it's true of false. I would like to see a better explanation in the variable name and preferably also in comments of why this algorithm is correct. It's not clear to me from the first look.

@david-caro
Copy link
Author

david-caro commented Jul 15, 2020

I started looking a bit more closely into this (I'm kinda new to the codebase :) ), fixed a bit the variable name, and added comments there.
The comments are assumptions though, I'm not 100% sure that that's the case, I've tested it with a bunch of different python codes to try to verify those assumptions, but the lack of proof is not proof by itself ;)
Please let me know what I got wrong, and/or what I should change to improve it.
Thanks!

For reference, I'm trying to do what's said in the email thread: https://mail.python.org/pipermail/python-dev/2003-April/034605.html

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

I don't believe this is right yet. Here's another test case (which should raise TypeError on obj.test = False):

class A(type):
    def __setattr__(cls, key, value):
        object.__setattr__(cls, key, value)

class B:  # **UPDATE**: I mistakenly has B(A) here
    pass

class C(B, A):
    pass

obj = C('D', (object,), {})
try:
    obj.test = False
except Exception as err:
    print("Good, it raised", repr(err))
else:
    assert False, "Didn't raise!!"

@@ -1458,6 +1458,23 @@ class someclass(metaclass=dynamicmetaclass):
pass
self.assertNotEqual(someclass, object)

def test_attribute_assignment_bpo41295(self):
Copy link
Member

Choose a reason for hiding this comment

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

I would move the test after test_carloverre (see line 4296).

for (i = 0; i < n; i++) {
PyTypeObject *base = (PyTypeObject*) PyTuple_GET_ITEM(mro, i);
if (base->tp_setattro == func) {
/* 'func' is the earliest non-Python implementation in the MRO. */
/* found a base class who's setattro matches the function being
Copy link
Member

Choose a reason for hiding this comment

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

"whose"

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@gvanrossum
Copy link
Member

gvanrossum commented Jul 16, 2020

Sorry, I had a typo in my example; it should be class B: instead of class B(A):. I've fixed this above and added a comment drawing attention to my mistake. But my point stands: I think that example (so modified) should raise.

The way I understand the example, just because B.__setattr__ is the same as object.__setattr__ doesn't mean that (UPDATE: clarified) you can call it directly -- in C's MRO, type.__setattr__ overrides object.__setattr__.

The change introduced in bpo-39960, broke being able to override the
__setattr__ function unless it's done by the next class in the MRO.

For example:
```
class Meta(type):
    def __setattr__(cls, key, value):
        type.__setattr__(cls, key, value)

class OtherClass:
    pass

class DefaultMeta(OtherClass, Meta):
    pass

obj = DefaultMeta('A', (object,), {})
obj.test = True
print(obj.test)
```

Would break as it would first check if `OtherClass` has a custom
`__setattr__` and if not would raise error right away.

This change goes through the whole MRO, until it finds `type` or `object`,
and if only if no class owns the method being called it will throw an
error.

Signed-off-by: David Caro <[email protected]>
@david-caro
Copy link
Author

Sorry, I had a typo in my example; it should be class B: instead of class B(A):. I've fixed this above and added a comment drawing attention to my mistake. But my point stands: I think that example (so modified) should raise.

The way I understand the example, just because B.__setattr__ is the same as object.__setattr__ doesn't mean that (UPDATE: clarified) you can call it directly -- in C's MRO, type.__setattr__ overrides object.__setattr__.

I've gotten the tests to pass if I look explicitly for PyType_Type and PyBaseObject_Type, but the some tests fail, I think because I should be checking in a more generic way (not only for type and object), I'll try to look into it this weekend, but if you know from the top of your head check for those to I'll really appreciate the (extra) tip. Thanks!

@gvanrossum
Copy link
Member

PS. Please don't use git push -f. Just make a new local commit and push that. We'll squash commits when we land, but during code review the squashing is quite unhelpful -- reviewers need to be able to quickly see what changed since their last review.

for (i = 0; i < n; i++) {
PyTypeObject *base = (PyTypeObject*) PyTuple_GET_ITEM(mro, i);
if (base->tp_setattro == func) {
/* 'func' is the earliest non-Python implementation in the MRO. */
if (base == &PyType_Type || base == &PyBaseObject_Type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely not right. Special-casing types here (any type, really) will not help the general case.

@scoder
Copy link
Contributor

scoder commented Jul 18, 2020

Let's see if the algorithm in #21528 works in all cases.

@gvanrossum
Copy link
Member

@david-caro I'm sorry, but the fix by @scoder in GH-21528 seems to be more accurate, so I'm closing this PR. Thanks for your efforts -- they helped us to find the right fix. Better luck with your next PR!

@gvanrossum gvanrossum closed this Jul 18, 2020
@david-caro david-caro deleted the fix-issue-41295 branch July 27, 2020 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants