diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 4b7422eee2..8b9d0b8e9d 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -578,7 +578,10 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) { /// dynamic_attr: Allow the garbage collector to traverse the internal instance `__dict__`. extern "C" inline int pybind11_traverse(PyObject *self, visitproc visit, void *arg) { #if PY_VERSION_HEX >= 0x030D0000 - PyObject_VisitManagedDict(self, visit, arg); + int ret = PyObject_VisitManagedDict(self, visit, arg); + if (ret) { + return ret; + } #else PyObject *&dict = *_PyObject_GetDictPtr(self); Py_VISIT(dict); diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 2030cd6715..84efb800db 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -104,6 +104,10 @@ TEST_SUBMODULE(class_, m) { ~NoConstructorNew() { print_destroyed(this); } }; + struct DynamicAttr { + DynamicAttr() = default; + }; + py::class_(m, "NoConstructor") .def_static("new_instance", &NoConstructor::new_instance, "Return an instance"); @@ -112,6 +116,8 @@ TEST_SUBMODULE(class_, m) { .def_static("__new__", [](const py::object &) { return NoConstructorNew::new_instance(); }); + py::class_(m, "DynamicAttr", py::dynamic_attr()).def(py::init<>()); + // test_pass_unique_ptr struct ToBeHeldByUniquePtr {}; py::class_>(m, "ToBeHeldByUniquePtr") diff --git a/tests/test_class.py b/tests/test_class.py index fae6a31899..ab7876e4c5 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gc import sys from unittest import mock @@ -45,6 +46,12 @@ def test_instance(msg): assert cstats.alive() == 0 +def test_get_referrers(): + instance = m.DynamicAttr() + instance.a = "test" + assert instance in gc.get_referrers(instance.__dict__) + + def test_instance_new(): instance = m.NoConstructorNew() # .__new__(m.NoConstructor.__class__)