summaryrefslogtreecommitdiffstats
path: root/Lib/test/test_capi
diff options
context:
space:
mode:
authorRichard Hansen <rhansen@rhansen.org>2024-10-14 19:28:41 (GMT)
committerGitHub <noreply@github.com>2024-10-14 19:28:41 (GMT)
commit843d28f59d2616d052d9d45f31823976da07f0f3 (patch)
tree85ca3958261acfd6c320d1a306c1871b4993da6b /Lib/test/test_capi
parente99650b80ace3893c2a80b3f2a4aca99cb305191 (diff)
downloadcpython-843d28f59d2616d052d9d45f31823976da07f0f3.zip
cpython-843d28f59d2616d052d9d45f31823976da07f0f3.tar.gz
cpython-843d28f59d2616d052d9d45f31823976da07f0f3.tar.bz2
gh-124872: Replace enter/exit events with "switched" (#124776)
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 gh-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).
Diffstat (limited to 'Lib/test/test_capi')
-rw-r--r--Lib/test/test_capi/test_watchers.py89
1 files changed, 45 insertions, 44 deletions
diff --git a/Lib/test/test_capi/test_watchers.py b/Lib/test/test_capi/test_watchers.py
index f21d262..4680d67 100644
--- a/Lib/test/test_capi/test_watchers.py
+++ b/Lib/test/test_capi/test_watchers.py
@@ -577,68 +577,62 @@ class TestContextObjectWatchers(unittest.TestCase):
def context_watcher(self, which_watcher):
wid = _testcapi.add_context_watcher(which_watcher)
try:
- yield wid
+ switches = _testcapi.get_context_switches(which_watcher)
+ except ValueError:
+ switches = None
+ try:
+ yield switches
finally:
_testcapi.clear_context_watcher(wid)
- def assert_event_counts(self, exp_enter_0, exp_exit_0,
- exp_enter_1, exp_exit_1):
- self.assertEqual(
- exp_enter_0, _testcapi.get_context_watcher_num_enter_events(0))
- self.assertEqual(
- exp_exit_0, _testcapi.get_context_watcher_num_exit_events(0))
- self.assertEqual(
- exp_enter_1, _testcapi.get_context_watcher_num_enter_events(1))
- self.assertEqual(
- exp_exit_1, _testcapi.get_context_watcher_num_exit_events(1))
+ def assert_event_counts(self, want_0, want_1):
+ self.assertEqual(len(_testcapi.get_context_switches(0)), want_0)
+ self.assertEqual(len(_testcapi.get_context_switches(1)), want_1)
def test_context_object_events_dispatched(self):
# verify that all counts are zero before any watchers are registered
- self.assert_event_counts(0, 0, 0, 0)
+ self.assert_event_counts(0, 0)
# verify that all counts remain zero when a context object is
# entered and exited with no watchers registered
ctx = contextvars.copy_context()
- ctx.run(self.assert_event_counts, 0, 0, 0, 0)
- self.assert_event_counts(0, 0, 0, 0)
+ ctx.run(self.assert_event_counts, 0, 0)
+ self.assert_event_counts(0, 0)
# verify counts are as expected when first watcher is registered
with self.context_watcher(0):
- self.assert_event_counts(0, 0, 0, 0)
- ctx.run(self.assert_event_counts, 1, 0, 0, 0)
- self.assert_event_counts(1, 1, 0, 0)
+ self.assert_event_counts(0, 0)
+ ctx.run(self.assert_event_counts, 1, 0)
+ self.assert_event_counts(2, 0)
# again with second watcher registered
with self.context_watcher(1):
- self.assert_event_counts(1, 1, 0, 0)
- ctx.run(self.assert_event_counts, 2, 1, 1, 0)
- self.assert_event_counts(2, 2, 1, 1)
+ self.assert_event_counts(2, 0)
+ ctx.run(self.assert_event_counts, 3, 1)
+ self.assert_event_counts(4, 2)
# verify counts are reset and don't change after both watchers are cleared
- ctx.run(self.assert_event_counts, 0, 0, 0, 0)
- self.assert_event_counts(0, 0, 0, 0)
-
- def test_enter_error(self):
- with self.context_watcher(2):
- with catch_unraisable_exception() as cm:
- ctx = contextvars.copy_context()
- ctx.run(int, 0)
- self.assertEqual(
- cm.unraisable.err_msg,
- "Exception ignored in "
- f"Py_CONTEXT_EVENT_EXIT watcher callback for {ctx!r}"
- )
- self.assertEqual(str(cm.unraisable.exc_value), "boom!")
-
- def test_exit_error(self):
- ctx = contextvars.copy_context()
- def _in_context(stack):
- stack.enter_context(self.context_watcher(2))
-
- with catch_unraisable_exception() as cm:
- with ExitStack() as stack:
- ctx.run(_in_context, stack)
- self.assertEqual(str(cm.unraisable.exc_value), "boom!")
+ ctx.run(self.assert_event_counts, 0, 0)
+ self.assert_event_counts(0, 0)
+
+ def test_callback_error(self):
+ ctx_outer = contextvars.copy_context()
+ ctx_inner = contextvars.copy_context()
+ unraisables = []
+
+ def _in_outer():
+ with self.context_watcher(2):
+ with catch_unraisable_exception() as cm:
+ ctx_inner.run(lambda: unraisables.append(cm.unraisable))
+ unraisables.append(cm.unraisable)
+
+ ctx_outer.run(_in_outer)
+ self.assertEqual([x.err_msg for x in unraisables],
+ ["Exception ignored in Py_CONTEXT_SWITCHED "
+ f"watcher callback for {ctx!r}"
+ for ctx in [ctx_inner, ctx_outer]])
+ self.assertEqual([str(x.exc_value) for x in unraisables],
+ ["boom!", "boom!"])
def test_clear_out_of_range_watcher_id(self):
with self.assertRaisesRegex(ValueError, r"Invalid context watcher ID -1"):
@@ -654,5 +648,12 @@ class TestContextObjectWatchers(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, r"no more context watcher IDs available"):
_testcapi.allocate_too_many_context_watchers()
+ def test_exit_base_context(self):
+ ctx = contextvars.Context()
+ _testcapi.clear_context_stack()
+ with self.context_watcher(0) as switches:
+ ctx.run(lambda: None)
+ self.assertEqual(switches, [ctx, None])
+
if __name__ == "__main__":
unittest.main()