Skip to content

Commit ecd0385

Browse files
authored
Merge pull request #83 from DavidCEllis/cached-property-preserve-dict
Use a __dict__ for cached_property values if it exists
2 parents d0de402 + 21e8674 commit ecd0385

File tree

3 files changed

+50
-25
lines changed

3 files changed

+50
-25
lines changed

src/ducktools/classbuilder/__init__.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -877,27 +877,29 @@ def __new__(
877877
if k not in {"__weakref__", "__dict__"}:
878878
fields[k] = v
879879

880-
# Special case cached_property
881-
# if a cached property is used we need to remove it so that
882-
# its attribute can be replaced by a slot, after the class
883-
# is constructed wrap the slot with a new special _SlottedCachedProperty
884-
# that will store the resulting value in the slot instead of in a dict.
880+
# Special case cached_property if there is no `__dict__` attribute
881+
# In the case where there is a __dict__ it is not necessary to replace
882+
# the cached_property attribute as the dict can be used, otherwise
883+
# it needs to be replaced in order to store the value in the slot
884+
# created.
885+
# Dict access is faster if there is a __dict__ available.
885886
cached_properties = {}
886887

887-
# Don't import functools
888-
if functools := sys.modules.get("functools"):
889-
# Iterate over a copy as we will mutate the original
890-
for k, v in ns.copy().items():
891-
if isinstance(v, functools.cached_property):
892-
cached_properties[k] = v
893-
del ns[k]
894-
# Add to slots only if it is not already a slot
895-
slot_attrib = base_attribs.get(k, NOTHING)
896-
if (
897-
slot_attrib is NOTHING
898-
or type(slot_attrib) not in existing_slot_types
899-
):
900-
slot_values[k] = None
888+
if "__dict__" not in slot_values and "__dict__" not in base_attribs:
889+
# Don't import functools
890+
if functools := sys.modules.get("functools"):
891+
# Iterate over a copy as we will mutate the original
892+
for k, v in ns.copy().items():
893+
if isinstance(v, functools.cached_property):
894+
cached_properties[k] = v
895+
del ns[k]
896+
# Add to slots only if it is not already a slot
897+
slot_attrib = base_attribs.get(k, NOTHING)
898+
if (
899+
slot_attrib is NOTHING
900+
or type(slot_attrib) not in existing_slot_types
901+
):
902+
slot_values[k] = None
901903

902904
# Place slots *after* everything else to be safe
903905
ns["__slots__"] = slot_values

tests/prefab/test_subclass_implementation.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from ducktools.classbuilder import get_flags
6+
from ducktools.classbuilder import _SlottedCachedProperty # type: ignore
77
from ducktools.classbuilder.prefab import Prefab, Attribute, SlotFields, get_attributes
88

99

@@ -257,7 +257,33 @@ def h2g2(self):
257257
ex = Example()
258258
assert not hasattr(ex, "__dict__")
259259
assert ex.h2g2 == 42
260+
assert isinstance(Example.h2g2, _SlottedCachedProperty)
260261

262+
def test_dict_present_leaves_cached_property(self):
263+
# Test cached_property is left in place if there is a __dict__ attribute available
264+
class Example(Prefab):
265+
__dict__: dict
266+
@functools.cached_property
267+
def h2g2(self):
268+
return 42
269+
270+
ex = Example()
271+
assert ex.h2g2 == 42
272+
assert isinstance(Example.h2g2, functools.cached_property)
273+
274+
def test_dict_inherited_leaves_cached_property(self):
275+
# Test it is left in place if __dict__ is inherited
276+
class Example(Prefab):
277+
__dict__: dict
278+
279+
class ExampleInherit(Example):
280+
@functools.cached_property
281+
def h2g2(self):
282+
return 42
283+
284+
ex = ExampleInherit()
285+
assert ex.h2g2 == 42
286+
assert isinstance(ExampleInherit.h2g2, functools.cached_property)
261287

262288
def test_subclass_cached_property(self):
263289
# Test we don't suffer from https://github.com/python-attrs/attrs/issues/1333
@@ -293,7 +319,7 @@ class Parent(Prefab):
293319
# This test exists to document the weirdness
294320
# Thankfully mypy flags this as an error
295321
class Child(Parent):
296-
@functools.cached_property
322+
@functools.cached_property # type: ignore
297323
def name(self):
298324
return "Bill"
299325

tests/test_slotmakermeta.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@ class ExampleBase(metaclass=SlotMakerMeta):
8585

8686

8787
class TestCachedProperty:
88-
# Test that a cached property causes __dict__ to be
89-
# automatically added to __slots__
90-
9188
@graalpy_fails
9289
def test_no_cached_property(self):
9390
class Example(metaclass=SlotMakerMeta):
@@ -116,4 +113,4 @@ class Example(metaclass=SlotMakerMeta):
116113
def cache(self):
117114
return 42
118115

119-
assert Example.__slots__ == {"x": None, "cache": None, "__dict__": "dict for cached property"}
116+
assert Example.__slots__ == {"x": None, "__dict__": "dict for cached property"}

0 commit comments

Comments
 (0)