Skip to content

Commit 000c563

Browse files
hynekJelleZijlstra
andauthored
Fix slotted reference cycles on 3.14 (#1446)
* Fix slotted reference cycles on 3.14 Ref python/cpython#136893 & python/cpython#135228 Co-authored-by: Jelle Zijlstra <[email protected]> * Add news fragment * Bump version tag to CMA Co-authored-by: Brandt Bucher <[email protected]> Co-authored-by: Jelle Zijlstra <[email protected]> * Apply review comment #1446 (comment) * EAFP --------- Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent cbaef3f commit 000c563

File tree

3 files changed

+29
-2
lines changed

3 files changed

+29
-2
lines changed

changelog.d/1446.change.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
On 3.14, the cycles in slotted classes are now manually broken.
2+
An explicit call to `gc.collect()` is still necessary, unfortunately.

src/attr/_make.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
PY_3_10_PLUS,
2525
PY_3_11_PLUS,
2626
PY_3_13_PLUS,
27+
PY_3_14_PLUS,
2728
_AnnotationExtractor,
2829
_get_annotations,
2930
get_generic_base,
@@ -618,6 +619,17 @@ def evolve(*args, **changes):
618619
return cls(**changes)
619620

620621

622+
# Hack to the get the underlying dict out of a mappingproxy
623+
# Use it with: cls.__dict__ | _deproxier
624+
# See: https://github.com/python/cpython/pull/136893
625+
class _Deproxier:
626+
def __ror__(self, other):
627+
return other
628+
629+
630+
_deproxier = _Deproxier()
631+
632+
621633
class _ClassBuilder:
622634
"""
623635
Iteratively build *one* class.
@@ -845,6 +857,20 @@ def _create_slots_class(self):
845857
if k not in (*tuple(self._attr_names), "__dict__", "__weakref__")
846858
}
847859

860+
if PY_3_14_PLUS:
861+
# Clean up old dict to avoid leaks.
862+
old_cls_dict = self._cls.__dict__ | _deproxier
863+
old_cls_dict.pop("__dict__", None)
864+
if "__weakref__" in self._cls.__dict__:
865+
del self._cls.__weakref__
866+
867+
# Manually bump internal version tag.
868+
try:
869+
self._cls.__abstractmethods__ = self._cls.__abstractmethods__
870+
except AttributeError:
871+
self._cls.__abstractmethods__ = frozenset({"__init__"})
872+
del self._cls.__abstractmethods__
873+
848874
# If our class doesn't have its own implementation of __setattr__
849875
# (either from the user or by us), check the bases, if one of them has
850876
# an attrs-made __setattr__, that needs to be reset. We don't walk the

tests/test_make.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import attr
2424

2525
from attr import _config
26-
from attr._compat import PY_3_10_PLUS, PY_3_14_PLUS
26+
from attr._compat import PY_3_10_PLUS
2727
from attr._make import (
2828
Attribute,
2929
Factory,
@@ -1939,7 +1939,6 @@ class C2(C):
19391939

19401940
assert [C2] == C.__subclasses__()
19411941

1942-
@pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.")
19431942
def test_no_references_to_original_when_using_cached_property(self):
19441943
"""
19451944
When subclassing a slotted class and using cached property, there are

0 commit comments

Comments
 (0)