python-attrs / attrs

Python Classes Without Boilerplate
https://www.attrs.org/
MIT License
5.3k stars 372 forks source link

attr.s(slots=true) leaves original class in __subclasses__ #691

Closed badicsalex closed 4 years ago

badicsalex commented 4 years ago

Hi,

I have a kind of a niche use-case: I use attrs classes with slots, and I tried to use __subclasses__ to discover class trees for serialization.

I know that using __subclasses__ is basically undocumented behaviour, so feel free to stop reading here and reject the ticket.

The issue looks like the same as #407: attr.s(slots=True) leaves the original class in the parent's __subclasses__:

@attr.s(auto_attribs=True, slots=True)
class A:
    field: int

@attr.s(auto_attribs=True, slots=True)
class B(A):
    field2: int

print(A)
print(hex(hash(A)))
print(B)
print(hex(hash(B)))
print(A.__subclasses__()[0])
print(hex(hash(A.__subclasses__()[0])))
print(B == A.__subclasses__()[0])
print(A.__subclasses__()[1])
print(hex(hash(A.__subclasses__()[1])))
print(B == A.__subclasses__()[1])

The issue is present with both Python 3.8.5 and Pypy 7.3.1

badicsalex commented 4 years ago

I see I have to manually do a gc.collect() to solve the issue. Sorry for not searching previous issues first.

hynek commented 4 years ago

The problem did sound familiar.

I think we should document it in https://www.attrs.org/en/stable/glossary.html#glossary

badicsalex commented 4 years ago

TBH, I wasn't aware of the glossary page. I was mostly looking at the API Reference, and didn't click "slotted classes", because I thought "well, I know what slotted classes are, so OK". If I did, "certain metaclass features like __init_subclass__ do not work with slotted classes." would certainly have caught my attention.

Since this only cropped up 2 times in the whole lifetime of the project, I think it's such a corner cases that you don't need to bother. If you still want to, you should probably explicitly add the gc.collect() workaround to the slots article, and also mention in the reference that people should click the "slotted classes" link.

hynek commented 4 years ago

I've added b0d2d92 & 6aca10f – I hope that helps in the future. :)

badicsalex commented 4 years ago

Thank you! I love your work, it's a great library.

hynek commented 4 years ago

Thank you for the kind words and the Paypal love! (as for the anonymity: I guess that's impossible due to money laundering laws? 🤷‍♂️)

SoundsSerious commented 2 years ago

I'm not 100% clear (more like 10% clear :) on the issues here, sounds like this is an edge case, but I'll report it was an issue in my application. This started happening on the switch from attr to attrs.

hynek commented 2 years ago

If you moved from attr.s to define, it’s because we stitched slots to true.

Don’t worry about not understanding fully what’s happening here; neither do I. ;)

maebert commented 8 months ago

Just adding something for visibility here: this thread helped me solve a maybe related issue, which was kind of a Heisenbug: things worked fine when I imported classes from a python shell, but in the actual execution environment, some attributes from my class were simply missing (or rather, returned a _CountingAttr instead of the default value).

I frankly can't claim to understand how exactly that happened (I generally enjoy that I don't have to think about garbage collection in Python), but in the end simply switching @define to @define(slots=False) solved the problem. I would love to post a MWE, but because it worked fine when importing things line by line this might take a while to reproduce either. In any case, this cost me half a day of my life, so may I propose a more prominent "Troubleshooting" section in the docs that highlights the complications that might arise from slotted classes?

hynek commented 8 months ago

Getting back _CountingAttr means that for some reason the define/attr.s decorator wasn't applied to the class for some reason. That doesn't like a gc issue but something wilder. 😳