diff --git a/changelog.d/1549.change.md b/changelog.d/1549.change.md new file mode 100644 index 000000000..63218aa5c --- /dev/null +++ b/changelog.d/1549.change.md @@ -0,0 +1 @@ +Inherited default factories can now be overridden in subclasses. diff --git a/src/attr/_make.py b/src/attr/_make.py index afbca4635..d71581958 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -310,6 +310,30 @@ def _is_class_var(annot): return annot.startswith(_CLASSVAR_PREFIXES) +def _update_inherited_default_factory(cls, attribute): + """ + Rebind self-taking default factories overridden by subclasses. + """ + if not isinstance(attribute.default, Factory): + return attribute + + default_factory = attribute.default + if not default_factory.takes_self: + return attribute + + factory_name = getattr(default_factory.factory, "__name__", None) + if factory_name is None: + return attribute + + factory = getattr(cls, factory_name, default_factory.factory) + if factory is default_factory.factory or not callable(factory): + return attribute + + return attribute.evolve( + default=Factory(factory, takes_self=True), + ) + + def _has_own_attribute(cls, attrib_name): """ Check whether *cls* defines *attrib_name* (and doesn't just inherit it). @@ -332,7 +356,9 @@ def _collect_base_attrs( if a.inherited or a.name in taken_attr_names: continue - a = a.evolve(inherited=True) # noqa: PLW2901 + a = _update_inherited_default_factory( # noqa: PLW2901 + cls, a.evolve(inherited=True) + ) base_attrs.append(a) base_attr_map[a.name] = base_cls @@ -370,7 +396,9 @@ def _collect_base_attrs_broken(cls, taken_attr_names): if a.name in taken_attr_names: continue - a = a.evolve(inherited=True) # noqa: PLW2901 + a = _update_inherited_default_factory( # noqa: PLW2901 + cls, a.evolve(inherited=True) + ) taken_attr_names.add(a.name) base_attrs.append(a) base_attr_map[a.name] = base_cls diff --git a/tests/test_make.py b/tests/test_make.py index b32f1054e..f04b00176 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -506,6 +506,29 @@ class D(B, C): assert d.x == d.xx() + @pytest.mark.parametrize("decorator", [attr.s, attr.define]) + def test_inherited_default_method_override(self, decorator): + """ + An inherited default method can be overridden by a subclass. + """ + + class A: + foo: str = attr.field() + + @foo.default + def _foo_default(self): + return "A" + + A = decorator(A) + + class B(A): + def _foo_default(self): + return "B" + + B = decorator(B) + + assert "B" == B().foo + def test_inherited(self): """ Inherited Attributes have `.inherited` True, otherwise False.