diff --git a/changelog.d/285.change.rst b/changelog.d/285.change.rst new file mode 100644 index 00000000..c3fbb797 --- /dev/null +++ b/changelog.d/285.change.rst @@ -0,0 +1,2 @@ +The attribute redefinition feature introduced in 17.3.0 now takes into account if an attribute is redefined via multiple inheritance. +In that case, the definition that is closer to the base of the class hierarchy wins. diff --git a/changelog.d/287.change.rst b/changelog.d/287.change.rst new file mode 100644 index 00000000..c3fbb797 --- /dev/null +++ b/changelog.d/287.change.rst @@ -0,0 +1,2 @@ +The attribute redefinition feature introduced in 17.3.0 now takes into account if an attribute is redefined via multiple inheritance. +In that case, the definition that is closer to the base of the class hierarchy wins. diff --git a/src/attr/_make.py b/src/attr/_make.py index 2877f8e1..a94b42b2 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -270,7 +270,7 @@ def _transform_attrs(cls, these, auto_attribs): # of attributes we've seen in `take_attr_names` and ignore their # redefinitions deeper in the hierarchy. super_attrs = [] - taken_attr_names = set(a.name for a in non_super_attrs) + taken_attr_names = {a.name: a for a in non_super_attrs} for super_cls in cls.__mro__[1:-1]: sub_attrs = getattr(super_cls, "__attrs_attrs__", None) if sub_attrs is not None: @@ -278,9 +278,16 @@ def _transform_attrs(cls, these, auto_attribs): # list in the end and get all attributes in the order they have # been defined. for a in reversed(sub_attrs): - if a.name not in taken_attr_names: + prev_a = taken_attr_names.get(a.name) + if prev_a is None: + super_attrs.append(a) + taken_attr_names[a.name] = a + elif prev_a == a: + # This happens thru multiple inheritance. We don't want + # to favor attributes that are further down in the tree + # so we move them to the back. + super_attrs.remove(a) super_attrs.append(a) - taken_attr_names.add(a.name) # Now reverse the list, such that the attributes are sorted by *descending* # age. IOW: the oldest attribute definition is at the head of the list. diff --git a/tests/test_make.py b/tests/test_make.py index 0c90532d..289c2939 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -214,6 +214,42 @@ class TestTransformAttrs(object): simple_attr("x"), ) == attrs + def test_multiple_inheritance(self): + """ + Order of attributes doesn't get mixed up by multiple inheritance. + + See #285 + """ + @attr.s + class A(object): + a1 = attr.ib(default="a1") + a2 = attr.ib(default="a2") + + @attr.s + class B(A): + b1 = attr.ib(default="b1") + b2 = attr.ib(default="b2") + + @attr.s + class C(B, A): + c1 = attr.ib(default="c1") + c2 = attr.ib(default="c2") + + @attr.s + class D(A): + d1 = attr.ib(default="d1") + d2 = attr.ib(default="d2") + + @attr.s + class E(D, C): + e1 = attr.ib(default="e1") + e2 = attr.ib(default="e2") + + assert ( + "E(a1='a1', a2='a2', b1='b1', b2='b2', c1='c1', c2='c2', d1='d1', " + "d2='d2', e1='e1', e2='e2')" + ) == repr(E()) + class TestAttributes(object): """