]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-125512: Revert "gh-124872: Replace enter/exit events with "switched" (#124776...
authorKirill Podoprigora <kirill.bast9@mail.ru>
Tue, 15 Oct 2024 14:42:16 +0000 (17:42 +0300)
committerGitHub <noreply@github.com>
Tue, 15 Oct 2024 14:42:16 +0000 (17:42 +0300)
Doc/c-api/contextvars.rst
Include/cpython/context.h
Lib/test/test_capi/test_watchers.py
Modules/_testcapi/watchers.c
Python/context.c
Tools/c-analyzer/cpython/ignored.tsv

index b7c6550ff34aac1e4dd6ef5d26f0e77b085dee20..8eba54a80dc80d47614a9bfa65430963374b3e90 100644 (file)
@@ -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
 
index 3a7a4b459c09ad04d0dcd469b3cc556c36bf390f..3c9be7873b939900119d1be5310fd9c64456f4d6 100644 (file)
@@ -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;
 
 /*
index 4680d6765de122bb3f4efc2ac185f0fafe833697..f21d2627c6094b6be9233f08bc9c4ce2def4ded0 100644 (file)
@@ -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()
index 321d3aeffb6ad18f364aaeb9dc6336508c6b1084..b4233d07134aea846b8878787c0c80d2cbddba73 100644 (file)
@@ -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},
index 95aa82206270f9829ff175e957a310a32ca22752..8bc487a33c890bfd305635e86c36148d736d3399 100644 (file)
@@ -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;
 }
 
index 2605825d3d007804a20004490a0433a9dfb0bc59..e6c599a2ac4a464a2011bd363c71f8133c82c37b 100644 (file)
@@ -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   -