diff --git a/changelog.d/1446.change.md b/changelog.d/1446.change.md new file mode 100644 index 000000000..2a069ea36 --- /dev/null +++ b/changelog.d/1446.change.md @@ -0,0 +1,2 @@ +On 3.14, the cycles in slotted classes are now manually broken. +An explicit call to `gc.collect()` is still necessary, unfortunately. diff --git a/src/attr/_make.py b/src/attr/_make.py index 4b3ec7d03..b24c8d0d7 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -24,6 +24,7 @@ PY_3_10_PLUS, PY_3_11_PLUS, PY_3_13_PLUS, + PY_3_14_PLUS, _AnnotationExtractor, _get_annotations, get_generic_base, @@ -618,6 +619,17 @@ def evolve(*args, **changes): return cls(**changes) +# Hack to the get the underlying dict out of a mappingproxy +# Use it with: cls.__dict__ | _deproxier +# See: https://github.com/python/cpython/pull/136893 +class _Deproxier: + def __ror__(self, other): + return other + + +_deproxier = _Deproxier() + + class _ClassBuilder: """ Iteratively build *one* class. @@ -845,6 +857,20 @@ def _create_slots_class(self): if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") } + if PY_3_14_PLUS: + # Clean up old dict to avoid leaks. + old_cls_dict = self._cls.__dict__ | _deproxier + old_cls_dict.pop("__dict__", None) + if "__weakref__" in self._cls.__dict__: + del self._cls.__weakref__ + + # Manually bump internal version tag. + try: + self._cls.__abstractmethods__ = self._cls.__abstractmethods__ + except AttributeError: + self._cls.__abstractmethods__ = frozenset({"__init__"}) + del self._cls.__abstractmethods__ + # If our class doesn't have its own implementation of __setattr__ # (either from the user or by us), check the bases, if one of them has # an attrs-made __setattr__, that needs to be reset. We don't walk the diff --git a/tests/test_make.py b/tests/test_make.py index 2e350577c..8f4846a78 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -23,7 +23,7 @@ import attr from attr import _config -from attr._compat import PY_3_10_PLUS, PY_3_14_PLUS +from attr._compat import PY_3_10_PLUS from attr._make import ( Attribute, Factory, @@ -1939,7 +1939,6 @@ class C2(C): assert [C2] == C.__subclasses__() - @pytest.mark.xfail(PY_3_14_PLUS, reason="Currently broken on nightly.") def test_no_references_to_original_when_using_cached_property(self): """ When subclassing a slotted class and using cached property, there are