Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d87482b

Browse files
gh-119333: Add C api to have contextvar enter/exit callbacks (#119335)
Co-authored-by: Erlend E. Aasland <[email protected]>
1 parent ad7c778 commit d87482b

File tree

10 files changed

+402
-0
lines changed

10 files changed

+402
-0
lines changed

Doc/c-api/contextvars.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,52 @@ Context object management functions:
101101
current context for the current thread. Returns ``0`` on success,
102102
and ``-1`` on error.
103103
104+
.. c:function:: int PyContext_AddWatcher(PyContext_WatchCallback callback)
105+
106+
Register *callback* as a context object watcher for the current interpreter.
107+
Return an ID which may be passed to :c:func:`PyContext_ClearWatcher`.
108+
In case of error (e.g. no more watcher IDs available),
109+
return ``-1`` and set an exception.
110+
111+
.. versionadded:: 3.14
112+
113+
.. c:function:: int PyContext_ClearWatcher(int watcher_id)
114+
115+
Clear watcher identified by *watcher_id* previously returned from
116+
:c:func:`PyContext_AddWatcher` for the current interpreter.
117+
Return ``0`` on success, or ``-1`` and set an exception on error
118+
(e.g. if the given *watcher_id* was never registered.)
119+
120+
.. versionadded:: 3.14
121+
122+
.. c:type:: PyContextEvent
123+
124+
Enumeration of possible context object watcher events:
125+
- ``Py_CONTEXT_EVENT_ENTER``
126+
- ``Py_CONTEXT_EVENT_EXIT``
127+
128+
.. versionadded:: 3.14
129+
130+
.. c:type:: int (*PyContext_WatchCallback)(PyContextEvent event, PyContext* ctx)
131+
132+
Type of a context object watcher callback function.
133+
If *event* is ``Py_CONTEXT_EVENT_ENTER``, then the callback is invoked
134+
after *ctx* has been set as the current context for the current thread.
135+
Otherwise, the callback is invoked before the deactivation of *ctx* as the current context
136+
and the restoration of the previous contex object for the current thread.
137+
138+
If the callback returns with an exception set, it must return ``-1``; this
139+
exception will be printed as an unraisable exception using
140+
:c:func:`PyErr_FormatUnraisable`. Otherwise it should return ``0``.
141+
142+
There may already be a pending exception set on entry to the callback. In
143+
this case, the callback should return ``0`` with the same exception still
144+
set. This means the callback may not call any other API that can set an
145+
exception unless it saves and clears the exception state first, and restores
146+
it before returning.
147+
148+
.. versionadded:: 3.14
149+
104150
105151
Context variable functions:
106152

Include/cpython/context.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,38 @@ PyAPI_FUNC(PyObject *) PyContext_CopyCurrent(void);
2727
PyAPI_FUNC(int) PyContext_Enter(PyObject *);
2828
PyAPI_FUNC(int) PyContext_Exit(PyObject *);
2929

30+
typedef enum {
31+
Py_CONTEXT_EVENT_ENTER,
32+
Py_CONTEXT_EVENT_EXIT,
33+
} PyContextEvent;
34+
35+
/*
36+
* A Callback to clue in non-python contexts impls about a
37+
* change in the active python context.
38+
*
39+
* The callback is invoked with the event and a reference to =
40+
* the context after its entered and before its exited.
41+
*
42+
* if the callback returns with an exception set, it must return -1. Otherwise
43+
* it should return 0
44+
*/
45+
typedef int (*PyContext_WatchCallback)(PyContextEvent, PyContext *);
46+
47+
/*
48+
* Register a per-interpreter callback that will be invoked for context object
49+
* enter/exit events.
50+
*
51+
* Returns a handle that may be passed to PyContext_ClearWatcher on success,
52+
* or -1 and sets and error if no more handles are available.
53+
*/
54+
PyAPI_FUNC(int) PyContext_AddWatcher(PyContext_WatchCallback callback);
55+
56+
/*
57+
* Clear the watcher associated with the watcher_id handle.
58+
*
59+
* Returns 0 on success or -1 if no watcher exists for the provided id.
60+
*/
61+
PyAPI_FUNC(int) PyContext_ClearWatcher(int watcher_id);
3062

3163
/* Create a new context variable.
3264

Include/internal/pycore_context.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "pycore_hamt.h" // PyHamtObject
99

10+
#define CONTEXT_MAX_WATCHERS 8
1011

1112
extern PyTypeObject _PyContextTokenMissing_Type;
1213

Include/internal/pycore_interp.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,10 @@ struct _is {
240240
PyObject *audit_hooks;
241241
PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS];
242242
PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS];
243+
PyContext_WatchCallback context_watchers[CONTEXT_MAX_WATCHERS];
243244
// One bit is set for each non-NULL entry in code_watchers
244245
uint8_t active_code_watchers;
246+
uint8_t active_context_watchers;
245247

246248
struct _py_object_state object_state;
247249
struct _Py_unicode_state unicode;

Lib/test/test_capi/test_watchers.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import contextvars
23

34
from contextlib import contextmanager, ExitStack
45
from test.support import (
@@ -571,5 +572,87 @@ def test_allocate_too_many_watchers(self):
571572
_testcapi.allocate_too_many_func_watchers()
572573

573574

575+
class TestContextObjectWatchers(unittest.TestCase):
576+
@contextmanager
577+
def context_watcher(self, which_watcher):
578+
wid = _testcapi.add_context_watcher(which_watcher)
579+
try:
580+
yield wid
581+
finally:
582+
_testcapi.clear_context_watcher(wid)
583+
584+
def assert_event_counts(self, exp_enter_0, exp_exit_0,
585+
exp_enter_1, exp_exit_1):
586+
self.assertEqual(
587+
exp_enter_0, _testcapi.get_context_watcher_num_enter_events(0))
588+
self.assertEqual(
589+
exp_exit_0, _testcapi.get_context_watcher_num_exit_events(0))
590+
self.assertEqual(
591+
exp_enter_1, _testcapi.get_context_watcher_num_enter_events(1))
592+
self.assertEqual(
593+
exp_exit_1, _testcapi.get_context_watcher_num_exit_events(1))
594+
595+
def test_context_object_events_dispatched(self):
596+
# verify that all counts are zero before any watchers are registered
597+
self.assert_event_counts(0, 0, 0, 0)
598+
599+
# verify that all counts remain zero when a context object is
600+
# entered and exited with no watchers registered
601+
ctx = contextvars.copy_context()
602+
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
603+
self.assert_event_counts(0, 0, 0, 0)
604+
605+
# verify counts are as expected when first watcher is registered
606+
with self.context_watcher(0):
607+
self.assert_event_counts(0, 0, 0, 0)
608+
ctx.run(self.assert_event_counts, 1, 0, 0, 0)
609+
self.assert_event_counts(1, 1, 0, 0)
610+
611+
# again with second watcher registered
612+
with self.context_watcher(1):
613+
self.assert_event_counts(1, 1, 0, 0)
614+
ctx.run(self.assert_event_counts, 2, 1, 1, 0)
615+
self.assert_event_counts(2, 2, 1, 1)
616+
617+
# verify counts are reset and don't change after both watchers are cleared
618+
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
619+
self.assert_event_counts(0, 0, 0, 0)
620+
621+
def test_enter_error(self):
622+
with self.context_watcher(2):
623+
with catch_unraisable_exception() as cm:
624+
ctx = contextvars.copy_context()
625+
ctx.run(int, 0)
626+
self.assertEqual(
627+
cm.unraisable.err_msg,
628+
"Exception ignored in "
629+
f"Py_CONTEXT_EVENT_EXIT watcher callback for {ctx!r}"
630+
)
631+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
632+
633+
def test_exit_error(self):
634+
ctx = contextvars.copy_context()
635+
def _in_context(stack):
636+
stack.enter_context(self.context_watcher(2))
637+
638+
with catch_unraisable_exception() as cm:
639+
with ExitStack() as stack:
640+
ctx.run(_in_context, stack)
641+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
642+
643+
def test_clear_out_of_range_watcher_id(self):
644+
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
645+
_testcapi.clear_context_watcher(-1)
646+
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID 8"):
647+
_testcapi.clear_context_watcher(8) # CONTEXT_MAX_WATCHERS = 8
648+
649+
def test_clear_unassigned_watcher_id(self):
650+
with self.assertRaisesRegex(ValueError, r"No context watcher set for ID 1"):
651+
_testcapi.clear_context_watcher(1)
652+
653+
def test_allocate_too_many_watchers(self):
654+
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
655+
_testcapi.allocate_too_many_context_watchers()
656+
574657
if __name__ == "__main__":
575658
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyContext_AddWatcher` and :c:func:`PyContext_ClearWatcher` APIs to
2+
register callbacks to receive notification on enter and exit of context objects.

Modules/_testcapi/watchers.c

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#define Py_BUILD_CORE
99
#include "pycore_function.h" // FUNC_MAX_WATCHERS
1010
#include "pycore_code.h" // CODE_MAX_WATCHERS
11+
#include "pycore_context.h" // CONTEXT_MAX_WATCHERS
1112

1213
/*[clinic input]
1314
module _testcapi
@@ -622,6 +623,147 @@ allocate_too_many_func_watchers(PyObject *self, PyObject *args)
622623
Py_RETURN_NONE;
623624
}
624625

626+
// Test contexct object watchers
627+
#define NUM_CONTEXT_WATCHERS 2
628+
static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1};
629+
static int num_context_object_enter_events[NUM_CONTEXT_WATCHERS] = {0, 0};
630+
static int num_context_object_exit_events[NUM_CONTEXT_WATCHERS] = {0, 0};
631+
632+
static int
633+
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyContext *ctx) {
634+
if (event == Py_CONTEXT_EVENT_ENTER) {
635+
num_context_object_enter_events[which_watcher]++;
636+
}
637+
else if (event == Py_CONTEXT_EVENT_EXIT) {
638+
num_context_object_exit_events[which_watcher]++;
639+
}
640+
else {
641+
return -1;
642+
}
643+
return 0;
644+
}
645+
646+
static int
647+
first_context_watcher_callback(PyContextEvent event, PyContext *ctx) {
648+
return handle_context_watcher_event(0, event, ctx);
649+
}
650+
651+
static int
652+
second_context_watcher_callback(PyContextEvent event, PyContext *ctx) {
653+
return handle_context_watcher_event(1, event, ctx);
654+
}
655+
656+
static int
657+
noop_context_event_handler(PyContextEvent event, PyContext *ctx) {
658+
return 0;
659+
}
660+
661+
static int
662+
error_context_event_handler(PyContextEvent event, PyContext *ctx) {
663+
PyErr_SetString(PyExc_RuntimeError, "boom!");
664+
return -1;
665+
}
666+
667+
static PyObject *
668+
add_context_watcher(PyObject *self, PyObject *which_watcher)
669+
{
670+
int watcher_id;
671+
assert(PyLong_Check(which_watcher));
672+
long which_l = PyLong_AsLong(which_watcher);
673+
if (which_l == 0) {
674+
watcher_id = PyContext_AddWatcher(first_context_watcher_callback);
675+
context_watcher_ids[0] = watcher_id;
676+
num_context_object_enter_events[0] = 0;
677+
num_context_object_exit_events[0] = 0;
678+
}
679+
else if (which_l == 1) {
680+
watcher_id = PyContext_AddWatcher(second_context_watcher_callback);
681+
context_watcher_ids[1] = watcher_id;
682+
num_context_object_enter_events[1] = 0;
683+
num_context_object_exit_events[1] = 0;
684+
}
685+
else if (which_l == 2) {
686+
watcher_id = PyContext_AddWatcher(error_context_event_handler);
687+
}
688+
else {
689+
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
690+
return NULL;
691+
}
692+
if (watcher_id < 0) {
693+
return NULL;
694+
}
695+
return PyLong_FromLong(watcher_id);
696+
}
697+
698+
static PyObject *
699+
clear_context_watcher(PyObject *self, PyObject *watcher_id)
700+
{
701+
assert(PyLong_Check(watcher_id));
702+
long watcher_id_l = PyLong_AsLong(watcher_id);
703+
if (PyContext_ClearWatcher(watcher_id_l) < 0) {
704+
return NULL;
705+
}
706+
// reset static events counters
707+
if (watcher_id_l >= 0) {
708+
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
709+
if (watcher_id_l == context_watcher_ids[i]) {
710+
context_watcher_ids[i] = -1;
711+
num_context_object_enter_events[i] = 0;
712+
num_context_object_exit_events[i] = 0;
713+
}
714+
}
715+
}
716+
Py_RETURN_NONE;
717+
}
718+
719+
static PyObject *
720+
get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id)
721+
{
722+
assert(PyLong_Check(watcher_id));
723+
long watcher_id_l = PyLong_AsLong(watcher_id);
724+
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
725+
return PyLong_FromLong(num_context_object_enter_events[watcher_id_l]);
726+
}
727+
728+
static PyObject *
729+
get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id)
730+
{
731+
assert(PyLong_Check(watcher_id));
732+
long watcher_id_l = PyLong_AsLong(watcher_id);
733+
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
734+
return PyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
735+
}
736+
737+
static PyObject *
738+
allocate_too_many_context_watchers(PyObject *self, PyObject *args)
739+
{
740+
int watcher_ids[CONTEXT_MAX_WATCHERS + 1];
741+
int num_watchers = 0;
742+
for (unsigned long i = 0; i < sizeof(watcher_ids) / sizeof(int); i++) {
743+
int watcher_id = PyContext_AddWatcher(noop_context_event_handler);
744+
if (watcher_id == -1) {
745+
break;
746+
}
747+
watcher_ids[i] = watcher_id;
748+
num_watchers++;
749+
}
750+
PyObject *exc = PyErr_GetRaisedException();
751+
for (int i = 0; i < num_watchers; i++) {
752+
if (PyContext_ClearWatcher(watcher_ids[i]) < 0) {
753+
PyErr_WriteUnraisable(Py_None);
754+
break;
755+
}
756+
}
757+
if (exc) {
758+
PyErr_SetRaisedException(exc);
759+
return NULL;
760+
}
761+
else if (PyErr_Occurred()) {
762+
return NULL;
763+
}
764+
Py_RETURN_NONE;
765+
}
766+
625767
/*[clinic input]
626768
_testcapi.set_func_defaults_via_capi
627769
func: object
@@ -689,6 +831,16 @@ static PyMethodDef test_methods[] = {
689831
_TESTCAPI_SET_FUNC_KWDEFAULTS_VIA_CAPI_METHODDEF
690832
{"allocate_too_many_func_watchers", allocate_too_many_func_watchers,
691833
METH_NOARGS, NULL},
834+
835+
// Code object watchers.
836+
{"add_context_watcher", add_context_watcher, METH_O, NULL},
837+
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
838+
{"get_context_watcher_num_enter_events",
839+
get_context_watcher_num_enter_events, METH_O, NULL},
840+
{"get_context_watcher_num_exit_events",
841+
get_context_watcher_num_exit_events, METH_O, NULL},
842+
{"allocate_too_many_context_watchers",
843+
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
692844
{NULL},
693845
};
694846

0 commit comments

Comments
 (0)