Don't reset custom __setattr__ in slotted classes#681
Conversation
d0a840a to
9e8cd23
Compare
pganssle
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround on this! Took me a while to grok how this works.
src/attr/_make.py
Outdated
| # XXX: a non-attrs class and subclass the resulting class with an attrs | ||
| # XXX: class. See `test_slotted_confused for details. For now that's | ||
| # XXX: OK with us. | ||
| for base_cls in self._cls.__bases__: |
There was a problem hiding this comment.
I assume you had a good reason for this, but why __bases__ and not __mro__[1:-1] here? It seems that that would fix the test_slotted_confused issue, and changing from __bases__ to __mro__[1:-1] doesn't cause any other tests to fail.
I think it would be good to either add a test that demonstrates the behavior you're trying to preserve by using __bases__ or switch to __mro__.
There was a problem hiding this comment.
Because __attrs_own_setattr__ is a computed attribute.
Take this for instance:
@attr.define
class A:
x: int = attr.field(on_setattr=frozen)
@attr.define
class B(A):
x: int = attr.field(on_setattr=NO_OP)A has __attrs_own_setattr__ = True but B doesn't. If I walk the whole MRO, it would turn up True.
IIUC, my only option would be to walk the MRO and apply the full resolution logic myself – whose avoidance's the whole reason behind __attrs_own_setattr__?
There was a problem hiding this comment.
A has
__attrs_own_setattr__ = Truebut B doesn't. If I walk the whole MRO, it would turn up True.
From my test, it seems like B does have __attrs_own_setattr__ == True, is this not intentional?
I think I've come up with a test that covers the situation you're trying to guard against, though:
def test_custom_setattr_subclass(self):
@attr.define
class A(object):
x = attr.field(on_setattr=setters.frozen)
@attr.define
class B(A):
x = attr.field(on_setattr=setters.NO_OP)
def __setattr__(self, key, value):
raise SystemError
@attr.define
class C(B):
pass
with pytest.raises(SystemError):
C(1).x = 2Probably worth adding that as a regression test so that no one shows up with a PR fixing the xfail-ing test by switching from __bases__ to __mro__[1:-1] or something of that nature.
There was a problem hiding this comment.
A has
__attrs_own_setattr__ = Truebut B doesn't. If I walk the whole MRO, it would turn up True.From my test, it seems like B does have
__attrs_own_setattr__ == True, is this not intentional?
No it's not, since B doesn't have an own setattr. 🙈
I think I've come up with a test that covers the situation you're trying to guard against, though:
def test_custom_setattr_subclass(self): @attr.define class A(object): x = attr.field(on_setattr=setters.frozen) @attr.define class B(A): x = attr.field(on_setattr=setters.NO_OP) def __setattr__(self, key, value): raise SystemError @attr.define class C(B): pass with pytest.raises(SystemError): C(1).x = 2
I'm pretty sure we do have such a test; I just wonder why anything works at all.
There was a problem hiding this comment.
That looks good, but (other than the strict XPASS), the test suite still succeeds when you use for base_cls in self._cls.__mro__[1:-1]:.
The only test that looks roughly like the one I suggested is this one, but the key to my test is that the "root" base class has an attrs-generated setattr and the intermediate class has a user-generated __setattr__. It's interesting, even with the __bases__ approach, it seems that there's some problem when some of these are slots classes and others aren't:
@pytest.mark.parametrize("a_slots", [True, False])
@pytest.mark.parametrize("b_slots", [True, False])
@pytest.mark.parametrize("c_slots", [True, False])
def test_setattr_inherited_do_not_reset_intermediate(self, a_slots, b_slots, c_slots):
@attr.define(slots=a_slots)
class A(object):
x = attr.ib(on_setattr=setters.frozen)
@attr.define(slots=b_slots)
class B(A):
x = attr.ib(on_setattr=setters.NO_OP)
def __setattr__(self, key, value):
raise SystemError
@attr.define(slots=c_slots)
class C(B):
pass
with pytest.raises(SystemError):
C(1).x = 3With the __mro__ approach, all 8 of these tests fail, but with the __bases__ approach, we still get failures whenever C is not a slots class. I assume that this is not intentional, and that __setattr__ inheritance should not be reset in this way? I think maybe this "check the base classes for a __setattr__" logic may need to be extended to non-slots classes.
There was a problem hiding this comment.
Argl, yeah so the problem is that we treat "user wrote a __setattr__" and "we wrote a __setattr__" the same way which is obviously wrong. We only ought to reset the latter one. This requires some deeper work.
There was a problem hiding this comment.
Paul! PAUL! I think I've got it! Please check out c5b690f
Now the tests also fail if you use __mro__[1:-1] AFAICT.
(I've also noticed that we've always used __bases__ in the call to type() so no need to be defensive with getattr() unless someone complains)
426b2ec to
0960124
Compare
pganssle
left a comment
There was a problem hiding this comment.
Putting the LGTM on this one even though there are un-addressed comments because I think that my remaining comments are "nice to haves" — I think the "meat" of this PR looks good.
If you do make changes and want me to take another look, let me know.
Fixes #680 Signed-off-by: Hynek Schlawack <hs@ox.cx>
Signed-off-by: Hynek Schlawack <hs@ox.cx>
Signed-off-by: Hynek Schlawack <hs@ox.cx>
e2bdc64 to
5644d21
Compare
Signed-off-by: Hynek Schlawack <hs@ox.cx>
Co-authored-by: Paul Ganssle <paul@ganssle.io>
Signed-off-by: Hynek Schlawack <hs@ox.cx>
So no reason for being defensive.
pganssle
left a comment
There was a problem hiding this comment.
Nice, I think this should cover it (or at least covers the test scenarios I came up with; this stuff is complicated enough that I doubt I'll ever be confident that it will "survive contact with the enemy").
Definitely the subject for another PR (since the scope of this one has already significantly expanded), but I wonder if it would make sense to add some hypothesis tests that generate inheritance hierarchies and assert some properties of the subclasses. I imagine doing so might be a bit complicated, but there do seem to be some useful properties we could assert here.
Co-authored-by: Paul Ganssle <paul@ganssle.io>
|
Thanks Paul! |
The detection of whether or not we should be resetting
__setattr__in slotted classes is currently wrong.Fixes #680