Skip to content

MRO vs. inherited attributes #428

Closed

Description

@galcik

The way how attributes are collected from super classes is different comparing to how attributes and methods are searched by getattr (based on MRO for new-style classes).

Example:

@attr.s
class A(object):

    x = attr.ib(10)

    def xx(self):
        return 10


@attr.s
class B(A):
    y = attr.ib(20)


@attr.s
class C(A):
    x = attr.ib(50)

    def xx(self):
        return 50


class D(B, C):
    pass


d = D()
print(d.x)  # prints 10
print(d.xx())  # prints 50

I think it would be great to the use the same approach for collecting attributes as used for searching methods and attributes.

The difference is caused by the fact that the function _transform_attrs in attrs/_make.py considers all attributes (own+inherited stored in __attrs_attrs__) and not only own attributes of super classes:

attrs/src/attr/_make.py

Lines 367 to 368 in 6a07b03

for super_cls in cls.__mro__[1:-1]:
sub_attrs = getattr(super_cls, "__attrs_attrs__", None)

Activity

  1. hynek commented on Sep 1, 2018

    @hynek
    Member

    Hm that’s unfortunate and fixing it likely means breakage. :|

  2. added this to the import attrs milestone on Oct 27, 2018
  3. hynek commented on Aug 19, 2019

    @hynek
    Member

    So this is interesting.

    The underlying issue is that attrs always writes all methods optimized for the current class to avoid super() calls/traversal which means we have to simulate the __mro__ ourselves when building the lists of attributes. FTR, dataclasses handle it the same way.

    As such, I was able to make your test case work by ignoring inherited attributes iff D is also decorated with @attr.s because then attrs has the chance to intervene.


    There's another problem too: users expect the attributes to be ordered (in __init__ parameter lists and reprs for example) in the order they are defined. E.g. if you have:

            @attr.s
            class C(object):
                c = attr.ib(default=100)
                x = attr.ib(default=1)
                b = attr.ib(default=23)
    
            @attr.s
            class D(C):
                a = attr.ib(default=42)
                x = attr.ib(default=2)
                d = attr.ib(default=3.14)

    The attribute order is c, b, a, x, d (N.B. that x changed position due to overwrite).

    If I ignore inherited attributes and walk the mro, the order becomes a, x, d, c, b.

    I'm open to suggestions how to resolve this to make everyone happy but I banged my head against it for over an hour and didn't really come up with anything viable. :(

    I guess traversing it twice would work? Once for getting the order of the names right and then again to resolve them to the correct classes? 🤔

  4. modified the milestones: import attrs, 20.1.0 on Mar 6, 2020
  5. hynek commented on Mar 30, 2020

    @hynek
    Member

    If you don't mind, have a look at #635 whether it covers everything you'd expect.

  6. hynek commented on Apr 6, 2020

    @hynek
    Member

    I have merged it now, please let me know if something is not OK.

  7. jacobg commented on Aug 18, 2020

    @jacobg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions