diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index f28155fe50148d..162b0b38f8555d 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1569,6 +1569,26 @@ def make_pairs(): self.assertEqual(d.get(key3_3), 44) self.assertGreaterEqual(eq_count, 1) + def test_overwrite_managed_dict(self): + # GH-130327: Overwriting an object's managed dictionary with another object's + # skipped traversal in favor of inline values, causing the GC to believe that + # the __dict__ wasn't reachable. + import gc + + class Shenanigans: + pass + + to_be_deleted = Shenanigans() + to_be_deleted.attr = "whatever" + holds_reference = Shenanigans() + holds_reference.__dict__ = to_be_deleted.__dict__ + holds_reference.ref = {"circular": to_be_deleted, "data": 42} + + del to_be_deleted + gc.collect() + self.assertEqual(holds_reference.ref['data'], 42) + self.assertEqual(holds_reference.attr, "whatever") + def test_unhashable_key(self): d = {'a': 1} key = [1, 2, 3] diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst new file mode 100644 index 00000000000000..9b9a282b5ab414 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-19-21-06-30.gh-issue-130327.z3TaR8.rst @@ -0,0 +1,2 @@ +Fix erroneous clearing of an object's :attr:`~object.__dict__` if +overwritten at runtime. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index bcd3c862fd59b2..5894fdb614ebdc 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -4825,10 +4825,8 @@ dict_traverse(PyObject *op, visitproc visit, void *arg) if (DK_IS_UNICODE(keys)) { if (_PyDict_HasSplitTable(mp)) { - if (!mp->ma_values->embedded) { - for (i = 0; i < n; i++) { - Py_VISIT(mp->ma_values->values[i]); - } + for (i = 0; i < n; i++) { + Py_VISIT(mp->ma_values->values[i]); } } else { @@ -7413,16 +7411,21 @@ PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) if((tp->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) { return 0; } - if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) { + PyDictObject *dict = _PyObject_ManagedDictPointer(obj)->dict; + if (dict != NULL) { + // GH-130327: If there's a managed dictionary available, we should + // *always* traverse it. The dict is responsible for traversing the + // inline values if it points to them. + Py_VISIT(dict); + } + else if (tp->tp_flags & Py_TPFLAGS_INLINE_VALUES) { PyDictValues *values = _PyObject_InlineValues(obj); if (values->valid) { for (Py_ssize_t i = 0; i < values->capacity; i++) { Py_VISIT(values->values[i]); } - return 0; } } - Py_VISIT(_PyObject_ManagedDictPointer(obj)->dict); return 0; }