Skip to content

Commit

Permalink
Handle attrib redefs thru multiple inheritance (#287)
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek authored Nov 11, 2017
1 parent 1e6627c commit a84a36d
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 3 deletions.
2 changes: 2 additions & 0 deletions changelog.d/285.change.rst
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions changelog.d/287.change.rst
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 10 additions & 3 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,17 +270,24 @@ 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:
# We iterate over sub_attrs backwards so we can reverse the whole
# 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.
Expand Down
36 changes: 36 additions & 0 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,42 @@ class C(Base):
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):
"""
Expand Down

0 comments on commit a84a36d

Please sign in to comment.