Skip to content

Commit e735e89

Browse files
committed
Fix inheritance resolution of cached properties in slotted class
This resolves the case where a sub-class of a slotted class defining some cached properties has a custom __getattr__() method. In that case, we need to build the custom __getattr__ implementation (see in _make_cached_property_getattr()) using cached properties from all classes in the MRO. In order to keep references of cached properties defined the inheritance hierarchy, we store them in a new __attrs_cached_properties__ attribute and finally build the "cached_properties" value, passed to _make_cached_property_getattr(), by combining current class' cached properties with that of all its parents. Also, when building __attrs_cached_properties__, we now clear current class' __dict__ (name 'cd'), thus saving an extra loop. Fix python-attrs#1288
1 parent 5618e6f commit e735e89

File tree

2 files changed

+63
-7
lines changed

2 files changed

+63
-7
lines changed

src/attr/_make.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -917,11 +917,27 @@ def _create_slots_class(self):
917917
names += ("__weakref__",)
918918

919919
if PY_3_8_PLUS:
920+
# Store class cached properties for further use by subclasses
921+
# (below) while clearing them out from __dict__ to avoid name
922+
# clashing.
923+
cd["__attrs_cached_properties__"] = {
924+
name: cd.pop(name).func
925+
for name in [
926+
name
927+
for name, cached_property in cd.items()
928+
if isinstance(cached_property, functools.cached_property)
929+
]
930+
}
931+
# Gather cached properties from parent classes.
920932
cached_properties = {
921-
name: cached_property.func
922-
for name, cached_property in cd.items()
923-
if isinstance(cached_property, functools.cached_property)
933+
name: func
934+
for base_cls in self._cls.__mro__[1:-1]
935+
for name, func in base_cls.__dict__.get(
936+
"__attrs_cached_properties__", {}
937+
).items()
924938
}
939+
# Then from this class.
940+
cached_properties.update(cd["__attrs_cached_properties__"])
925941
else:
926942
# `functools.cached_property` was introduced in 3.8.
927943
# So can't be used before this.
@@ -934,10 +950,6 @@ def _create_slots_class(self):
934950
# Add cached properties to names for slotting.
935951
names += tuple(cached_properties.keys())
936952

937-
for name in cached_properties:
938-
# Clear out function from class to avoid clashing.
939-
del cd[name]
940-
941953
additional_closure_functions_to_update.extend(
942954
cached_properties.values()
943955
)

tests/test_slots.py

+44
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,50 @@ def f(self):
891891
assert b.z == "z"
892892

893893

894+
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
895+
def test_slots_getattr_in_subclass_without_cached_property():
896+
"""
897+
Ensure that when a subclass of a slotted class with cached properties
898+
defines a __getattr__ but has no cached property itself, parent's cached
899+
properties are reachable.
900+
901+
Cover definition and usage of __attrs_cached_properties__ internal
902+
attribute.
903+
904+
Regression test for issue https://github.com/python-attrs/attrs/issues/1288
905+
"""
906+
907+
@attr.s(slots=True)
908+
class A:
909+
@functools.cached_property
910+
def f(self):
911+
return 0
912+
913+
@attr.s(slots=True)
914+
class B(A):
915+
def __getattr__(self, item):
916+
return item
917+
918+
@attr.s(slots=True)
919+
class C(B):
920+
@functools.cached_property
921+
def g(self):
922+
return 1
923+
924+
b = B()
925+
assert b.f == 0
926+
assert b.z == "z"
927+
928+
c = C()
929+
assert c.f == 0
930+
assert c.g == 1
931+
assert c.a == "a"
932+
933+
assert list(A.__attrs_cached_properties__) == ["f"]
934+
assert not B.__attrs_cached_properties__
935+
assert list(C.__attrs_cached_properties__) == ["g"]
936+
937+
894938
@pytest.mark.skipif(not PY_3_8_PLUS, reason="cached_property is 3.8+")
895939
def test_slots_getattr_in_subclass_gets_superclass_cached_property():
896940
"""

0 commit comments

Comments
 (0)