Skip to content

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

Closed
david-caro wants to merge 1 commit into
python:masterfrom
david-caro:fix-issue-41295
Closed

bpo-41295: Allow overriding __setattr__ on multiple inheritance#21473
david-caro wants to merge 1 commit into
python:masterfrom
david-caro:fix-issue-41295

Conversation

@david-caro

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

Copy link
Copy Markdown

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

Comment thread Objects/typeobject.c Outdated
Comment thread Objects/typeobject.c Outdated
@david-caro

Copy link
Copy Markdown
Author

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

@tiran tiran left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown

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
Copy Markdown
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
Copy Markdown
Author

I have made the requested changes; please review again

@bedevere-bot

Copy link
Copy Markdown

Thanks for making the requested changes!

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

Comment thread Lib/test/test_descr.py Outdated
Comment thread Lib/test/test_descr.py Outdated
@bedevere-bot

Copy link
Copy Markdown

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
Copy Markdown
Author

Awesome, thanks a lot for the quick review :)

I have made the requested changes; please review again

@bedevere-bot

Copy link
Copy Markdown

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

tiran commented Jul 14, 2020

Copy link
Copy Markdown
Member

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

@scoder

scoder commented Jul 14, 2020

Copy link
Copy Markdown
Contributor

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

david-caro commented Jul 15, 2020

Copy link
Copy Markdown
Author

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

@gvanrossum gvanrossum left a comment

Copy link
Copy Markdown
Member

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!!"

Comment thread Lib/test/test_descr.py Outdated

Copy link
Copy Markdown
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).

Comment thread Objects/typeobject.c Outdated

Copy link
Copy Markdown
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
Copy Markdown

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

gvanrossum commented Jul 16, 2020

Copy link
Copy Markdown
Member

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 <david@dcaro.es>
@david-caro

Copy link
Copy Markdown
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
Copy Markdown
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.

Comment thread Objects/typeobject.c
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
Copy Markdown
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

scoder commented Jul 18, 2020

Copy link
Copy Markdown
Contributor

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

@gvanrossum

Copy link
Copy Markdown
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