gh-149216: Notify type watchers on heap type deallocation (GH-149236)

Authored-by: Anuj Bharambe <anujnitinb@gmail.com>
This commit is contained in:
Anuj Nitin Bharambe
2026-05-05 15:54:07 +05:30
committed by GitHub
parent 114781040c
commit f6d16a0d70
5 changed files with 88 additions and 1 deletions
+14
View File
@@ -110,11 +110,16 @@ Type Objects
:c:func:`!_PyType_Lookup` is not called on *type* between the modifications;
this is an implementation detail and subject to change.)
The callback is also invoked when a watched heap type is deallocated.
An extension should never call ``PyType_Watch`` with a *watcher_id* that was
not returned to it by a previous call to :c:func:`PyType_AddWatcher`.
.. versionadded:: 3.12
.. versionchanged:: 3.15
The callback is now also invoked when a watched heap type is deallocated.
.. c:function:: int PyType_Unwatch(int watcher_id, PyObject *type)
@@ -138,8 +143,17 @@ Type Objects
called on *type* or any type in its MRO; violating this rule could cause
infinite recursion.
The callback may be called during type deallocation. In this case, the type
object is temporarily resurrected (its reference count is at least 1) and all
its attributes are still valid. However, the callback should not store new
strong references to the type, as this would resurrect the object and prevent
its deallocation.
.. versionadded:: 3.12
.. versionchanged:: 3.15
The callback may now be called during deallocation of a watched heap type.
.. c:function:: int PyType_HasFeature(PyTypeObject *o, int feature)
+22
View File
@@ -208,6 +208,7 @@ class TestTypeWatchers(unittest.TestCase):
TYPES = 0 # appends modified types to global event list
ERROR = 1 # unconditionally sets and signals a RuntimeException
WRAP = 2 # appends modified type wrapped in list to global event list
NAME = 3 # appends type name (string) to global event list
# duplicating the C constant
TYPE_MAX_WATCHERS = 8
@@ -377,6 +378,27 @@ class TestTypeWatchers(unittest.TestCase):
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
self.clear_watcher(1)
def test_watch_type_dealloc(self):
# Use the NAME watcher (kind=3) which records the type's name as a
# string, avoiding any reference to the type object itself during
# deallocation.
with self.watcher(kind=self.NAME) as wid:
class MyTestType: pass
self.watch(wid, MyTestType)
del MyTestType
gc_collect()
events = _testcapi.get_type_modified_events()
self.assertIn("MyTestType", events)
def test_watch_type_dealloc_error(self):
with self.watcher(kind=self.ERROR) as wid:
class MyTestType2: pass
self.watch(wid, MyTestType2)
with catch_unraisable_exception() as cm:
del MyTestType2
gc_collect()
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
def test_no_more_ids_available(self):
with self.assertRaisesRegex(RuntimeError, r"no more type watcher IDs"):
with ExitStack() as stack:
@@ -0,0 +1,5 @@
:c:type:`PyType_WatchCallback` callbacks registered via
:c:func:`PyType_AddWatcher` are now also invoked when a watched heap type is
deallocated. Previously, type watchers were only notified of modifications,
which could cause stale references when a type was freed and its address was
reused.
+20 -1
View File
@@ -212,13 +212,32 @@ type_modified_callback_error(PyTypeObject *type)
return -1;
}
static int
type_modified_callback_name(PyTypeObject *type)
{
assert(PyList_Check(g_type_modified_events));
PyObject *name = PyUnicode_FromString(type->tp_name);
if (name == NULL) {
return -1;
}
if (PyList_Append(g_type_modified_events, name) < 0) {
Py_DECREF(name);
return -1;
}
Py_DECREF(name);
return 0;
}
static PyObject *
add_type_watcher(PyObject *self, PyObject *kind)
{
int watcher_id;
assert(PyLong_Check(kind));
long kind_l = PyLong_AsLong(kind);
if (kind_l == 2) {
if (kind_l == 3) {
watcher_id = PyType_AddWatcher(type_modified_callback_name);
}
else if (kind_l == 2) {
watcher_id = PyType_AddWatcher(type_modified_callback_wrap);
}
else if (kind_l == 1) {
+27
View File
@@ -6940,6 +6940,33 @@ type_dealloc(PyObject *self)
// Assert this is a heap-allocated type object
_PyObject_ASSERT((PyObject *)type, type->tp_flags & Py_TPFLAGS_HEAPTYPE);
// Notify type watchers before teardown. The type object is still fully
// intact at this point (dict, bases, mro, name are all valid), so
// callbacks can safely inspect it.
if (type->tp_watched) {
_PyObject_ResurrectStart(self);
PyInterpreterState *interp = _PyInterpreterState_GET();
int bits = type->tp_watched;
int i = 0;
while (bits) {
assert(i < TYPE_MAX_WATCHERS);
if (bits & 1) {
PyType_WatchCallback cb = interp->type_watchers[i];
if (cb && (cb(type) < 0)) {
PyErr_FormatUnraisable(
"Exception ignored in type watcher callback #%d "
"for %R",
i, type);
}
}
i++;
bits >>= 1;
}
if (_PyObject_ResurrectEnd(self)) {
return; // callback resurrected the object
}
}
_PyObject_GC_UNTRACK(type);
type_dealloc_common(type);