Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AttributeError when accessing an attribute with a default value set in a child class #1240

Open
Wenzel opened this issue Feb 20, 2024 · 4 comments

Comments

@Wenzel
Copy link

Wenzel commented Feb 20, 2024

Hi,

I'm surprised that this attrs code doesn't work:

#!/usr/bin/env python3

from abc import ABC
from attrs import define, field

@define(slots=False, auto_attribs=True, auto_detect=True)
class A(ABC):
    field_1: int = 42
    field_2: int = field(init=False)

    def __attrs_post_init__(self):
        print(f"A.__attrs_post_init__: field_1 default = {self.field_1}")
        self.field_2 = self.field_1 * 2


@define(slots=False, auto_attribs=True, auto_detect=True)
class B(A):
    field_3: int = 0

    def __attrs_pre_init__(self, field_1: int, *args, **kwargs):
        super().__init__(field_1)

    def __attrs_post_init__(self):
        super().__attrs_post_init__()
        # raises AttributeError when accessing field_3
        print(f"B.__attrs_post_init__: field_3 default = {self.field_3}")
        print(self.field_3)

if __name__ == '__main__':
    instance = B()

When I try to access field_3 in the child's __attrs_post_init__, the attribute, despite having a default value like another one in the parent class, is undefined.

My assumption was that, by the time attrs has reached __attrs_post_init__, the fields with default values should have been set already (which is the case for the parent class)

Is there some subtlety that i'm missing here with attrs initialization ?

Thanks !

@hynek
Copy link
Member

hynek commented Feb 21, 2024

I can't quite follow everything that's happening because you're essentially breaking attrs with you super() calls. attrs never calls super() and is not built for it. Every class builds an optimal version of __init__ according the fields that it discovers.

@Wenzel
Copy link
Author

Wenzel commented Feb 21, 2024

Hi @hynek and thanks for your answer.

So a class decorated by attrs is not meant to call super() ?
How should it initialize it's parent attributes ?

In the docs you mentioned that it's __attrs_pre_init__() role:
https://www.attrs.org/en/stable/init.html#pre-init

Which is what I did.
So i suppose it's my super() call in __attrs_post_init__() that you think is breaking attrs ?
Because even if I comment that super() call:

@define(slots=False, auto_attribs=True, auto_detect=True)
class B(A):
    field_3: int = 0

    def __attrs_pre_init__(self, field_1: int, *args, **kwargs):
        super().__init__(field_1)

    def __attrs_post_init__(self):
        # super().__attrs_post_init__()
        # raises AttributeError when accessing field_3
        print(f"B.__attrs_post_init__: field_3 default = {self.field_3}")
        print(self.field_3)

The self.field_3 attribute will not be initialized by the default value at this point.

How attrs is expected to behave with inheritance ?
Is that part of the scope ? Or should I only use attrs with standalone without inheritance ?

Thanks !

@hynek
Copy link
Member

hynek commented Feb 22, 2024

So a class decorated by attrs is not meant to call super() ? How should it initialize it's parent attributes ?

You don't have to. The __init__ of an attrs class that inherits from another attrs class will initialize them for you.

In the docs you mentioned that it's __attrs_pre_init__() role: https://www.attrs.org/en/stable/init.html#pre-init

It says:

The sole reason for the existence of attrs_pre_init is to give users the chance to call super().init(), because some subclassing-based APIs require that.

I guess I can add a clarification that that's never necessary with attrs classes.

Which is what I did. So i suppose it's my super() call in __attrs_post_init__() that you think is breaking attrs ? Because even if I comment that super() call:

@define(slots=False, auto_attribs=True, auto_detect=True)
class B(A):
    field_3: int = 0

    def __attrs_pre_init__(self, field_1: int, *args, **kwargs):
        super().__init__(field_1)

    def __attrs_post_init__(self):
        # super().__attrs_post_init__()
        # raises AttributeError when accessing field_3
        print(f"B.__attrs_post_init__: field_3 default = {self.field_3}")
        print(self.field_3)

The self.field_3 attribute will not be initialized by the default value at this point.

This works as expected:

from abc import ABC
from attrs import define, field

@define
class A(ABC):
    field_1: int = 42
    field_2: int = field(init=False)

    def __attrs_post_init__(self):
        print(f"A.__attrs_post_init__: field_1 default = {self.field_1}")
        self.field_2 = self.field_1 * 2


@define
class B(A):
    field_3: int = 0

    def __attrs_post_init__(self):
        super().__attrs_post_init__()
        print(f"B.__attrs_post_init__: field_3 default = {self.field_3}")
        print(self.field_2)
        print(self.field_3)

if __name__ == '__main__':
    instance = B()

How attrs is expected to behave with inheritance ? Is that part of the scope ? Or should I only use attrs with standalone without inheritance ?

attrs does work with inheritance in general, but it can get a bit hairy when you use a lot of pre-inits and post-inits.

JFTR, you can always inspect the __init__ that attrs wrote for you:

>>> import inspect
>>> print(inspect.getsource(B.__init__))
def __init__(self, field_1=attr_dict['field_1'].default, field_3=attr_dict['field_3'].default):
    self.field_1 = field_1
    self.field_3 = field_3
    self.__attrs_post_init__()

@hynek
Copy link
Member

hynek commented Feb 22, 2024

warning added in ec9ef8d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants