diff --git a/Doc/c-api/contextvars.rst b/Doc/c-api/contextvars.rst index 8eba54a80dc80d..a1e06221bd6e9e 100644 --- a/Doc/c-api/contextvars.rst +++ b/Doc/c-api/contextvars.rst @@ -136,20 +136,16 @@ Context object management functions: .. versionadded:: 3.14 -.. c:type:: int (*PyContext_WatchCallback)(PyContextEvent event, PyObject *obj) +.. c:type:: void (*PyContext_WatchCallback)(PyContextEvent event, PyObject *obj) Context object watcher callback function. The object passed to the callback is event-specific; see :c:type:`PyContextEvent` for details. - If the callback returns with an exception set, it must return ``-1``; this - exception will be printed as an unraisable exception using - :c:func:`PyErr_FormatUnraisable`. Otherwise it should return ``0``. + Any pending exception is cleared before the callback is called and restored + after the callback returns. - There may already be a pending exception set on entry to the callback. In - this case, the callback should return ``0`` with the same exception still - set. This means the callback may not call any other API that can set an - exception unless it saves and clears the exception state first, and restores - it before returning. + If the callback raises an exception it will be printed as an unraisable + exception using :c:func:`PyErr_FormatUnraisable` and discarded. .. versionadded:: 3.14 diff --git a/Include/cpython/context.h b/Include/cpython/context.h index 3c9be7873b9399..4f8f5f5d3ea2c0 100644 --- a/Include/cpython/context.h +++ b/Include/cpython/context.h @@ -49,10 +49,13 @@ typedef enum { * Context object watcher callback function. The object passed to the callback * is event-specific; see PyContextEvent for details. * - * if the callback returns with an exception set, it must return -1. Otherwise - * it should return 0 + * Any pending exception is cleared before the callback is called and restored + * after the callback returns. + * + * If the callback raises an exception it will be printed as an unraisable + * exception using PyErr_FormatUnraisable and discarded. */ -typedef int (*PyContext_WatchCallback)(PyContextEvent, PyObject *); +typedef void (*PyContext_WatchCallback)(PyContextEvent, PyObject *); /* * Register a per-interpreter callback that will be invoked for context object diff --git a/Lib/test/test_capi/test_watchers.py b/Lib/test/test_capi/test_watchers.py index f21d2627c6094b..ec3fd0c64160a5 100644 --- a/Lib/test/test_capi/test_watchers.py +++ b/Lib/test/test_capi/test_watchers.py @@ -640,6 +640,16 @@ def _in_context(stack): ctx.run(_in_context, stack) self.assertEqual(str(cm.unraisable.exc_value), "boom!") + def test_exception_save(self): + with self.context_watcher(2): + with catch_unraisable_exception() as cm: + def _in_context(): + raise RuntimeError("test") + + with self.assertRaisesRegex(RuntimeError, "test"): + contextvars.copy_context().run(_in_context) + 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"): _testcapi.clear_context_watcher(-1) diff --git a/Misc/NEWS.d/next/C_API/2024-11-22-18-58-20.gh-issue-124872.o2Pl5p.rst b/Misc/NEWS.d/next/C_API/2024-11-22-18-58-20.gh-issue-124872.o2Pl5p.rst new file mode 100644 index 00000000000000..ac9afdbfeac37a --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-11-22-18-58-20.gh-issue-124872.o2Pl5p.rst @@ -0,0 +1,3 @@ +Callbacks registered with :c:func:`PyContext_AddWatcher` (type +:c:type:`PyContext_WatchCallback`) now return ``void`` instead of ``int``, and +no longer need to back up and restore exception state. diff --git a/Modules/_testcapi/watchers.c b/Modules/_testcapi/watchers.c index b4233d07134aea..1124e1a44d5295 100644 --- a/Modules/_testcapi/watchers.c +++ b/Modules/_testcapi/watchers.c @@ -629,7 +629,7 @@ static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1}; 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 +static void handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *ctx) { if (event == Py_CONTEXT_EVENT_ENTER) { num_context_object_enter_events[which_watcher]++; @@ -638,30 +638,27 @@ handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject * num_context_object_exit_events[which_watcher]++; } else { - return -1; + Py_UNREACHABLE(); } - return 0; } -static int +static void first_context_watcher_callback(PyContextEvent event, PyObject *ctx) { - return handle_context_watcher_event(0, event, ctx); + handle_context_watcher_event(0, event, ctx); } -static int +static void second_context_watcher_callback(PyContextEvent event, PyObject *ctx) { - return handle_context_watcher_event(1, event, ctx); + handle_context_watcher_event(1, event, ctx); } -static int +static void noop_context_event_handler(PyContextEvent event, PyObject *ctx) { - return 0; } -static int +static void error_context_event_handler(PyContextEvent event, PyObject *ctx) { PyErr_SetString(PyExc_RuntimeError, "boom!"); - return -1; } static PyObject * diff --git a/Python/context.c b/Python/context.c index 8bc487a33c890b..a9c9aca63aeca0 100644 --- a/Python/context.c +++ b/Python/context.c @@ -125,11 +125,14 @@ notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx) if (bits & 1) { PyContext_WatchCallback cb = interp->context_watchers[i]; assert(cb != NULL); - if (cb(event, ctx) < 0) { + PyObject *exc = _PyErr_GetRaisedException(ts); + cb(event, ctx); + if (_PyErr_Occurred(ts) != NULL) { PyErr_FormatUnraisable( "Exception ignored in %s watcher callback for %R", context_event_name(event), ctx); } + _PyErr_SetRaisedException(ts, exc); } i++; bits >>= 1;