Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inheritance resolution of cached properties in slotted class #1289

Closed
wants to merge 1 commit into from
Closed
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/1289.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix inheritance resolution of cached properties in slotted class when subclasses do not define any `@cached_property` themselves but do define a custom `__getattr__()` method.
26 changes: 19 additions & 7 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,11 +917,27 @@ def _create_slots_class(self):
names += ("__weakref__",)

if PY_3_8_PLUS:
# Store class cached properties for further use by subclasses
# (below) while clearing them out from __dict__ to avoid name
# clashing.
cd["__attrs_cached_properties__"] = {
name: cd.pop(name).func
for name in [
name
for name, cached_property in cd.items()
if isinstance(cached_property, functools.cached_property)
]
}
# Gather cached properties from parent classes.
cached_properties = {
name: cached_property.func
for name, cached_property in cd.items()
if isinstance(cached_property, functools.cached_property)
name: func
for base_cls in self._cls.__mro__[1:-1]
for name, func in base_cls.__dict__.get(
"__attrs_cached_properties__", {}
).items()
}
# Then from this class.
cached_properties.update(cd["__attrs_cached_properties__"])
else:
# `functools.cached_property` was introduced in 3.8.
# So can't be used before this.
Expand All @@ -934,10 +950,6 @@ def _create_slots_class(self):
# Add cached properties to names for slotting.
names += tuple(cached_properties.keys())

for name in cached_properties:
# Clear out function from class to avoid clashing.
del cd[name]

additional_closure_functions_to_update.extend(
cached_properties.values()
)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,50 @@ def f(self):
assert b.z == "z"


@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_getattr_in_subclass_without_cached_property():
"""
Ensure that when a subclass of a slotted class with cached properties
defines a __getattr__ but has no cached property itself, parent's cached
properties are reachable.

Cover definition and usage of __attrs_cached_properties__ internal
attribute.

Regression test for issue https://github.com/python-attrs/attrs/issues/1288
"""

@attr.s(slots=True)
class A:
@functools.cached_property
def f(self):
return 0

@attr.s(slots=True)
class B(A):
def __getattr__(self, item):
return item

@attr.s(slots=True)
class C(B):
@functools.cached_property
def g(self):
return 1

b = B()
assert b.f == 0
assert b.z == "z"

c = C()
assert c.f == 0
assert c.g == 1
assert c.a == "a"

assert list(A.__attrs_cached_properties__) == ["f"]
assert B.__attrs_cached_properties__ == {}
assert list(C.__attrs_cached_properties__) == ["g"]


@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
def test_slots_getattr_in_subclass_gets_superclass_cached_property():
"""
Expand Down