| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 330c527 commit b4cff77
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -123,16 +123,10 @@ Context object management functions: | |||
| 123 | 123 | ||
| 124 | 124 | Enumeration of possible context object watcher events: | |
| 125 | 125 | ||
| 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. | ||
| 136 | 130 | ||
| 137 | 131 | .. versionadded:: 3.14 | |
| 138 | 132 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,20 +29,11 @@ PyAPI_FUNC(int) PyContext_Exit(PyObject *); | |||
| 29 | 29 | ||
| 30 | 30 | typedef enum { | |
| 31 | 31 | /* | |
| 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. | ||
| 37 | 35 | */ | |
| 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, | ||
| 46 | 37 | } PyContextEvent; | |
| 47 | 38 | ||
| 48 | 39 | /* | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -577,68 +577,62 @@ class TestContextObjectWatchers(unittest.TestCase): | |||
| 577 | 577 | def context_watcher(self, which_watcher): | |
| 578 | 578 | wid = _testcapi.add_context_watcher(which_watcher) | |
| 579 | 579 | try: | |
| 580 | - yield wid | ||
| 580 | + switches = _testcapi.get_context_switches(which_watcher) | ||
| 581 | + except ValueError: | ||
| 582 | + switches = None | ||
| 583 | + try: | ||
| 584 | + yield switches | ||
| 581 | 585 | finally: | |
| 582 | 586 | _testcapi.clear_context_watcher(wid) | |
| 583 | 587 | ||
| 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)) | ||
| 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) | ||
| 594 | 591 | ||
| 595 | 592 | def test_context_object_events_dispatched(self): | |
| 596 | 593 | # 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) | ||
| 598 | 595 | ||
| 599 | 596 | # verify that all counts remain zero when a context object is | |
| 600 | 597 | # entered and exited with no watchers registered | |
| 601 | 598 | 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) | ||
| 604 | 601 | ||
| 605 | 602 | # verify counts are as expected when first watcher is registered | |
| 606 | 603 | 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) | ||
| 604 | + self.assert_event_counts(0, 0) | ||
| 605 | + ctx.run(self.assert_event_counts, 1, 0) | ||
| 606 | + self.assert_event_counts(2, 0) | ||
| 610 | 607 | ||
| 611 | 608 | # again with second watcher registered | |
| 612 | 609 | 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) | ||
| 610 | + self.assert_event_counts(2, 0) | ||
| 611 | + ctx.run(self.assert_event_counts, 3, 1) | ||
| 612 | + self.assert_event_counts(4, 2) | ||
| 616 | 613 | ||
| 617 | 614 | # 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!") | ||
| 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!"]) | ||
| 642 | 636 | ||
| 643 | 637 | def test_clear_out_of_range_watcher_id(self): | |
| 644 | 638 | with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"): | |
@@ -654,5 +648,12 @@ def test_allocate_too_many_watchers(self): | |||
| 654 | 648 | with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"): | |
| 655 | 649 | _testcapi.allocate_too_many_context_watchers() | |
| 656 | 650 | ||
| 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 | + | ||
| 657 | 658 | if __name__ == "__main__": | |
| 658 | 659 | unittest.main() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -626,16 +626,12 @@ allocate_too_many_func_watchers(PyObject *self, PyObject *args) | |||
| 626 | 626 | // Test contexct object watchers | |
| 627 | 627 | #define NUM_CONTEXT_WATCHERS 2 | |
| 628 | 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}; | ||
| 629 | + static PyObject *context_switches[NUM_CONTEXT_WATCHERS]; | ||
| 631 | 630 | ||
| 632 | 631 | static int | |
| 633 | 632 | 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 | + PyList_Append(context_switches[which_watcher], ctx); | ||
| 639 | 635 | } | |
| 640 | 636 | else { | |
| 641 | 637 | return -1; | |
@@ -667,31 +663,28 @@ error_context_event_handler(PyContextEvent event, PyObject *ctx) { | |||
| 667 | 663 | static PyObject * | |
| 668 | 664 | add_context_watcher(PyObject *self, PyObject *which_watcher) | |
| 669 | 665 | { | |
| 670 | - int watcher_id; | ||
| 666 | + static const PyContext_WatchCallback callbacks[] = { | ||
| 667 | + &first_context_watcher_callback, | ||
| 668 | + &second_context_watcher_callback, | ||
| 669 | + &error_context_event_handler, | ||
| 670 | + }; | ||
| 671 | 671 | assert(PyLong_Check(which_watcher)); | |
| 672 | 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 { | ||
| 673 | + if (which_l < 0 || which_l >= (long)Py_ARRAY_LENGTH(callbacks)) { | ||
| 689 | 674 | PyErr_Format(PyExc_ValueError, "invalid watcher %d", which_l); | |
| 690 | 675 | return NULL; | |
| 691 | 676 | } | |
| 677 | + int watcher_id = PyContext_AddWatcher(callbacks[which_l]); | ||
| 692 | 678 | if (watcher_id < 0) { | |
| 693 | 679 | return NULL; | |
| 694 | 680 | } | |
| 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 | + } | ||
| 695 | 688 | return PyLong_FromLong(watcher_id); | |
| 696 | 689 | } | |
| 697 | 690 | ||
@@ -708,30 +701,42 @@ clear_context_watcher(PyObject *self, PyObject *watcher_id) | |||
| 708 | 701 | for (int i = 0; i < NUM_CONTEXT_WATCHERS; i++) { | |
| 709 | 702 | if (watcher_id_l == context_watcher_ids[i]) { | |
| 710 | 703 | context_watcher_ids[i] = -1; | |
| 711 | - num_context_object_enter_events[i] = 0; | ||
| 712 | - num_context_object_exit_events[i] = 0; | ||
| 704 | + Py_CLEAR(context_switches[i]); | ||
| 713 | 705 | } | |
| 714 | 706 | } | |
| 715 | 707 | } | |
| 716 | 708 | Py_RETURN_NONE; | |
| 717 | 709 | } | |
| 718 | 710 | ||
| 719 | 711 | static PyObject * | |
| 720 | - get_context_watcher_num_enter_events(PyObject *self, PyObject *watcher_id) | ||
| 712 | + clear_context_stack(PyObject *self, PyObject *args) | ||
| 721 | 713 | { | |
| 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]); | ||
| 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; | ||
| 726 | 725 | } | |
| 727 | 726 | ||
| 728 | 727 | static PyObject * | |
| 729 | - get_context_watcher_num_exit_events(PyObject *self, PyObject *watcher_id) | ||
| 728 | + get_context_switches(PyObject *self, PyObject *watcher_id) | ||
| 730 | 729 | { | |
| 731 | 730 | assert(PyLong_Check(watcher_id)); | |
| 732 | 731 | 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]); | ||
| 732 | + if (watcher_id_l < 0 || watcher_id_l >= NUM_CONTEXT_WATCHERS) { | ||
| 733 | + PyErr_Format(PyExc_ValueError, "invalid watcher %d", 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]); | ||
| 735 | 740 | } | |
| 736 | 741 | ||
| 737 | 742 | static PyObject * | |
@@ -835,10 +840,8 @@ static PyMethodDef test_methods[] = { | |||
| 835 | 840 | // Code object watchers. | |
| 836 | 841 | {"add_context_watcher", add_context_watcher, METH_O, NULL}, | |
| 837 | 842 | {"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}, | ||
| 843 | + {"clear_context_stack", clear_context_stack, METH_NOARGS, NULL}, | ||
| 844 | + {"get_context_switches", get_context_switches, METH_O, NULL}, | ||
| 842 | 845 | {"allocate_too_many_context_watchers", | |
| 843 | 846 | (PyCFunction) allocate_too_many_context_watchers, METH_NOARGS, NULL}, | |
| 844 | 847 | {NULL}, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -102,10 +102,8 @@ PyContext_CopyCurrent(void) | |||
| 102 | 102 | static const char * | |
| 103 | 103 | context_event_name(PyContextEvent event) { | |
| 104 | 104 | 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"; | ||
| 109 | 107 | default: | |
| 110 | 108 | return "?"; | |
| 111 | 109 | } | |
@@ -115,6 +113,13 @@ context_event_name(PyContextEvent event) { | |||
| 115 | 113 | static void | |
| 116 | 114 | notify_context_watchers(PyThreadState *ts, PyContextEvent event, PyObject *ctx) | |
| 117 | 115 | { | |
| 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 | + } | ||
| 118 | 123 | assert(Py_REFCNT(ctx) > 0); | |
| 119 | 124 | PyInterpreterState *interp = ts->interp; | |
| 120 | 125 | assert(interp->_initialized); | |
@@ -175,6 +180,16 @@ PyContext_ClearWatcher(int watcher_id) | |||
| 175 | 180 | } | |
| 176 | 181 | ||
| 177 | 182 | ||
| 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 | + | ||
| 178 | 193 | static int | |
| 179 | 194 | _PyContext_Enter(PyThreadState *ts, PyObject *octx) | |
| 180 | 195 | { | |
@@ -191,9 +206,7 @@ _PyContext_Enter(PyThreadState *ts, PyObject *octx) | |||
| 191 | 206 | ctx->ctx_entered = 1; | |
| 192 | 207 | ||
| 193 | 208 | ts->context = Py_NewRef(ctx); | |
| 194 | - ts->context_ver++; | ||
| 195 | - | ||
| 196 | - notify_context_watchers(ts, Py_CONTEXT_EVENT_ENTER, octx); | ||
| 209 | + context_switched(ts); | ||
| 197 | 210 | return 0; | |
| 198 | 211 | } | |
| 199 | 212 | ||
@@ -227,13 +240,11 @@ _PyContext_Exit(PyThreadState *ts, PyObject *octx) | |||
| 227 | 240 | return -1; | |
| 228 | 241 | } | |
| 229 | 242 | ||
| 230 | - notify_context_watchers(ts, Py_CONTEXT_EVENT_EXIT, octx); | ||
| 231 | 243 | Py_SETREF(ts->context, (PyObject *)ctx->ctx_prev); | |
| 232 | - ts->context_ver++; | ||
| 233 | 244 | ||
| 234 | 245 | ctx->ctx_prev = NULL; | |
| 235 | 246 | ctx->ctx_entered = 0; | |
| 236 | - | ||
| 247 | + context_switched(ts); | ||
| 237 | 248 | return 0; | |
| 238 | 249 | } | |
| 239 | 250 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -455,8 +455,8 @@ Modules/_testcapi/watchers.c - pyfunc_watchers - | |||
| 455 | 455 | Modules/_testcapi/watchers.c - func_watcher_ids - | |
| 456 | 456 | Modules/_testcapi/watchers.c - func_watcher_callbacks - | |
| 457 | 457 | 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.c add_context_watcher callbacks - | ||
| 460 | 460 | Modules/_testcapimodule.c - BasicStaticTypes - | |
| 461 | 461 | Modules/_testcapimodule.c - num_basic_static_types_used - | |
| 462 | 462 | Modules/_testcapimodule.c - ContainerNoGC_members - | |
| Back | FazBrowse Home | New Git URL |
0 commit comments