Type-annotate but skip class attributes in an auto_attribs class

Viewed 426

I am currently designing a class structure using attrs. It is a tree-like structure where I need a parent backlink for certain purposes. OTOH, I want to use frozen classes (there are cached properties, which rely on read-only state).

This means that the tree must be instantiated bottom-up, child-before parent. To solve this, I invented a special kind of weakref object, which can be lazy-bound exactly once.

class LateRef(Generic[T]):
    def __init__(self):
        self.ref: Optional[T] = None

    def set(self, obj: T):
        if self.ref is not None:
            raise RuntimeError('Reference can only be set once.')
        self.ref = weakref.ref(obj)

    def __call__(self) -> Optional[T]:
        if self.ref is None:
            return None
        return self.ref()

There is custom initialization code to create an unbound reference in the child, and bind them in the parent:

@attr.s(auto_attribs=True, frozen=True)
class Child:
    some_attribute:str = "whatever"
   
    def __attrs_post_init__(self):
        self.__dict__['parent'] = LateRef()

@attr.s(auto_attribs=True, frozen=True)
class Parent:
    children: List[Child] = attr.ib(converter=_deepclone) # off-screen :-)
    
    def __attrs_post_init__(self):
        for child in self.children:
            child.parent.set(self)

I need to sneak in the Child.parent attribute through the backdoor due to the frozenness of child. Previously I put it as a class attribute with custom factory, but then it is turned into an attrs-attribute, which it really shouldn't be. E.g. I don't want the parent to turn up in hashing, comparison, str representation and so on.

The code is already working, BUT: As you see I would like to do things right and type-annotate as far as possible. Putting the parent directly in the __dict__ means that the usual tools don't know the expected type. In fact they might even complain that the parent attribute doesn't exist at all.

Long story short question: Is there a way to declare parent as class attribute with type annotation, while telling attr.s to skip / ignore it? Or is there another solution I am overlooking?

Edit: i.e. How to make a static type checker recognize the parent attribute's type, without making it an attr.ib.

2 Answers

If your only concern is to bypass the frozen-ness of a frozen class, I’d suggest to take the same route as attrs in its __init__ methods and use object.__setattr__ directly. attrs classes are always frozen, so we have to work around it too.

Might not be the most elegant way, but Python sadly isn’t really designed for immutability, so we have to work with what we have.

That said, your _deepclone converter seems to indicate that you don’t need/want to add the original objects to the Parent? In that case attr.evolve() is the perfect tool for you.

P.S. you can write just @attr.frozen now. auto_attribs is True by default. See https://www.attrs.org/en/stable/api.html#next-generation-apis

The solution I settled on: (Thanks @hynek for the discussion and your awesome work!)

@attr.frozen(slots=False)
class Child:
    some_attribute:str = "whatever"
   
    @functools.cached_property
    def parent(self) -> LateRef["Parent"]:
        return LateRef()

# Parent class stays the same

Works nearly the same (parent is stored as __dict__["parent"] on first access), and is understood flawlessly at least by VSCode.

Related