diff --git a/changelog.d/221.change.rst b/changelog.d/221.change.rst new file mode 100644 index 00000000..a7cb88ee --- /dev/null +++ b/changelog.d/221.change.rst @@ -0,0 +1 @@ +Subclasses now can overwrite attribute definitions of their superclass. diff --git a/src/attr/_make.py b/src/attr/_make.py index e687dfad..987e50be 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -181,11 +181,6 @@ def _transform_attrs(cls, these): If *these* is passed, use that and don't look for them on the class. """ - super_cls = [] - for c in reversed(cls.__mro__[1:-1]): - sub_attrs = getattr(c, "__attrs_attrs__", None) - if sub_attrs is not None: - super_cls.extend(a for a in sub_attrs if a not in super_cls) if these is None: ca_list = [(name, attr) for name, attr @@ -201,6 +196,17 @@ def _transform_attrs(cls, these): for attr_name, ca in sorted(ca_list, key=lambda e: e[1].counter) ] + + super_cls = [] + non_super_names = set(a.name for a in non_super_attrs) + for c in reversed(cls.__mro__[1:-1]): + sub_attrs = getattr(c, "__attrs_attrs__", None) + if sub_attrs is not None: + super_cls.extend( + a for a in sub_attrs + if a not in super_cls and a.name not in non_super_names + ) + attr_names = [a.name for a in super_cls + non_super_attrs] AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) diff --git a/tests/test_dark_magic.py b/tests/test_dark_magic.py index 1567986b..4baca203 100644 --- a/tests/test_dark_magic.py +++ b/tests/test_dark_magic.py @@ -271,3 +271,15 @@ class TestDarkMagic(object): return self.x + 1 assert C(1, 2) == C() + + @pytest.mark.parametrize("slots", [True, False]) + @pytest.mark.parametrize("frozen", [True, False]) + def test_attrib_overwrite(self, slots, frozen): + """ + Subclasses can overwrite attributes of their superclass. + """ + @attr.s(slots=slots, frozen=frozen) + class SubOverwrite(Super): + x = attr.ib(default=attr.Factory(list)) + + assert SubOverwrite([]) == SubOverwrite()