diff options
author | Kirill Podoprigora <kirill.bast9@mail.ru> | 2024-10-15 14:42:16 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-15 14:42:16 (GMT) |
commit | d3c82b9ccedd77fc302f5ab8ab0220b3372f574c (patch) | |
tree | ee15c5f380dadeac8ee875abbc73b20f8067a12e | |
parent | 55c4f4c30b49734ce35dc88139b8b4fdc94c66fd (diff) | |
download | cpython-d3c82b9ccedd77fc302f5ab8ab0220b3372f574c.zip cpython-d3c82b9ccedd77fc302f5ab8ab0220b3372f574c.tar.gz cpython-d3c82b9ccedd77fc302f5ab8ab0220b3372f574c.tar.bz2 |
gh-125512: Revert "gh-124872: Replace enter/exit events with "switched" (#124776)" (#125513)
-rw-r--r-- | Doc/c-api/contextvars.rst | 14 | ||||
-rw-r--r-- | Include/cpython/context.h | 17 | ||||
-rw-r--r-- | Lib/test/test_capi/test_watchers.py | 89 | ||||
-rw-r--r-- | Modules/_testcapi/watchers.c | 79 | ||||
-rw-r--r-- | Python/context.c | 31 | ||||
-rw-r--r-- | Tools/c-analyzer/cpython/ignored.tsv | 4 |
6 files changed, 117 insertions, 117 deletions
diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index b7c6550..8eba54a 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -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 diff --git a/Include/cpython/context.h b/Include/cpython/context.h index 3a7a4b4..3c9be78 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -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; /* diff --git a/Lib/test/test_capi/test_watchers.py b/Lib/test/test_capi/test_watchers.py index 4680d67..f21d262 100644 --- a/Lib/test/test_capi/test_watchers.py +++ b/Lib/test/test_capi/test_watchers.py @@ -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) - - def test_callback_error(self): - ctx_outer = contextvars.copy_context() - ctx_inner = contextvars.copy_context() - unraisables = [] - - 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) - - 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!"]) + ctx.run(self.assert_event_counts, 0, 0, 0, 0) + self.assert_event_counts(0, 0, 0, 0) + + 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 test_exit_error(self): + ctx = contextvars.copy_context() + def _in_context(stack): + stack.enter_context(self.context_watcher(2)) + + 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() diff --git a/Modules/_testcapi/watchers.c b/Modules/_testcapi/watchers.c index 321d3ae..b4233d0 100644 --- a/Modules/_testcapi/watchers.c +++ b/Modules/_testcapi/watchers.c @@ -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)) +get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id) { - 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; + 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_enter_events[watcher_id_l]); } static PyObject * -get_context_switches(PyObject *Py_UNUSED(self), PyObject *watcher_id) +get_context_watcher_num_exit_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_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}, diff --git a/Python/context.c b/Python/context.c index 95aa822..8bc487a 100644 --- a/Python/context.c +++ b/Python/context.c @@ -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; } diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 2605825..e6c599a 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -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 - |