Skip to content

Commit 0b18cf1

Browse files
committed
pythongh-119333, pythongh-99633: Replace enter/exit events with "switched"
Users want to know when the current context switches to a different context object. Right now this happens when and only when a context is entered or exited, so the enter and exit events are synonymous with "switched". However, if the changes proposed for pythongh-99633 are implemented, the current context will also switch for reasons other than context enter or exit. Since users actually care about context switches and not enter or exit, replace the enter and exit events with a single switched event. The former exit event was emitted just before exiting the context. The new switched event is emitted after the context is exited to match the semantics users expect of an event with a past-tense name. If users need the ability to clean up before the switch takes effect, another event type can be added in the future. It is not added here because YAGNI. I skipped 0 in the enum as a matter of practice. Skipping 0 makes it easier to troubleshoot when code forgets to set zeroed memory, and it aligns with best practices for other tools (e.g., https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
1 parent ebac569 commit 0b18cf1

File tree

5 files changed

+58
-102
lines changed

5 files changed

+58
-102
lines changed

Doc/c-api/contextvars.rst

+3-10
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,9 @@ Context object management functions:
123123
124124
Enumeration of possible context object watcher events:
125125
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.
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.
136129
137130
.. versionadded:: 3.14
138131

Include/cpython/context.h

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

3030
typedef enum {
3131
/*
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.
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.
3735
*/
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,
36+
Py_CONTEXT_SWITCHED = 1,
4637
} PyContextEvent;
4738

4839
/*

Lib/test/test_capi/test_watchers.py

+35-43
Original file line numberDiff line numberDiff line change
@@ -581,64 +581,56 @@ def context_watcher(self, which_watcher):
581581
finally:
582582
_testcapi.clear_context_watcher(wid)
583583

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))
584+
def assert_event_counts(self, exp_switched_0, exp_switched_1):
585+
self.assertEqual(exp_switched_0,
586+
_testcapi.get_context_watcher_num_switched_events(0))
587+
self.assertEqual(exp_switched_1,
588+
_testcapi.get_context_watcher_num_switched_events(1))
594589

595590
def test_context_object_events_dispatched(self):
596591
# verify that all counts are zero before any watchers are registered
597-
self.assert_event_counts(0, 0, 0, 0)
592+
self.assert_event_counts(0, 0)
598593

599594
# verify that all counts remain zero when a context object is
600595
# entered and exited with no watchers registered
601596
ctx = contextvars.copy_context()
602-
ctx.run(self.assert_event_counts, 0, 0, 0, 0)
603-
self.assert_event_counts(0, 0, 0, 0)
597+
ctx.run(self.assert_event_counts, 0, 0)
598+
self.assert_event_counts(0, 0)
604599

605600
# verify counts are as expected when first watcher is registered
606601
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)
602+
self.assert_event_counts(0, 0)
603+
ctx.run(self.assert_event_counts, 1, 0)
604+
self.assert_event_counts(2, 0)
610605

611606
# again with second watcher registered
612607
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)
608+
self.assert_event_counts(2, 0)
609+
ctx.run(self.assert_event_counts, 3, 1)
610+
self.assert_event_counts(4, 2)
616611

617612
# 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!")
613+
ctx.run(self.assert_event_counts, 0, 0)
614+
self.assert_event_counts(0, 0)
615+
616+
def test_callback_error(self):
617+
ctx_outer = contextvars.copy_context()
618+
ctx_inner = contextvars.copy_context()
619+
unraisables = []
620+
621+
def _in_outer():
622+
with self.context_watcher(2):
623+
with catch_unraisable_exception() as cm:
624+
ctx_inner.run(lambda: unraisables.append(cm.unraisable))
625+
unraisables.append(cm.unraisable)
626+
627+
ctx_outer.run(_in_outer)
628+
self.assertEqual([x.err_msg for x in unraisables],
629+
["Exception ignored in Py_CONTEXT_SWITCHED "
630+
f"watcher callback for {ctx!r}"
631+
for ctx in [ctx_inner, ctx_outer]])
632+
self.assertEqual([str(x.exc_value) for x in unraisables],
633+
["boom!", "boom!"])
642634

643635
def test_exception_save(self):
644636
with self.context_watcher(2):

Modules/_testcapi/watchers.c

+10-28
Original file line numberDiff line numberDiff line change
@@ -626,16 +626,12 @@ 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 int num_context_object_enter_events[NUM_CONTEXT_WATCHERS] = {0, 0};
630-
static int num_context_object_exit_events[NUM_CONTEXT_WATCHERS] = {0, 0};
629+
static int num_context_object_switched_events[NUM_CONTEXT_WATCHERS] = {0, 0};
631630

632631
static void
633632
handle_context_watcher_event(int which_watcher, PyContextEvent event, PyObject *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]++;
633+
if (event == Py_CONTEXT_SWITCHED) {
634+
num_context_object_switched_events[which_watcher]++;
639635
}
640636
else {
641637
Py_UNREACHABLE();
@@ -670,14 +666,12 @@ add_context_watcher(PyObject *self, PyObject *which_watcher)
670666
if (which_l == 0) {
671667
watcher_id = PyContext_AddWatcher(first_context_watcher_callback);
672668
context_watcher_ids[0] = watcher_id;
673-
num_context_object_enter_events[0] = 0;
674-
num_context_object_exit_events[0] = 0;
669+
num_context_object_switched_events[0] = 0;
675670
}
676671
else if (which_l == 1) {
677672
watcher_id = PyContext_AddWatcher(second_context_watcher_callback);
678673
context_watcher_ids[1] = watcher_id;
679-
num_context_object_enter_events[1] = 0;
680-
num_context_object_exit_events[1] = 0;
674+
num_context_object_switched_events[1] = 0;
681675
}
682676
else if (which_l == 2) {
683677
watcher_id = PyContext_AddWatcher(error_context_event_handler);
@@ -705,30 +699,20 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
705699
for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) {
706700
if (watcher_id_l == context_watcher_ids[i]) {
707701
context_watcher_ids[i] = -1;
708-
num_context_object_enter_events[i] = 0;
709-
num_context_object_exit_events[i] = 0;
702+
num_context_object_switched_events[i] = 0;
710703
}
711704
}
712705
}
713706
Py_RETURN_NONE;
714707
}
715708

716709
static PyObject *
717-
get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id)
718-
{
719-
assert(PyLong_Check(watcher_id));
720-
long watcher_id_l = PyLong_AsLong(watcher_id);
721-
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
722-
return PyLong_FromLong(num_context_object_enter_events[watcher_id_l]);
723-
}
724-
725-
static PyObject *
726-
get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id)
710+
get_context_watcher_num_switched_events(PyObject *self, PyObject *watcher_id)
727711
{
728712
assert(PyLong_Check(watcher_id));
729713
long watcher_id_l = PyLong_AsLong(watcher_id);
730714
assert(watcher_id_l >= 0 && watcher_id_l < NUM_CONTEXT_WATCHERS);
731-
return PyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
715+
return PyLong_FromLong(num_context_object_switched_events[watcher_id_l]);
732716
}
733717

734718
static PyObject *
@@ -832,10 +816,8 @@ static PyMethodDef test_methods[] = {
832816
// Code object watchers.
833817
{"add_context_watcher", add_context_watcher, METH_O, NULL},
834818
{"clear_context_watcher", clear_context_watcher, METH_O, NULL},
835-
{"get_context_watcher_num_enter_events",
836-
get_context_watcher_num_enter_events, METH_O, NULL},
837-
{"get_context_watcher_num_exit_events",
838-
get_context_watcher_num_exit_events, METH_O, NULL},
819+
{"get_context_watcher_num_switched_events",
820+
get_context_watcher_num_switched_events, METH_O, NULL},
839821
{"allocate_too_many_context_watchers",
840822
(PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL},
841823
{NULL},

Python/context.c

+6-8
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,16 @@ PyContext_CopyCurrent(void)
102102
static const char *
103103
context_event_name(PyContextEvent event) {
104104
switch (event) {
105-
case Py_CONTEXT_EVENT_ENTER:
106-
return "Py_CONTEXT_EVENT_ENTER";
107-
case Py_CONTEXT_EVENT_EXIT:
108-
return "Py_CONTEXT_EVENT_EXIT";
105+
case Py_CONTEXT_SWITCHED:
106+
return "Py_CONTEXT_SWITCHED";
109107
default:
110108
return "?";
111109
}
112110
Py_UNREACHABLE();
113111
}
114112

115113
static void
116-
notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyContext *ctx)
114+
notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx)
117115
{
118116
assert(Py_REFCNT(ctx) > 0);
119117
PyInterpreterState *interp = ts->interp;
@@ -126,7 +124,7 @@ notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyContext *ctx)
126124
PyContext_WatchCallback cb = interp->context_watchers[i];
127125
assert(cb != NULL);
128126
PyObject *exc = _PyErr_GetRaisedException(ts);
129-
cb(event, (PyObject *)ctx);
127+
cb(event, ctx);
130128
if (_PyErr_Occurred(ts) != NULL) {
131129
PyErr_FormatUnraisable(
132130
"Exception ignored in %s watcher callback for %R",
@@ -196,7 +194,7 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx)
196194
ts->context = Py_NewRef(ctx);
197195
ts->context_ver++;
198196

199-
notify_context_watchers(ts, Py_CONTEXT_EVENT_ENTER, ctx);
197+
notify_context_watchers(ts, Py_CONTEXT_SWITCHED, ts->context);
200198
return 0;
201199
}
202200

@@ -230,13 +228,13 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx)
230228
return -1;
231229
}
232230

233-
notify_context_watchers(ts, Py_CONTEXT_EVENT_EXIT, ctx);
234231
Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev);
235232
ts->context_ver++;
236233

237234
ctx->ctx_prev = NULL;
238235
ctx->ctx_entered = 0;
239236

237+
notify_context_watchers(ts, Py_CONTEXT_SWITCHED, ts->context);
240238
return 0;
241239
}
242240

0 commit comments

Comments
 (0)