Skip to content

Commit d3c82b9

Browse files
authored
pythongh-125512: Revert "pythongh-124872: Replace enter/exit events with "switched" (python#124776)" (python#125513)
1 parent 55c4f4c commit d3c82b9

File tree

6 files changed

+117
-117
lines changed

6 files changed

+117
-117
lines changed

Doc/c-api/contextvars.rst

+10-4
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,16 @@ Context object management functions:
123123
124124
Enumeration of possible context object watcher events:
125125
126-
- ``Py_CONTEXT_SWITCHED``: The :term:`current context` has switched to a
127-
different context. The object passed to the watch callback is the
128-
now-current :class:`contextvars.Context` object, or None if no context is
129-
current.
126+
- ``Py_CONTEXT_EVENT_ENTER``: A context has been entered, causing the
127+
:term:`current context` to switch to it. The object passed to the watch
128+
callback is the now-current :class:`contextvars.Context` object. Each
129+
enter event will eventually have a corresponding exit event for the same
130+
context object after any subsequently entered contexts have themselves been
131+
exited.
132+
- ``Py_CONTEXT_EVENT_EXIT``: A context is about to be exited, which will
133+
cause the :term:`current context` to switch back to what it was before the
134+
context was entered. The object passed to the watch callback is the
135+
still-current :class:`contextvars.Context` object.
130136
131137
.. versionadded:: 3.14
132138

Include/cpython/context.h

+13-4
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ PyAPI_FUNC(int) PyContext_Exit(PyObject *);
2929

3030
typedef enum {
3131
/*
32-
* The current context has switched to a different context. The object
33-
* passed to the watch callback is the now-current contextvars.Context
34-
* object, or None if no context is current.
32+
* A context has been entered, causing the "current context" to switch to
33+
* it. The object passed to the watch callback is the now-current
34+
* contextvars.Context object. Each enter event will eventually have a
35+
* corresponding exit event for the same context object after any
36+
* subsequently entered contexts have themselves been exited.
3537
*/
36-
Py_CONTEXT_SWITCHED = 1,
38+
Py_CONTEXT_EVENT_ENTER,
39+
/*
40+
* A context is about to be exited, which will cause the "current context"
41+
* to switch back to what it was before the context was entered. The
42+
* object passed to the watch callback is the still-current
43+
* contextvars.Context object.
44+
*/
45+
Py_CONTEXT_EVENT_EXIT,
3746
} PyContextEvent;
3847

3948
/*

Lib/test/test_capi/test_watchers.py

+44-45
Original file line numberDiff line numberDiff line change
@@ -577,62 +577,68 @@ class TestContextObjectWatchers(unittest.TestCase):
577577
def context_watcher(self, which_watcher):
578578
wid = _testcapi.add_context_watcher(which_watcher)
579579
try:
580-
switches = _testcapi.get_context_switches(which_watcher)
581-
except ValueError:
582-
switches = None
583-
try:
584-
yield switches
580+
yield wid
585581
finally:
586582
_testcapi.clear_context_watcher(wid)
587583

588-
def assert_event_counts(self, want_0, want_1):
589-
self.assertEqual(len(_testcapi.get_context_switches(0)), want_0)
590-
self.assertEqual(len(_testcapi.get_context_switches(1)), want_1)
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))
591594

592595
def test_context_object_events_dispatched(self):
593596
# verify that all counts are zero before any watchers are registered
594-
self.assert_event_counts(0, 0)
597+
self.assert_event_counts(0, 0, 0, 0)
595598

596599
# verify that all counts remain zero when a context object is
597600
# entered and exited with no watchers registered
598601
ctx = contextvars.copy_context()
599-
ctx.run(self.assert_event_counts, 0, 0)
600-
self.assert_event_counts(0, 0)
602+
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
603+
self.assert_event_counts(0, 0, 0, 0)
601604

602605
# verify counts are as expected when first watcher is registered
603606
with self.context_watcher(0):
604-
self.assert_event_counts(0, 0)
605-
ctx.run(self.assert_event_counts, 1, 0)
606-
self.assert_event_counts(2, 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)
607610

608611
# again with second watcher registered
609612
with self.context_watcher(1):
610-
self.assert_event_counts(2, 0)
611-
ctx.run(self.assert_event_counts, 3, 1)
612-
self.assert_event_counts(4, 2)
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)
613616

614617
# verify counts are reset and don't change after both watchers are cleared
615-
ctx.run(self.assert_event_counts, 0, 0)
616-
self.assert_event_counts(0, 0)
617-
618-
def test_callback_error(self):
619-
ctx_outer = contextvars.copy_context()
620-
ctx_inner = contextvars.copy_context()
621-
unraisables = []
622-
623-
def _in_outer():
624-
with self.context_watcher(2):
625-
with catch_unraisable_exception() as cm:
626-
ctx_inner.run(lambda: unraisables.append(cm.unraisable))
627-
unraisables.append(cm.unraisable)
628-
629-
ctx_outer.run(_in_outer)
630-
self.assertEqual([x.err_msg for x in unraisables],
631-
["Exception ignored in Py_CONTEXT_SWITCHED "
632-
f"watcher callback for {ctx!r}"
633-
for ctx in [ctx_inner, ctx_outer]])
634-
self.assertEqual([str(x.exc_value) for x in unraisables],
635-
["boom!", "boom!"])
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!")
636642

637643
def test_clear_out_of_range_watcher_id(self):
638644
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
@@ -648,12 +654,5 @@ def test_allocate_too_many_watchers(self):
648654
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
649655
_testcapi.allocate_too_many_context_watchers()
650656

651-
def test_exit_base_context(self):
652-
ctx = contextvars.Context()
653-
_testcapi.clear_context_stack()
654-
with self.context_watcher(0) as switches:
655-
ctx.run(lambda: None)
656-
self.assertEqual(switches, [ctx, None])
657-
658657
if __name__ == "__main__":
659658
unittest.main()

Modules/_testcapi/watchers.c

+38-41
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,16 @@ allocate_too_many_func_watchers(PyObject *self, PyObject *args)
626626
// Test contexct object watchers
627627
#define NUM_CONTEXT_WATCHERS 2
628628
static int context_watcher_ids[NUM_CONTEXT_WATCHERS] = {-1, -1};
629-
static PyObject *context_switches[NUM_CONTEXT_WATCHERS];
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};
630631

631632
static int
632633
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *ctx) {
633-
if (event == Py_CONTEXT_SWITCHED) {
634-
PyList_Append(context_switches[which_watcher], 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]++;
635639
}
636640
else {
637641
return -1;
@@ -663,28 +667,31 @@ error_context_event_handler(PyContextEvent event, PyObject *ctx) {
663667
static PyObject *
664668
add_context_watcher(PyObject *self, PyObject *which_watcher)
665669
{
666-
static const PyContext_WatchCallback callbacks[] = {
667-
&first_context_watcher_callback,
668-
&second_context_watcher_callback,
669-
&error_context_event_handler,
670-
};
670+
int watcher_id;
671671
assert(PyLong_Check(which_watcher));
672672
long which_l = PyLong_AsLong(which_watcher);
673-
if (which_l < 0 || which_l >= (long)Py_ARRAY_LENGTH(callbacks)) {
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 {
674689
PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l);
675690
return NULL;
676691
}
677-
int watcher_id = PyContext_AddWatcher(callbacks[which_l]);
678692
if (watcher_id < 0) {
679693
return NULL;
680694
}
681-
if (which_l >= 0 && which_l < NUM_CONTEXT_WATCHERS) {
682-
context_watcher_ids[which_l] = watcher_id;
683-
Py_XSETREF(context_switches[which_l], PyList_New(0));
684-
if (context_switches[which_l] == NULL) {
685-
return NULL;
686-
}
687-
}
688695
return PyLong_FromLong(watcher_id);
689696
}
690697

@@ -701,42 +708,30 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
701708
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
702709
if (watcher_id_l == context_watcher_ids[i]) {
703710
context_watcher_ids[i] = -1;
704-
Py_CLEAR(context_switches[i]);
711+
num_context_object_enter_events[i] = 0;
712+
num_context_object_exit_events[i] = 0;
705713
}
706714
}
707715
}
708716
Py_RETURN_NONE;
709717
}
710718

711719
static PyObject *
712-
clear_context_stack(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
720+
get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id)
713721
{
714-
PyThreadState *tstate = PyThreadState_Get();
715-
if (tstate->context == NULL) {
716-
Py_RETURN_NONE;
717-
}
718-
if (((PyContext *)tstate->context)->ctx_prev != NULL) {
719-
PyErr_SetString(PyExc_RuntimeError,
720-
"must first exit all non-base contexts");
721-
return NULL;
722-
}
723-
Py_CLEAR(tstate->context);
724-
Py_RETURN_NONE;
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]);
725726
}
726727

727728
static PyObject *
728-
get_context_switches(PyObject *Py_UNUSED(self), PyObject *watcher_id)
729+
get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id)
729730
{
730731
assert(PyLong_Check(watcher_id));
731732
long watcher_id_l = PyLong_AsLong(watcher_id);
732-
if (watcher_id_l < 0 || watcher_id_l >= NUM_CONTEXT_WATCHERS) {
733-
PyErr_Format(PyExc_ValueError, "invalid watcher %ld", watcher_id_l);
734-
return NULL;
735-
}
736-
if (context_switches[watcher_id_l] == NULL) {
737-
return PyList_New(0);
738-
}
739-
return Py_NewRef(context_switches[watcher_id_l]);
733+
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
734+
return PyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
740735
}
741736

742737
static PyObject *
@@ -840,8 +835,10 @@ static PyMethodDef test_methods[] = {
840835
// Code object watchers.
841836
{"add_context_watcher", add_context_watcher, METH_O, NULL},
842837
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
843-
{"clear_context_stack", clear_context_stack, METH_NOARGS, NULL},
844-
{"get_context_switches", get_context_switches, 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},
845842
{"allocate_too_many_context_watchers",
846843
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
847844
{NULL},

Python/context.c

+10-21
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ PyContext_CopyCurrent(void)
102102
static const char *
103103
context_event_name(PyContextEvent event) {
104104
switch (event) {
105-
case Py_CONTEXT_SWITCHED:
106-
return "Py_CONTEXT_SWITCHED";
105+
case Py_CONTEXT_EVENT_ENTER:
106+
return "Py_CONTEXT_EVENT_ENTER";
107+
case Py_CONTEXT_EVENT_EXIT:
108+
return "Py_CONTEXT_EVENT_EXIT";
107109
default:
108110
return "?";
109111
}
@@ -113,13 +115,6 @@ context_event_name(PyContextEvent event) {
113115
static void
114116
notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx)
115117
{
116-
if (ctx == NULL) {
117-
// This will happen after exiting the last context in the stack, which
118-
// can occur if context_get was never called before entering a context
119-
// (e.g., called `contextvars.Context().run()` on a fresh thread, as
120-
// PyContext_Enter doesn't call context_get).
121-
ctx = Py_None;
122-
}
123118
assert(Py_REFCNT(ctx) > 0);
124119
PyInterpreterState *interp = ts->interp;
125120
assert(interp->_initialized);
@@ -180,16 +175,6 @@ PyContext_ClearWatcher(int watcher_id)
180175
}
181176

182177

183-
static inline void
184-
context_switched(PyThreadState *ts)
185-
{
186-
ts->context_ver++;
187-
// ts->context is used instead of context_get() because context_get() might
188-
// throw if ts->context is NULL.
189-
notify_context_watchers(ts, Py_CONTEXT_SWITCHED, ts->context);
190-
}
191-
192-
193178
static int
194179
_PyContext_Enter(PyThreadState *ts, PyObject *octx)
195180
{
@@ -206,7 +191,9 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx)
206191
ctx->ctx_entered = 1;
207192

208193
ts->context = Py_NewRef(ctx);
209-
context_switched(ts);
194+
ts->context_ver++;
195+
196+
notify_context_watchers(ts, Py_CONTEXT_EVENT_ENTER, octx);
210197
return 0;
211198
}
212199

@@ -240,11 +227,13 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx)
240227
return -1;
241228
}
242229

230+
notify_context_watchers(ts, Py_CONTEXT_EVENT_EXIT, octx);
243231
Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev);
232+
ts->context_ver++;
244233

245234
ctx->ctx_prev = NULL;
246235
ctx->ctx_entered = 0;
247-
context_switched(ts);
236+
248237
return 0;
249238
}
250239

Tools/c-analyzer/cpython/ignored.tsv

+2-2
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ Modules/_testcapi/watchers.c - pyfunc_watchers -
455455
Modules/_testcapi/watchers.c - func_watcher_ids -
456456
Modules/_testcapi/watchers.c - func_watcher_callbacks -
457457
Modules/_testcapi/watchers.c - context_watcher_ids -
458-
Modules/_testcapi/watchers.c - context_switches -
459-
Modules/_testcapi/watchers.c add_context_watcher callbacks -
458+
Modules/_testcapi/watchers.c - num_context_object_enter_events -
459+
Modules/_testcapi/watchers.c - num_context_object_exit_events -
460460
Modules/_testcapimodule.c - BasicStaticTypes -
461461
Modules/_testcapimodule.c - num_basic_static_types_used -
462462
Modules/_testcapimodule.c - ContainerNoGC_members -

0 commit comments

Comments
 (0)