Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1549.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Inherited default factories can now be overridden in subclasses.
32 changes: 30 additions & 2 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading