mirror of https://github.com/python/cpython
This commit is contained in:
parent
55c4f4c30b
commit
d3c82b9cce
|
@ -123,10 +123,16 @@ Context object management functions:
|
|||
|
||||
Enumeration of possible context object watcher events:
|
||||
|
||||
- ``Py_CONTEXT_SWITCHED``: The :term:`current context` has switched to a
|
||||
different context. The object passed to the watch callback is the
|
||||
now-current :class:`contextvars.Context` object, or None if no context is
|
||||
current.
|
||||
- ``Py_CONTEXT_EVENT_ENTER``: A context has been entered, causing the
|
||||
:term:`current context` to switch to it. The object passed to the watch
|
||||
callback is the now-current :class:`contextvars.Context` object. Each
|
||||
enter event will eventually have a corresponding exit event for the same
|
||||
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
|
||||
|
||||
|
|
|
@ -29,11 +29,20 @@ PyAPI_FUNC(int) PyContext_Exit(PyObject *);
|
|||
|
||||
typedef enum {
|
||||
/*
|
||||
* The current context has switched to a different context. The object
|
||||
* passed to the watch callback is the now-current contextvars.Context
|
||||
* object, or None if no context is current.
|
||||
* A context has been entered, causing the "current context" to switch to
|
||||
* it. The object passed to the watch callback is the now-current
|
||||
* contextvars.Context object. Each enter event will eventually have a
|
||||
* corresponding exit event for the same context object after any
|
||||
* subsequently entered contexts have themselves been exited.
|
||||
*/
|
||||
Py_CONTEXT_SWITCHED = 1,
|
||||
Py_CONTEXT_EVENT_ENTER,
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/*
|
||||
|
|
|
@ -577,62 +577,68 @@ class TestContextObjectWatchers(unittest.TestCase):
|
|||
def context_watcher(self, which_watcher):
|
||||
wid = _testcapi.add_context_watcher(which_watcher)
|
||||
try:
|
||||
switches = _testcapi.get_context_switches(which_watcher)
|
||||
except ValueError:
|
||||
switches = None
|
||||
try:
|
||||
yield switches
|
||||
yield wid
|
||||
finally:
|
||||
_testcapi.clear_context_watcher(wid)
|
||||
|
||||
def assert_event_counts(self, want_0, want_1):
|
||||
self.assertEqual(len(_testcapi.get_context_switches(0)), want_0)
|
||||
self.assertEqual(len(_testcapi.get_context_switches(1)), want_1)
|
||||
def assert_event_counts(self, exp_enter_0, exp_exit_0,
|
||||
exp_enter_1, exp_exit_1):
|
||||
self.assertEqual(
|
||||
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):
|
||||
# verify that all counts are zero before any watchers are registered
|
||||
self.assert_event_counts(0, 0)
|
||||
self.assert_event_counts(0, 0, 0, 0)
|
||||
|
||||
# verify that all counts remain zero when a context object is
|
||||
# entered and exited with no watchers registered
|
||||
ctx = contextvars.copy_context()
|
||||
ctx.run(self.assert_event_counts, 0, 0)
|
||||
self.assert_event_counts(0, 0)
|
||||
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
|
||||
self.assert_event_counts(0, 0, 0, 0)
|
||||
|
||||
# verify counts are as expected when first watcher is registered
|
||||
with self.context_watcher(0):
|
||||
self.assert_event_counts(0, 0)
|
||||
ctx.run(self.assert_event_counts, 1, 0)
|
||||
self.assert_event_counts(2, 0)
|
||||
self.assert_event_counts(0, 0, 0, 0)
|
||||
ctx.run(self.assert_event_counts, 1, 0, 0, 0)
|
||||
self.assert_event_counts(1, 1, 0, 0)
|
||||
|
||||
# again with second watcher registered
|
||||
with self.context_watcher(1):
|
||||
self.assert_event_counts(2, 0)
|
||||
ctx.run(self.assert_event_counts, 3, 1)
|
||||
self.assert_event_counts(4, 2)
|
||||
self.assert_event_counts(1, 1, 0, 0)
|
||||
ctx.run(self.assert_event_counts, 2, 1, 1, 0)
|
||||
self.assert_event_counts(2, 2, 1, 1)
|
||||
|
||||
# verify counts are reset and don't change after both watchers are cleared
|
||||
ctx.run(self.assert_event_counts, 0, 0)
|
||||
self.assert_event_counts(0, 0)
|
||||
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
|
||||
self.assert_event_counts(0, 0, 0, 0)
|
||||
|
||||
def test_callback_error(self):
|
||||
ctx_outer = contextvars.copy_context()
|
||||
ctx_inner = contextvars.copy_context()
|
||||
unraisables = []
|
||||
def test_enter_error(self):
|
||||
with self.context_watcher(2):
|
||||
with catch_unraisable_exception() as cm:
|
||||
ctx = contextvars.copy_context()
|
||||
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 _in_outer():
|
||||
with self.context_watcher(2):
|
||||
with catch_unraisable_exception() as cm:
|
||||
ctx_inner.run(lambda: unraisables.append(cm.unraisable))
|
||||
unraisables.append(cm.unraisable)
|
||||
def test_exit_error(self):
|
||||
ctx = contextvars.copy_context()
|
||||
def _in_context(stack):
|
||||
stack.enter_context(self.context_watcher(2))
|
||||
|
||||
ctx_outer.run(_in_outer)
|
||||
self.assertEqual([x.err_msg for x in unraisables],
|
||||
["Exception ignored in Py_CONTEXT_SWITCHED "
|
||||
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!"])
|
||||
with catch_unraisable_exception() as cm:
|
||||
with ExitStack() as stack:
|
||||
ctx.run(_in_context, stack)
|
||||
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
|
||||
|
||||
def test_clear_out_of_range_watcher_id(self):
|
||||
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
|
||||
|
@ -648,12 +654,5 @@ class TestContextObjectWatchers(unittest.TestCase):
|
|||
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
|
||||
_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__":
|
||||
unittest.main()
|
||||
|
|
|
@ -626,12 +626,16 @@ allocate_too_many_func_watchers(PyObject *self, PyObject *args)
|
|||
// Test contexct object watchers
|
||||
#define NUM_CONTEXT_WATCHERS 2
|
||||
static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1};
|
||||
static PyObject *context_switches[NUM_CONTEXT_WATCHERS];
|
||||
static int num_context_object_enter_events[NUM_CONTEXT_WATCHERS] = {0, 0};
|
||||
static int num_context_object_exit_events[NUM_CONTEXT_WATCHERS] = {0, 0};
|
||||
|
||||
static int
|
||||
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *ctx) {
|
||||
if (event == Py_CONTEXT_SWITCHED) {
|
||||
PyList_Append(context_switches[which_watcher], ctx);
|
||||
if (event == Py_CONTEXT_EVENT_ENTER) {
|
||||
num_context_object_enter_events[which_watcher]++;
|
||||
}
|
||||
else if (event == Py_CONTEXT_EVENT_EXIT) {
|
||||
num_context_object_exit_events[which_watcher]++;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
|
@ -663,28 +667,31 @@ error_context_event_handler(PyContextEvent event, PyObject *ctx) {
|
|||
static PyObject *
|
||||
add_context_watcher(PyObject *self, PyObject *which_watcher)
|
||||
{
|
||||
static const PyContext_WatchCallback callbacks[] = {
|
||||
&first_context_watcher_callback,
|
||||
&second_context_watcher_callback,
|
||||
&error_context_event_handler,
|
||||
};
|
||||
int watcher_id;
|
||||
assert(PyLong_Check(which_watcher));
|
||||
long which_l = PyLong_AsLong(which_watcher);
|
||||
if (which_l < 0 || which_l >= (long)Py_ARRAY_LENGTH(callbacks)) {
|
||||
if (which_l == 0) {
|
||||
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);
|
||||
return NULL;
|
||||
}
|
||||
int watcher_id = PyContext_AddWatcher(callbacks[which_l]);
|
||||
if (watcher_id < 0) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -701,7 +708,8 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
|
|||
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
|
||||
if (watcher_id_l == context_watcher_ids[i]) {
|
||||
context_watcher_ids[i] = -1;
|
||||
Py_CLEAR(context_switches[i]);
|
||||
num_context_object_enter_events[i] = 0;
|
||||
num_context_object_exit_events[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -709,34 +717,21 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
|
|||
}
|
||||
|
||||
static PyObject *
|
||||
clear_context_stack(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
|
||||
{
|
||||
PyThreadState *tstate = PyThreadState_Get();
|
||||
if (tstate->context == NULL) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
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 *
|
||||
get_context_switches(PyObject *Py_UNUSED(self), PyObject *watcher_id)
|
||||
get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id)
|
||||
{
|
||||
assert(PyLong_Check(watcher_id));
|
||||
long watcher_id_l = PyLong_AsLong(watcher_id);
|
||||
if (watcher_id_l < 0 || watcher_id_l >= NUM_CONTEXT_WATCHERS) {
|
||||
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]);
|
||||
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
|
||||
return PyLong_FromLong(num_context_object_enter_events[watcher_id_l]);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id)
|
||||
{
|
||||
assert(PyLong_Check(watcher_id));
|
||||
long watcher_id_l = PyLong_AsLong(watcher_id);
|
||||
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
|
||||
return PyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
|
@ -840,8 +835,10 @@ static PyMethodDef test_methods[] = {
|
|||
// Code object watchers.
|
||||
{"add_context_watcher", add_context_watcher, METH_O, NULL},
|
||||
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
|
||||
{"clear_context_stack", clear_context_stack, METH_NOARGS, NULL},
|
||||
{"get_context_switches", get_context_switches, METH_O, NULL},
|
||||
{"get_context_watcher_num_enter_events",
|
||||
get_context_watcher_num_enter_events, METH_O, NULL},
|
||||
{"get_context_watcher_num_exit_events",
|
||||
get_context_watcher_num_exit_events, METH_O, NULL},
|
||||
{"allocate_too_many_context_watchers",
|
||||
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
|
||||
{NULL},
|
||||
|
|
|
@ -102,8 +102,10 @@ PyContext_CopyCurrent(void)
|
|||
static const char *
|
||||
context_event_name(PyContextEvent event) {
|
||||
switch (event) {
|
||||
case Py_CONTEXT_SWITCHED:
|
||||
return "Py_CONTEXT_SWITCHED";
|
||||
case Py_CONTEXT_EVENT_ENTER:
|
||||
return "Py_CONTEXT_EVENT_ENTER";
|
||||
case Py_CONTEXT_EVENT_EXIT:
|
||||
return "Py_CONTEXT_EVENT_EXIT";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
|
@ -113,13 +115,6 @@ context_event_name(PyContextEvent event) {
|
|||
static void
|
||||
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);
|
||||
PyInterpreterState *interp = ts->interp;
|
||||
assert(interp->_initialized);
|
||||
|
@ -180,16 +175,6 @@ 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
|
||||
_PyContext_Enter(PyThreadState *ts, PyObject *octx)
|
||||
{
|
||||
|
@ -206,7 +191,9 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx)
|
|||
ctx->ctx_entered = 1;
|
||||
|
||||
ts->context = Py_NewRef(ctx);
|
||||
context_switched(ts);
|
||||
ts->context_ver++;
|
||||
|
||||
notify_context_watchers(ts, Py_CONTEXT_EVENT_ENTER, octx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -240,11 +227,13 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx)
|
|||
return -1;
|
||||
}
|
||||
|
||||
notify_context_watchers(ts, Py_CONTEXT_EVENT_EXIT, octx);
|
||||
Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev);
|
||||
ts->context_ver++;
|
||||
|
||||
ctx->ctx_prev = NULL;
|
||||
ctx->ctx_entered = 0;
|
||||
context_switched(ts);
|
||||
|
||||
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_callbacks -
|
||||
Modules/_testcapi/watchers.c - context_watcher_ids -
|
||||
Modules/_testcapi/watchers.c - context_switches -
|
||||
Modules/_testcapi/watchers.c add_context_watcher callbacks -
|
||||
Modules/_testcapi/watchers.c - num_context_object_enter_events -
|
||||
Modules/_testcapi/watchers.c - num_context_object_exit_events -
|
||||
Modules/_testcapimodule.c - BasicStaticTypes -
|
||||
Modules/_testcapimodule.c - num_basic_static_types_used -
|
||||
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