Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-124872: Back up exception before calling PyContext_WatchCallback
I believe that the value of a simpler API (and defense against poorly
written callbacks) outweighs the cost of backing up and restoring the
thread's exception state.
  • Loading branch information
rhansen committed Oct 1, 2024
commit acf0fdb7cc4eac4ef8fd941edb522a5050b37623
9 changes: 3 additions & 6 deletions Doc/c-api/contextvars.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,13 @@ Context object management functions:
Otherwise, the callback is invoked before the deactivation of *ctx* as the current context
and the restoration of the previous contex object for the current thread.

Any pending exception is cleared before the callback is called and restored
after the callback returns.

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``.

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.

.. versionadded:: 3.14


Expand Down
3 changes: 3 additions & 0 deletions Include/cpython/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ typedef enum {
* The callback is invoked with the event and a reference to
* the context after its entered and before its exited.
*
* Any pending exception is cleared before the callback is called and restored
* after the callback returns.
*
* if the callback returns with an exception set, it must return -1. Otherwise
* it should return 0
*/
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_capi/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions Python/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,13 @@ static void notify_context_watchers(PyContextEvent event, PyContext *ctx, PyThre
if (bits & 1) {
PyContext_WatchCallback cb = interp->context_watchers[i];
assert(cb != NULL);
PyObject *exc = _PyErr_GetRaisedException(ts);
if (cb(event, ctx) < 0) {
PyErr_FormatUnraisable(
"Exception ignored in %s watcher callback for %R",
context_event_name(event), ctx);
}
_PyErr_SetRaisedException(ts, exc);
}
i++;
bits >>= 1;
Expand Down