mirror of https://github.com/python/cpython
gh-124872: Replace enter/exit events with "switched" (#124776)
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for gh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
This commit is contained in:
parent
e99650b80a
commit
843d28f59d
|
@ -123,16 +123,10 @@ Context object management functions:
|
||||||
|
|
||||||
Enumeration of possible context object watcher events:
|
Enumeration of possible context object watcher events:
|
||||||
|
|
||||||
- ``Py_CONTEXT_EVENT_ENTER``: A context has been entered, causing the
|
- ``Py_CONTEXT_SWITCHED``: The :term:`current context` has switched to a
|
||||||
:term:`current context` to switch to it. The object passed to the watch
|
different context. The object passed to the watch callback is the
|
||||||
callback is the now-current :class:`contextvars.Context` object. Each
|
now-current :class:`contextvars.Context` object, or None if no context is
|
||||||
enter event will eventually have a corresponding exit event for the same
|
current.
|
||||||
context object after any subsequently entered contexts have themselves been
|
|
||||||
exited.
|
|
||||||
- ``Py_CONTEXT_EVENT_EXIT``: A context is about to be exited, which will
|
|
||||||
cause the :term:`current context` to switch back to what it was before the
|
|
||||||
context was entered. The object passed to the watch callback is the
|
|
||||||
still-current :class:`contextvars.Context` object.
|
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
|
@ -29,20 +29,11 @@ PyAPI_FUNC(int) PyContext_Exit(PyObject *);
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
/*
|
/*
|
||||||
* A context has been entered, causing the "current context" to switch to
|
* The current context has switched to a different context. The object
|
||||||
* it. The object passed to the watch callback is the now-current
|
* passed to the watch callback is the now-current contextvars.Context
|
||||||
* contextvars.Context object. Each enter event will eventually have a
|
* object, or None if no context is current.
|
||||||
* corresponding exit event for the same context object after any
|
|
||||||
* subsequently entered contexts have themselves been exited.
|
|
||||||
*/
|
*/
|
||||||
Py_CONTEXT_EVENT_ENTER,
|
Py_CONTEXT_SWITCHED = 1,
|
||||||
/*
|
|
||||||
* A context is about to be exited, which will cause the "current context"
|
|
||||||
* to switch back to what it was before the context was entered. The
|
|
||||||
* object passed to the watch callback is the still-current
|
|
||||||
* contextvars.Context object.
|
|
||||||
*/
|
|
||||||
Py_CONTEXT_EVENT_EXIT,
|
|
||||||
} PyContextEvent;
|
} PyContextEvent;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -577,68 +577,62 @@ class TestContextObjectWatchers(unittest.TestCase):
|
||||||
def context_watcher(self, which_watcher):
|
def context_watcher(self, which_watcher):
|
||||||
wid = _testcapi.add_context_watcher(which_watcher)
|
wid = _testcapi.add_context_watcher(which_watcher)
|
||||||
try:
|
try:
|
||||||
yield wid
|
switches = _testcapi.get_context_switches(which_watcher)
|
||||||
|
except ValueError:
|
||||||
|
switches = None
|
||||||
|
try:
|
||||||
|
yield switches
|
||||||
finally:
|
finally:
|
||||||
_testcapi.clear_context_watcher(wid)
|
_testcapi.clear_context_watcher(wid)
|
||||||
|
|
||||||
def assert_event_counts(self, exp_enter_0, exp_exit_0,
|
def assert_event_counts(self, want_0, want_1):
|
||||||
exp_enter_1, exp_exit_1):
|
self.assertEqual(len(_testcapi.get_context_switches(0)), want_0)
|
||||||
self.assertEqual(
|
self.assertEqual(len(_testcapi.get_context_switches(1)), want_1)
|
||||||
exp_enter_0, _testcapi.get_context_watcher_num_enter_events(0))
|
|
||||||
self.assertEqual(
|
|
||||||
exp_exit_0, _testcapi.get_context_watcher_num_exit_events(0))
|
|
||||||
self.assertEqual(
|
|
||||||
exp_enter_1, _testcapi.get_context_watcher_num_enter_events(1))
|
|
||||||
self.assertEqual(
|
|
||||||
exp_exit_1, _testcapi.get_context_watcher_num_exit_events(1))
|
|
||||||
|
|
||||||
def test_context_object_events_dispatched(self):
|
def test_context_object_events_dispatched(self):
|
||||||
# verify that all counts are zero before any watchers are registered
|
# verify that all counts are zero before any watchers are registered
|
||||||
self.assert_event_counts(0, 0, 0, 0)
|
self.assert_event_counts(0, 0)
|
||||||
|
|
||||||
# verify that all counts remain zero when a context object is
|
# verify that all counts remain zero when a context object is
|
||||||
# entered and exited with no watchers registered
|
# entered and exited with no watchers registered
|
||||||
ctx = contextvars.copy_context()
|
ctx = contextvars.copy_context()
|
||||||
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
|
ctx.run(self.assert_event_counts, 0, 0)
|
||||||
self.assert_event_counts(0, 0, 0, 0)
|
self.assert_event_counts(0, 0)
|
||||||
|
|
||||||
# verify counts are as expected when first watcher is registered
|
# verify counts are as expected when first watcher is registered
|
||||||
with self.context_watcher(0):
|
with self.context_watcher(0):
|
||||||
self.assert_event_counts(0, 0, 0, 0)
|
self.assert_event_counts(0, 0)
|
||||||
ctx.run(self.assert_event_counts, 1, 0, 0, 0)
|
ctx.run(self.assert_event_counts, 1, 0)
|
||||||
self.assert_event_counts(1, 1, 0, 0)
|
self.assert_event_counts(2, 0)
|
||||||
|
|
||||||
# again with second watcher registered
|
# again with second watcher registered
|
||||||
with self.context_watcher(1):
|
with self.context_watcher(1):
|
||||||
self.assert_event_counts(1, 1, 0, 0)
|
self.assert_event_counts(2, 0)
|
||||||
ctx.run(self.assert_event_counts, 2, 1, 1, 0)
|
ctx.run(self.assert_event_counts, 3, 1)
|
||||||
self.assert_event_counts(2, 2, 1, 1)
|
self.assert_event_counts(4, 2)
|
||||||
|
|
||||||
# verify counts are reset and don't change after both watchers are cleared
|
# verify counts are reset and don't change after both watchers are cleared
|
||||||
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
|
ctx.run(self.assert_event_counts, 0, 0)
|
||||||
self.assert_event_counts(0, 0, 0, 0)
|
self.assert_event_counts(0, 0)
|
||||||
|
|
||||||
def test_enter_error(self):
|
def test_callback_error(self):
|
||||||
with self.context_watcher(2):
|
ctx_outer = contextvars.copy_context()
|
||||||
with catch_unraisable_exception() as cm:
|
ctx_inner = contextvars.copy_context()
|
||||||
ctx = contextvars.copy_context()
|
unraisables = []
|
||||||
ctx.run(int, 0)
|
|
||||||
self.assertEqual(
|
|
||||||
cm.unraisable.err_msg,
|
|
||||||
"Exception ignored in "
|
|
||||||
f"Py_CONTEXT_EVENT_EXIT watcher callback for {ctx!r}"
|
|
||||||
)
|
|
||||||
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
|
|
||||||
|
|
||||||
def test_exit_error(self):
|
def _in_outer():
|
||||||
ctx = contextvars.copy_context()
|
with self.context_watcher(2):
|
||||||
def _in_context(stack):
|
with catch_unraisable_exception() as cm:
|
||||||
stack.enter_context(self.context_watcher(2))
|
ctx_inner.run(lambda: unraisables.append(cm.unraisable))
|
||||||
|
unraisables.append(cm.unraisable)
|
||||||
|
|
||||||
with catch_unraisable_exception() as cm:
|
ctx_outer.run(_in_outer)
|
||||||
with ExitStack() as stack:
|
self.assertEqual([x.err_msg for x in unraisables],
|
||||||
ctx.run(_in_context, stack)
|
["Exception ignored in Py_CONTEXT_SWITCHED "
|
||||||
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
|
f"watcher callback for {ctx!r}"
|
||||||
|
for ctx in [ctx_inner, ctx_outer]])
|
||||||
|
self.assertEqual([str(x.exc_value) for x in unraisables],
|
||||||
|
["boom!", "boom!"])
|
||||||
|
|
||||||
def test_clear_out_of_range_watcher_id(self):
|
def test_clear_out_of_range_watcher_id(self):
|
||||||
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
|
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
|
||||||
|
@ -654,5 +648,12 @@ class TestContextObjectWatchers(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
|
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
|
||||||
_testcapi.allocate_too_many_context_watchers()
|
_testcapi.allocate_too_many_context_watchers()
|
||||||
|
|
||||||
|
def test_exit_base_context(self):
|
||||||
|
ctx = contextvars.Context()
|
||||||
|
_testcapi.clear_context_stack()
|
||||||
|
with self.context_watcher(0) as switches:
|
||||||
|
ctx.run(lambda: None)
|
||||||
|
self.assertEqual(switches, [ctx, None])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -626,16 +626,12 @@ allocate_too_many_func_watchers(PyObject *self, PyObject *args)
|
||||||
// Test contexct object watchers
|
// Test contexct object watchers
|
||||||
#define NUM_CONTEXT_WATCHERS 2
|
#define NUM_CONTEXT_WATCHERS 2
|
||||||
static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1};
|
static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1};
|
||||||
static int num_context_object_enter_events[NUM_CONTEXT_WATCHERS] = {0, 0};
|
static PyObject *context_switches[NUM_CONTEXT_WATCHERS];
|
||||||
static int num_context_object_exit_events[NUM_CONTEXT_WATCHERS] = {0, 0};
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *ctx) {
|
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *ctx) {
|
||||||
if (event == Py_CONTEXT_EVENT_ENTER) {
|
if (event == Py_CONTEXT_SWITCHED) {
|
||||||
num_context_object_enter_events[which_watcher]++;
|
PyList_Append(context_switches[which_watcher], ctx);
|
||||||
}
|
|
||||||
else if (event == Py_CONTEXT_EVENT_EXIT) {
|
|
||||||
num_context_object_exit_events[which_watcher]++;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -667,31 +663,28 @@ error_context_event_handler(PyContextEvent event, PyObject *ctx) {
|
||||||
static PyObject *
|
static PyObject *
|
||||||
add_context_watcher(PyObject *self, PyObject *which_watcher)
|
add_context_watcher(PyObject *self, PyObject *which_watcher)
|
||||||
{
|
{
|
||||||
int watcher_id;
|
static const PyContext_WatchCallback callbacks[] = {
|
||||||
|
&first_context_watcher_callback,
|
||||||
|
&second_context_watcher_callback,
|
||||||
|
&error_context_event_handler,
|
||||||
|
};
|
||||||
assert(PyLong_Check(which_watcher));
|
assert(PyLong_Check(which_watcher));
|
||||||
long which_l = PyLong_AsLong(which_watcher);
|
long which_l = PyLong_AsLong(which_watcher);
|
||||||
if (which_l == 0) {
|
if (which_l < 0 || which_l >= (long)Py_ARRAY_LENGTH(callbacks)) {
|
||||||
watcher_id = PyContext_AddWatcher(first_context_watcher_callback);
|
|
||||||
context_watcher_ids[0] = watcher_id;
|
|
||||||
num_context_object_enter_events[0] = 0;
|
|
||||||
num_context_object_exit_events[0] = 0;
|
|
||||||
}
|
|
||||||
else if (which_l == 1) {
|
|
||||||
watcher_id = PyContext_AddWatcher(second_context_watcher_callback);
|
|
||||||
context_watcher_ids[1] = watcher_id;
|
|
||||||
num_context_object_enter_events[1] = 0;
|
|
||||||
num_context_object_exit_events[1] = 0;
|
|
||||||
}
|
|
||||||
else if (which_l == 2) {
|
|
||||||
watcher_id = PyContext_AddWatcher(error_context_event_handler);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
|
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
int watcher_id = PyContext_AddWatcher(callbacks[which_l]);
|
||||||
if (watcher_id < 0) {
|
if (watcher_id < 0) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
if (which_l >= 0 && which_l < NUM_CONTEXT_WATCHERS) {
|
||||||
|
context_watcher_ids[which_l] = watcher_id;
|
||||||
|
Py_XSETREF(context_switches[which_l], PyList_New(0));
|
||||||
|
if (context_switches[which_l] == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
return PyLong_FromLong(watcher_id);
|
return PyLong_FromLong(watcher_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -708,8 +701,7 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
|
||||||
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
|
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
|
||||||
if (watcher_id_l == context_watcher_ids[i]) {
|
if (watcher_id_l == context_watcher_ids[i]) {
|
||||||
context_watcher_ids[i] = -1;
|
context_watcher_ids[i] = -1;
|
||||||
num_context_object_enter_events[i] = 0;
|
Py_CLEAR(context_switches[i]);
|
||||||
num_context_object_exit_events[i] = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -717,21 +709,34 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id)
|
clear_context_stack(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
|
||||||
{
|
{
|
||||||
assert(PyLong_Check(watcher_id));
|
PyThreadState *tstate = PyThreadState_Get();
|
||||||
long watcher_id_l = PyLong_AsLong(watcher_id);
|
if (tstate->context == NULL) {
|
||||||
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
|
Py_RETURN_NONE;
|
||||||
return PyLong_FromLong(num_context_object_enter_events[watcher_id_l]);
|
}
|
||||||
|
if (((PyContext *)tstate->context)->ctx_prev != NULL) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError,
|
||||||
|
"must first exit all non-base contexts");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_CLEAR(tstate->context);
|
||||||
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id)
|
get_context_switches(PyObject *Py_UNUSED(self), PyObject *watcher_id)
|
||||||
{
|
{
|
||||||
assert(PyLong_Check(watcher_id));
|
assert(PyLong_Check(watcher_id));
|
||||||
long watcher_id_l = PyLong_AsLong(watcher_id);
|
long watcher_id_l = PyLong_AsLong(watcher_id);
|
||||||
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
|
if (watcher_id_l < 0 || watcher_id_l >= NUM_CONTEXT_WATCHERS) {
|
||||||
return PyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
|
PyErr_Format(PyExc_ValueError, "invalid watcher %ld", watcher_id_l);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (context_switches[watcher_id_l] == NULL) {
|
||||||
|
return PyList_New(0);
|
||||||
|
}
|
||||||
|
return Py_NewRef(context_switches[watcher_id_l]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PyObject *
|
static PyObject *
|
||||||
|
@ -835,10 +840,8 @@ static PyMethodDef test_methods[] = {
|
||||||
// Code object watchers.
|
// Code object watchers.
|
||||||
{"add_context_watcher", add_context_watcher, METH_O, NULL},
|
{"add_context_watcher", add_context_watcher, METH_O, NULL},
|
||||||
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
|
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
|
||||||
{"get_context_watcher_num_enter_events",
|
{"clear_context_stack", clear_context_stack, METH_NOARGS, NULL},
|
||||||
get_context_watcher_num_enter_events, METH_O, NULL},
|
{"get_context_switches", get_context_switches, METH_O, NULL},
|
||||||
{"get_context_watcher_num_exit_events",
|
|
||||||
get_context_watcher_num_exit_events, METH_O, NULL},
|
|
||||||
{"allocate_too_many_context_watchers",
|
{"allocate_too_many_context_watchers",
|
||||||
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
|
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
|
||||||
{NULL},
|
{NULL},
|
||||||
|
|
|
@ -102,10 +102,8 @@ PyContext_CopyCurrent(void)
|
||||||
static const char *
|
static const char *
|
||||||
context_event_name(PyContextEvent event) {
|
context_event_name(PyContextEvent event) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case Py_CONTEXT_EVENT_ENTER:
|
case Py_CONTEXT_SWITCHED:
|
||||||
return "Py_CONTEXT_EVENT_ENTER";
|
return "Py_CONTEXT_SWITCHED";
|
||||||
case Py_CONTEXT_EVENT_EXIT:
|
|
||||||
return "Py_CONTEXT_EVENT_EXIT";
|
|
||||||
default:
|
default:
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
@ -115,6 +113,13 @@ context_event_name(PyContextEvent event) {
|
||||||
static void
|
static void
|
||||||
notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx)
|
notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx)
|
||||||
{
|
{
|
||||||
|
if (ctx == NULL) {
|
||||||
|
// This will happen after exiting the last context in the stack, which
|
||||||
|
// can occur if context_get was never called before entering a context
|
||||||
|
// (e.g., called `contextvars.Context().run()` on a fresh thread, as
|
||||||
|
// PyContext_Enter doesn't call context_get).
|
||||||
|
ctx = Py_None;
|
||||||
|
}
|
||||||
assert(Py_REFCNT(ctx) > 0);
|
assert(Py_REFCNT(ctx) > 0);
|
||||||
PyInterpreterState *interp = ts->interp;
|
PyInterpreterState *interp = ts->interp;
|
||||||
assert(interp->_initialized);
|
assert(interp->_initialized);
|
||||||
|
@ -175,6 +180,16 @@ PyContext_ClearWatcher(int watcher_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
context_switched(PyThreadState *ts)
|
||||||
|
{
|
||||||
|
ts->context_ver++;
|
||||||
|
// ts->context is used instead of context_get() because context_get() might
|
||||||
|
// throw if ts->context is NULL.
|
||||||
|
notify_context_watchers(ts, Py_CONTEXT_SWITCHED, ts->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
_PyContext_Enter(PyThreadState *ts, PyObject *octx)
|
_PyContext_Enter(PyThreadState *ts, PyObject *octx)
|
||||||
{
|
{
|
||||||
|
@ -191,9 +206,7 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx)
|
||||||
ctx->ctx_entered = 1;
|
ctx->ctx_entered = 1;
|
||||||
|
|
||||||
ts->context = Py_NewRef(ctx);
|
ts->context = Py_NewRef(ctx);
|
||||||
ts->context_ver++;
|
context_switched(ts);
|
||||||
|
|
||||||
notify_context_watchers(ts, Py_CONTEXT_EVENT_ENTER, octx);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,13 +240,11 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_context_watchers(ts, Py_CONTEXT_EVENT_EXIT, octx);
|
|
||||||
Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev);
|
Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev);
|
||||||
ts->context_ver++;
|
|
||||||
|
|
||||||
ctx->ctx_prev = NULL;
|
ctx->ctx_prev = NULL;
|
||||||
ctx->ctx_entered = 0;
|
ctx->ctx_entered = 0;
|
||||||
|
context_switched(ts);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -455,8 +455,8 @@ Modules/_testcapi/watchers.c - pyfunc_watchers -
|
||||||
Modules/_testcapi/watchers.c - func_watcher_ids -
|
Modules/_testcapi/watchers.c - func_watcher_ids -
|
||||||
Modules/_testcapi/watchers.c - func_watcher_callbacks -
|
Modules/_testcapi/watchers.c - func_watcher_callbacks -
|
||||||
Modules/_testcapi/watchers.c - context_watcher_ids -
|
Modules/_testcapi/watchers.c - context_watcher_ids -
|
||||||
Modules/_testcapi/watchers.c - num_context_object_enter_events -
|
Modules/_testcapi/watchers.c - context_switches -
|
||||||
Modules/_testcapi/watchers.c - num_context_object_exit_events -
|
Modules/_testcapi/watchers.c add_context_watcher callbacks -
|
||||||
Modules/_testcapimodule.c - BasicStaticTypes -
|
Modules/_testcapimodule.c - BasicStaticTypes -
|
||||||
Modules/_testcapimodule.c - num_basic_static_types_used -
|
Modules/_testcapimodule.c - num_basic_static_types_used -
|
||||||
Modules/_testcapimodule.c - ContainerNoGC_members -
|
Modules/_testcapimodule.c - ContainerNoGC_members -
|
||||||
|
|
Can't render this file because it has a wrong number of fields in line 4.
|
Loading…
Reference in New Issue