python-attrs / attrs

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

distinguish a class that inherits from an attrs class from a true (decorated) attrs class #1334

Open Darkdragon84 opened 3 months ago

Darkdragon84 commented 3 months ago

This question is related to issue https://github.com/python-attrs/attrs/issues/1332, where I needed an __init_subclass__ to be run only once for subclasses of an attrs base class. The issue with using built-in __init_subclass__ is that it doesn't play well with decorated classes, such as attrs or dataclass classes. Hence the addition of a new __attrs_init_subclass__ classmethod.

However, this method (rightfully) only gets called for subclasses that are explicitly decorated as attrs classes. I need a way to run code in an __init_subclass__ method of an attrs class for both attrs and plain old Python (POP) subclasses. I first thought of checking attrs.has on the subclass to see if it is an attrs or POP class. But of course the subclass inherits the __attrs_attrs__ field from the attrs base class, so this can't be used to make the distinction.

My question is thus: How can I distinguish a class that inherits from an attrs class from a true (decorated) attrs class? Some search suggests one way to achieve this is to check whether __attrs_attrs__ is in the class dict of the subclass.

from attrs import frozen

def is_directly_decorated(cls) -> bool:
    return "__attrs_attrs__" in cls.__dict__

def print_if_directly_decorated(cls):
    print(
        f"{cls.__name__} "
        + ("is" if is_directly_decorated(cls) else "isn't")
        + " directly decorated"
    )

@frozen
class BaseAttr: ...

class Sub(BaseAttr): ...

@frozen
class SubAttr(BaseAttr): ...

print_if_directly_decorated(Sub)
print_if_directly_decorated(SubAttr)

this gives

Sub isn't directly decorated
SubAttr is directly decorated

Is this a safe way to do this? Or can I exploit some built-in or attrs functions/methods to make this distinction?

hynek commented 3 months ago

Checking the class's __dict__ is how we check for direct attributes since 24.1, too.

Darkdragon84 commented 3 months ago

thanks for confirming 👍 would it make sense to add anything along those lines to attr.has?

hynek commented 3 months ago

I think it makes sense to expand has a bit (also maybe accept instances), but it would’ve to be gated behind an option I’m afraid. :|

Darkdragon84 commented 3 months ago

oh that's totally fine! Thanks for considering this :pray: