Skip to content

Commit c9e95d4

Browse files
committed
pythongh-127124: Pass optional state to context watcher callback
This enables users to associate state with the callback without relying on globals. Also: * Refactor the tests for improved readability and extensibility, and to cover the new state object. * Drop the pointer from the `PyContext_WatchCallback` typedef. This de-obfuscates the fact that pointers are involved, and makes it possible to forward-declare functions to improve readability: static PyContext_WatchCallback my_callback; int my_callback(PyObject *cbarg, PyContextEvent event, PyObject *obj) { ... }
1 parent f7bb658 commit c9e95d4

File tree

10 files changed

+145
-164
lines changed

10 files changed

+145
-164
lines changed

Doc/c-api/contextvars.rst

+18-12
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,25 @@ Context object management functions:
101101
current context for the current thread. Returns ``0`` on success,
102102
and ``-1`` on error.
103103
104-
.. c:function:: int PyContext_AddWatcher(PyContext_WatchCallback callback)
104+
.. c:function:: int PyContext_AddWatcher(PyContext_WatchCallback *callback, PyObject *cbarg)
105105
106-
Register *callback* as a context object watcher for the current interpreter.
107-
Return an ID which may be passed to :c:func:`PyContext_ClearWatcher`.
108-
In case of error (e.g. no more watcher IDs available),
109-
return ``-1`` and set an exception.
106+
Registers *callback* as a context object watcher for the current
107+
interpreter, and *cbarg* (which may be NULL; if not, this function creates a
108+
new reference) as the object to pass as the callback's first parameter. On
109+
success, returns a non-negative ID which may be passed to
110+
:c:func:`PyContext_ClearWatcher` to unregister the callback and remove the
111+
added reference to *cbarg*. Sets an exception and returns ``-1`` on error
112+
(e.g., no more watcher IDs available).
110113
111114
.. versionadded:: 3.14
112115
113116
.. c:function:: int PyContext_ClearWatcher(int watcher_id)
114117
115-
Clear watcher identified by *watcher_id* previously returned from
116-
:c:func:`PyContext_AddWatcher` for the current interpreter.
117-
Return ``0`` on success, or ``-1`` and set an exception on error
118-
(e.g. if the given *watcher_id* was never registered.)
118+
Clears the watcher identified by *watcher_id* previously returned from
119+
:c:func:`PyContext_AddWatcher` for the current interpreter, and removes the
120+
reference created for the *cbarg* object that was registered with the
121+
callback. Returns ``0`` on success, or sets an exception and returns ``-1``
122+
on error (e.g., if the given *watcher_id* was never registered).
119123
120124
.. versionadded:: 3.14
121125
@@ -130,10 +134,12 @@ Context object management functions:
130134
131135
.. versionadded:: 3.14
132136
133-
.. c:type:: int (*PyContext_WatchCallback)(PyContextEvent event, PyObject *obj)
137+
.. c:type:: int PyContext_WatchCallback(PyObject *cbarg, PyContextEvent event, PyObject *obj)
134138
135-
Context object watcher callback function. The object passed to the callback
136-
is event-specific; see :c:type:`PyContextEvent` for details.
139+
Context object watcher callback function. *cbarg* is the same object
140+
registered in the call to :c:func:`PyContext_AddWatcher`, as a borrowed
141+
reference if non-NULL. The *obj* object is event-specific; see
142+
:c:type:`PyContextEvent` for details.
137143
138144
If the callback returns with an exception set, it must return ``-1``; this
139145
exception will be printed as an unraisable exception using

Include/cpython/context.h

+4-23
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,10 @@ typedef enum {
3636
Py_CONTEXT_SWITCHED = 1,
3737
} PyContextEvent;
3838

39-
/*
40-
* Context object watcher callback function. The object passed to the callback
41-
* is event-specific; see PyContextEvent for details.
42-
*
43-
* if the callback returns with an exception set, it must return -1. Otherwise
44-
* it should return 0
45-
*/
46-
typedef int (*PyContext_WatchCallback)(PyContextEvent, PyObject *);
47-
48-
/*
49-
* Register a per-interpreter callback that will be invoked for context object
50-
* enter/exit events.
51-
*
52-
* Returns a handle that may be passed to PyContext_ClearWatcher on success,
53-
* or -1 and sets and error if no more handles are available.
54-
*/
55-
PyAPI_FUNC(int) PyContext_AddWatcher(PyContext_WatchCallback callback);
56-
57-
/*
58-
* Clear the watcher associated with the watcher_id handle.
59-
*
60-
* Returns 0 on success or -1 if no watcher exists for the provided id.
61-
*/
39+
typedef int PyContext_WatchCallback(
40+
PyObject *cbarg, PyContextEvent event, PyObject *obj);
41+
PyAPI_FUNC(int) PyContext_AddWatcher(
42+
PyContext_WatchCallback *callback, PyObject *cbarg);
6243
PyAPI_FUNC(int) PyContext_ClearWatcher(int watcher_id);
6344

6445
/* Create a new context variable.

Include/internal/pycore_interp.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,10 @@ struct _is {
242242
PyObject *audit_hooks;
243243
PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS];
244244
PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS];
245-
PyContext_WatchCallback context_watchers[CONTEXT_MAX_WATCHERS];
245+
struct {
246+
PyContext_WatchCallback *callback;
247+
PyObject *arg;
248+
} context_watchers[CONTEXT_MAX_WATCHERS];
246249
// One bit is set for each non-NULL entry in code_watchers
247250
uint8_t active_code_watchers;
248251
uint8_t active_context_watchers;

Lib/test/test_capi/test_watchers.py

+19-36
Original file line numberDiff line numberDiff line change
@@ -589,60 +589,43 @@ def test_allocate_too_many_watchers(self):
589589

590590
class TestContextObjectWatchers(unittest.TestCase):
591591
@contextmanager
592-
def context_watcher(self, which_watcher):
593-
wid = _testcapi.add_context_watcher(which_watcher)
592+
def context_watcher(self, cfg=None):
593+
if cfg is None:
594+
cfg = {}
595+
cfg.setdefault('log', [])
596+
wid = _testcapi.add_context_watcher(cfg)
594597
try:
595-
switches = _testcapi.get_context_switches(which_watcher)
596-
except ValueError:
597-
switches = None
598-
try:
599-
yield switches
598+
yield cfg.get('log', None)
600599
finally:
601600
_testcapi.clear_context_watcher(wid)
602601

603-
def assert_event_counts(self, want_0, want_1):
604-
self.assertEqual(len(_testcapi.get_context_switches(0)), want_0)
605-
self.assertEqual(len(_testcapi.get_context_switches(1)), want_1)
606-
607602
def test_context_object_events_dispatched(self):
608-
# verify that all counts are zero before any watchers are registered
609-
self.assert_event_counts(0, 0)
610-
611-
# verify that all counts remain zero when a context object is
612-
# entered and exited with no watchers registered
613603
ctx = contextvars.copy_context()
614-
ctx.run(self.assert_event_counts, 0, 0)
615-
self.assert_event_counts(0, 0)
616-
617-
# verify counts are as expected when first watcher is registered
618-
with self.context_watcher(0):
619-
self.assert_event_counts(0, 0)
620-
ctx.run(self.assert_event_counts, 1, 0)
621-
self.assert_event_counts(2, 0)
622-
623-
# again with second watcher registered
624-
with self.context_watcher(1):
625-
self.assert_event_counts(2, 0)
626-
ctx.run(self.assert_event_counts, 3, 1)
627-
self.assert_event_counts(4, 2)
628-
629-
# verify counts are reset and don't change after both watchers are cleared
630-
ctx.run(self.assert_event_counts, 0, 0)
631-
self.assert_event_counts(0, 0)
604+
with self.context_watcher() as switches_0:
605+
self.assertEqual(len(switches_0), 0)
606+
ctx.run(lambda: self.assertEqual(len(switches_0), 1))
607+
self.assertEqual(len(switches_0), 2)
608+
with self.context_watcher() as switches_1:
609+
self.assertEqual((len(switches_0), len(switches_1)), (2, 0))
610+
ctx.run(lambda: self.assertEqual(
611+
(len(switches_0), len(switches_1)), (3, 1)))
612+
self.assertEqual((len(switches_0), len(switches_1)), (4, 2))
632613

633614
def test_callback_error(self):
634615
ctx_outer = contextvars.copy_context()
635616
ctx_inner = contextvars.copy_context()
636617
unraisables = []
637618

638619
def _in_outer():
639-
with self.context_watcher(2):
620+
with self.context_watcher(cfg={'err': RuntimeError('boom!')}):
640621
with catch_unraisable_exception() as cm:
641622
ctx_inner.run(lambda: unraisables.append(cm.unraisable))
642623
unraisables.append(cm.unraisable)
643624

644625
try:
645626
ctx_outer.run(_in_outer)
627+
self.assertEqual([x is not None for x in unraisables],
628+
[True, True])
646629
self.assertEqual([x.err_msg for x in unraisables],
647630
["Exception ignored in Py_CONTEXT_SWITCHED "
648631
f"watcher callback for {ctx!r}"
@@ -670,7 +653,7 @@ def test_allocate_too_many_watchers(self):
670653
def test_exit_base_context(self):
671654
ctx = contextvars.Context()
672655
_testcapi.clear_context_stack()
673-
with self.context_watcher(0) as switches:
656+
with self.context_watcher() as switches:
674657
ctx.run(lambda: None)
675658
self.assertEqual(switches, [ctx, None])
676659

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added optional callback state to :c:func:`PyContext_AddWatcher` and
2+
:c:type:`PyContext_WatchCallback`.

Modules/_testcapi/clinic/watchers.c.h

+36-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)