Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd04d9c0

Browse files
committed
gh-124872: Replace enter/exit events with "switched"
Users want to know when the current context switches to a differentcontext object. Right now this happens when and only when a contextis entered or exited, so the enter and exit events are synonymous with"switched". However, if the changes proposed forgh-99633 areimplemented, the current context will also switch for reasons otherthan context enter or exit. Since users actually care about contextswitches and not enter or exit, replace the enter and exit events witha 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 matchthe semantics users expect of an event with a past-tense name. Ifusers need the ability to clean up before the switch takes effect,another event type can be added in the future. It is not added herebecause YAGNI.I skipped 0 in the enum as a matter of practice. Skipping 0 makes iteasier to troubleshoot when code forgets to set zeroed memory, and italigns with best practices for other tools (e.g.,https://protobuf.dev/programming-guides/dos-donts/#unspecified-enum).
1 parent029a0c9 commitd04d9c0

File tree

6 files changed

+109
-115
lines changed

6 files changed

+109
-115
lines changed

‎Doc/c-api/contextvars.rst‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,10 @@ 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, or None if no context is
129+
current.
136130
137131
.. versionadded:: 3.14
138132

‎Include/cpython/context.h‎

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,11 @@ PyAPI_FUNC(int) PyContext_Exit(PyObject *);
2929

3030
typedefenum {
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, or None if no context is current.
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‎

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -577,68 +577,62 @@ class TestContextObjectWatchers(unittest.TestCase):
577577
defcontext_watcher(self,which_watcher):
578578
wid=_testcapi.add_context_watcher(which_watcher)
579579
try:
580-
yieldwid
580+
switches=_testcapi.get_context_switches(which_watcher)
581+
exceptValueError:
582+
switches=None
583+
try:
584+
yieldswitches
581585
finally:
582586
_testcapi.clear_context_watcher(wid)
583587

584-
defassert_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))
588+
defassert_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)
594591

595592
deftest_context_object_events_dispatched(self):
596593
# verify that all counts are zero before any watchers are registered
597-
self.assert_event_counts(0,0,0,0)
594+
self.assert_event_counts(0,0)
598595

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

605602
# verify counts are as expected when first watcher is registered
606603
withself.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)
604+
self.assert_event_counts(0,0)
605+
ctx.run(self.assert_event_counts,1,0)
606+
self.assert_event_counts(2,0)
610607

611608
# again with second watcher registered
612609
withself.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)
610+
self.assert_event_counts(2,0)
611+
ctx.run(self.assert_event_counts,3,1)
612+
self.assert_event_counts(4,2)
616613

617614
# 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-
deftest_enter_error(self):
622-
withself.context_watcher(2):
623-
withcatch_unraisable_exception()ascm:
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-
deftest_exit_error(self):
634-
ctx=contextvars.copy_context()
635-
def_in_context(stack):
636-
stack.enter_context(self.context_watcher(2))
637-
638-
withcatch_unraisable_exception()ascm:
639-
withExitStack()asstack:
640-
ctx.run(_in_context,stack)
641-
self.assertEqual(str(cm.unraisable.exc_value),"boom!")
615+
ctx.run(self.assert_event_counts,0,0)
616+
self.assert_event_counts(0,0)
617+
618+
deftest_callback_error(self):
619+
ctx_outer=contextvars.copy_context()
620+
ctx_inner=contextvars.copy_context()
621+
unraisables= []
622+
623+
def_in_outer():
624+
withself.context_watcher(2):
625+
withcatch_unraisable_exception()ascm:
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_msgforxinunraisables],
631+
["Exception ignored in Py_CONTEXT_SWITCHED "
632+
f"watcher callback for{ctx!r}"
633+
forctxin [ctx_inner,ctx_outer]])
634+
self.assertEqual([str(x.exc_value)forxinunraisables],
635+
["boom!","boom!"])
642636

643637
deftest_exception_save(self):
644638
withself.context_watcher(2):
@@ -664,5 +658,12 @@ def test_allocate_too_many_watchers(self):
664658
withself.assertRaisesRegex(RuntimeError,r"no more context watcher IDs available"):
665659
_testcapi.allocate_too_many_context_watchers()
666660

661+
deftest_exit_base_context(self):
662+
ctx=contextvars.Context()
663+
_testcapi.clear_context_stack()
664+
withself.context_watcher(0)asswitches:
665+
ctx.run(lambda:None)
666+
self.assertEqual(switches, [ctx,None])
667+
667668
if__name__=="__main__":
668669
unittest.main()

‎Modules/_testcapi/watchers.c‎

Lines changed: 41 additions & 38 deletions
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
#defineNUM_CONTEXT_WATCHERS 2
628628
staticintcontext_watcher_ids[NUM_CONTEXT_WATCHERS]= {-1,-1};
629-
staticintnum_context_object_enter_events[NUM_CONTEXT_WATCHERS]= {0,0};
630-
staticintnum_context_object_exit_events[NUM_CONTEXT_WATCHERS]= {0,0};
629+
staticPyObject*context_switches[NUM_CONTEXT_WATCHERS];
631630

632631
staticvoid
633632
handle_context_watcher_event(intwhich_watcher,PyContextEventevent,PyObject*ctx) {
634-
if (event==Py_CONTEXT_EVENT_ENTER) {
635-
num_context_object_enter_events[which_watcher]++;
636-
}
637-
elseif (event==Py_CONTEXT_EVENT_EXIT) {
638-
num_context_object_exit_events[which_watcher]++;
633+
if (event==Py_CONTEXT_SWITCHED) {
634+
PyList_Append(context_switches[which_watcher],ctx);
639635
}
640636
else {
641637
Py_UNREACHABLE();
@@ -664,31 +660,28 @@ error_context_event_handler(PyContextEvent event, PyObject *ctx) {
664660
staticPyObject*
665661
add_context_watcher(PyObject*self,PyObject*which_watcher)
666662
{
667-
intwatcher_id;
663+
staticconstPyContext_WatchCallbackcallbacks[]= {
664+
&first_context_watcher_callback,
665+
&second_context_watcher_callback,
666+
&error_context_event_handler,
667+
};
668668
assert(PyLong_Check(which_watcher));
669669
longwhich_l=PyLong_AsLong(which_watcher);
670-
if (which_l==0) {
671-
watcher_id=PyContext_AddWatcher(first_context_watcher_callback);
672-
context_watcher_ids[0]=watcher_id;
673-
num_context_object_enter_events[0]=0;
674-
num_context_object_exit_events[0]=0;
675-
}
676-
elseif (which_l==1) {
677-
watcher_id=PyContext_AddWatcher(second_context_watcher_callback);
678-
context_watcher_ids[1]=watcher_id;
679-
num_context_object_enter_events[1]=0;
680-
num_context_object_exit_events[1]=0;
681-
}
682-
elseif (which_l==2) {
683-
watcher_id=PyContext_AddWatcher(error_context_event_handler);
684-
}
685-
else {
670+
if (which_l<0||which_l >= (long)Py_ARRAY_LENGTH(callbacks)) {
686671
PyErr_Format(PyExc_ValueError,"invalid watcher %d",which_l);
687672
returnNULL;
688673
}
674+
intwatcher_id=PyContext_AddWatcher(callbacks[which_l]);
689675
if (watcher_id<0) {
690676
returnNULL;
691677
}
678+
if (which_l >=0&&which_l<NUM_CONTEXT_WATCHERS) {
679+
context_watcher_ids[which_l]=watcher_id;
680+
Py_XSETREF(context_switches[which_l],PyList_New(0));
681+
if (context_switches[which_l]==NULL) {
682+
returnNULL;
683+
}
684+
}
692685
returnPyLong_FromLong(watcher_id);
693686
}
694687

@@ -705,30 +698,42 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id)
705698
for (inti=0;i<NUM_CONTEXT_WATCHERS;i++) {
706699
if (watcher_id_l==context_watcher_ids[i]) {
707700
context_watcher_ids[i]=-1;
708-
num_context_object_enter_events[i]=0;
709-
num_context_object_exit_events[i]=0;
701+
Py_CLEAR(context_switches[i]);
710702
}
711703
}
712704
}
713705
Py_RETURN_NONE;
714706
}
715707

716708
staticPyObject*
717-
get_context_watcher_num_enter_events(PyObject*self,PyObject*watcher_id)
709+
clear_context_stack(PyObject*self,PyObject*args)
718710
{
719-
assert(PyLong_Check(watcher_id));
720-
longwatcher_id_l=PyLong_AsLong(watcher_id);
721-
assert(watcher_id_l >=0&&watcher_id_l<NUM_CONTEXT_WATCHERS);
722-
returnPyLong_FromLong(num_context_object_enter_events[watcher_id_l]);
711+
PyThreadState*tstate=PyThreadState_Get();
712+
if (tstate->context==NULL) {
713+
Py_RETURN_NONE;
714+
}
715+
if (((PyContext*)tstate->context)->ctx_prev!=NULL) {
716+
PyErr_SetString(PyExc_RuntimeError,
717+
"must first exit all non-base contexts");
718+
returnNULL;
719+
}
720+
Py_CLEAR(tstate->context);
721+
Py_RETURN_NONE;
723722
}
724723

725724
staticPyObject*
726-
get_context_watcher_num_exit_events(PyObject*self,PyObject*watcher_id)
725+
get_context_switches(PyObject*self,PyObject*watcher_id)
727726
{
728727
assert(PyLong_Check(watcher_id));
729728
longwatcher_id_l=PyLong_AsLong(watcher_id);
730-
assert(watcher_id_l >=0&&watcher_id_l<NUM_CONTEXT_WATCHERS);
731-
returnPyLong_FromLong(num_context_object_exit_events[watcher_id_l]);
729+
if (watcher_id_l<0||watcher_id_l >=NUM_CONTEXT_WATCHERS) {
730+
PyErr_Format(PyExc_ValueError,"invalid watcher %d",watcher_id_l);
731+
returnNULL;
732+
}
733+
if (context_switches[watcher_id_l]==NULL) {
734+
returnPyList_New(0);
735+
}
736+
returnPy_NewRef(context_switches[watcher_id_l]);
732737
}
733738

734739
staticPyObject*
@@ -832,10 +837,8 @@ static PyMethodDef test_methods[] = {
832837
// Code object watchers.
833838
{"add_context_watcher",add_context_watcher,METH_O,NULL},
834839
{"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},
840+
{"clear_context_stack",clear_context_stack,METH_NOARGS,NULL},
841+
{"get_context_switches",get_context_switches,METH_O,NULL},
839842
{"allocate_too_many_context_watchers",
840843
(PyCFunction)allocate_too_many_context_watchers,METH_NOARGS,NULL},
841844
{NULL},

‎Python/context.c‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,19 +102,24 @@ PyContext_CopyCurrent(void)
102102
staticconstchar*
103103
context_event_name(PyContextEventevent) {
104104
switch (event) {
105-
casePy_CONTEXT_EVENT_ENTER:
106-
return"Py_CONTEXT_EVENT_ENTER";
107-
casePy_CONTEXT_EVENT_EXIT:
108-
return"Py_CONTEXT_EVENT_EXIT";
105+
casePy_CONTEXT_SWITCHED:
106+
return"Py_CONTEXT_SWITCHED";
109107
default:
110108
return"?";
111109
}
112110
Py_UNREACHABLE();
113111
}
114112

115113
staticvoid
116-
notify_context_watchers(PyThreadState*ts,PyContextEventevent,PyContext*ctx)
114+
notify_context_watchers(PyThreadState*ts,PyContextEventevent,PyObject*ctx)
117115
{
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+
}
118123
assert(Py_REFCNT(ctx)>0);
119124
PyInterpreterState*interp=ts->interp;
120125
assert(interp->_initialized);
@@ -126,7 +131,7 @@ notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyContext *ctx)
126131
PyContext_WatchCallbackcb=interp->context_watchers[i];
127132
assert(cb!=NULL);
128133
PyObject*exc=_PyErr_GetRaisedException(ts);
129-
cb(event,(PyObject*)ctx);
134+
cb(event,ctx);
130135
if (_PyErr_Occurred(ts)!=NULL) {
131136
PyErr_FormatUnraisable(
132137
"Exception ignored in %s watcher callback for %R",
@@ -196,7 +201,7 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx)
196201
ts->context=Py_NewRef(ctx);
197202
ts->context_ver++;
198203

199-
notify_context_watchers(ts,Py_CONTEXT_EVENT_ENTER,ctx);
204+
notify_context_watchers(ts,Py_CONTEXT_SWITCHED,ts->context);
200205
return0;
201206
}
202207

@@ -230,13 +235,13 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx)
230235
return-1;
231236
}
232237

233-
notify_context_watchers(ts,Py_CONTEXT_EVENT_EXIT,ctx);
234238
Py_SETREF(ts->context, (PyObject*)ctx->ctx_prev);
235239
ts->context_ver++;
236240

237241
ctx->ctx_prev=NULL;
238242
ctx->ctx_entered=0;
239243

244+
notify_context_watchers(ts,Py_CONTEXT_SWITCHED,ts->context);
240245
return0;
241246
}
242247

‎Tools/c-analyzer/cpython/ignored.tsv‎

Lines changed: 2 additions & 2 deletions
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-num_context_object_enter_events-
459-
Modules/_testcapi/watchers.c-num_context_object_exit_events-
458+
Modules/_testcapi/watchers.c-context_switches-
459+
Modules/_testcapi/watchers.cadd_context_watchercallbacks-
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)

[8]ページ先頭

©2009-2025 Movatter.jp