Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -377,6 +378,27 @@ def test_clear_unassigned_watcher_id(self):
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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 20 additions & 1 deletion Modules/_testcapi/watchers.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -6879,6 +6879,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);

Expand Down
Loading