Skip to content

Commit 7bba92f

Browse files
carljmmpage
authored andcommitted
pythongh-91052: Add PyDict_Unwatch for unwatching a dictionary (python#98055)
1 parent 776f894 commit 7bba92f

File tree

6 files changed

+115
-28
lines changed

6 files changed

+115
-28
lines changed

Doc/c-api/dict.rst

+20-1
Original file line numberDiff line numberDiff line change
@@ -246,24 +246,41 @@ Dictionary Objects
246246
of error (e.g. no more watcher IDs available), return ``-1`` and set an
247247
exception.
248248
249+
.. versionadded:: 3.12
250+
249251
.. c:function:: int PyDict_ClearWatcher(int watcher_id)
250252
251253
Clear watcher identified by *watcher_id* previously returned from
252254
:c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
253255
if the given *watcher_id* was never registered.)
254256
257+
.. versionadded:: 3.12
258+
255259
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)
256260
257261
Mark dictionary *dict* as watched. The callback granted *watcher_id* by
258262
:c:func:`PyDict_AddWatcher` will be called when *dict* is modified or
259-
deallocated.
263+
deallocated. Return ``0`` on success or ``-1`` on error.
264+
265+
.. versionadded:: 3.12
266+
267+
.. c:function:: int PyDict_Unwatch(int watcher_id, PyObject *dict)
268+
269+
Mark dictionary *dict* as no longer watched. The callback granted
270+
*watcher_id* by :c:func:`PyDict_AddWatcher` will no longer be called when
271+
*dict* is modified or deallocated. The dict must previously have been
272+
watched by this watcher. Return ``0`` on success or ``-1`` on error.
273+
274+
.. versionadded:: 3.12
260275
261276
.. c:type:: PyDict_WatchEvent
262277
263278
Enumeration of possible dictionary watcher events: ``PyDict_EVENT_ADDED``,
264279
``PyDict_EVENT_MODIFIED``, ``PyDict_EVENT_DELETED``, ``PyDict_EVENT_CLONED``,
265280
``PyDict_EVENT_CLEARED``, or ``PyDict_EVENT_DEALLOCATED``.
266281
282+
.. versionadded:: 3.12
283+
267284
.. c:type:: int (*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *new_value)
268285
269286
Type of a dict watcher callback function.
@@ -289,3 +306,5 @@ Dictionary Objects
289306
If the callback returns with an exception set, it must return ``-1``; this
290307
exception will be printed as an unraisable exception using
291308
:c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
309+
310+
.. versionadded:: 3.12

Doc/whatsnew/3.12.rst

+5
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,11 @@ New Features
546546
which sets the vectorcall field of a given :c:type:`PyFunctionObject`.
547547
(Contributed by Andrew Frost in :gh:`92257`.)
548548

549+
* The C API now permits registering callbacks via :c:func:`PyDict_AddWatcher`,
550+
:c:func:`PyDict_AddWatch` and related APIs to be called whenever a dictionary
551+
is modified. This is intended for use by optimizing interpreters, JIT
552+
compilers, or debuggers.
553+
549554
Porting to Python 3.12
550555
----------------------
551556

Include/cpython/dictobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
106106

107107
// Mark given dictionary as "watched" (callback will be called if it is modified)
108108
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
109+
PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);

Lib/test/test_capi.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import weakref
2121
from test import support
2222
from test.support import MISSING_C_DOCSTRINGS
23+
from test.support import catch_unraisable_exception
2324
from test.support import import_helper
2425
from test.support import threading_helper
2526
from test.support import warnings_helper
@@ -1421,6 +1422,9 @@ def assert_events(self, expected):
14211422
def watch(self, wid, d):
14221423
_testcapi.watch_dict(wid, d)
14231424

1425+
def unwatch(self, wid, d):
1426+
_testcapi.unwatch_dict(wid, d)
1427+
14241428
def test_set_new_item(self):
14251429
d = {}
14261430
with self.watcher() as wid:
@@ -1477,27 +1481,24 @@ def test_dealloc(self):
14771481
del d
14781482
self.assert_events(["dealloc"])
14791483

1484+
def test_unwatch(self):
1485+
d = {}
1486+
with self.watcher() as wid:
1487+
self.watch(wid, d)
1488+
d["foo"] = "bar"
1489+
self.unwatch(wid, d)
1490+
d["hmm"] = "baz"
1491+
self.assert_events(["new:foo:bar"])
1492+
14801493
def test_error(self):
14811494
d = {}
1482-
unraisables = []
1483-
def unraisable_hook(unraisable):
1484-
unraisables.append(unraisable)
14851495
with self.watcher(kind=self.ERROR) as wid:
14861496
self.watch(wid, d)
1487-
orig_unraisable_hook = sys.unraisablehook
1488-
sys.unraisablehook = unraisable_hook
1489-
try:
1497+
with catch_unraisable_exception() as cm:
14901498
d["foo"] = "bar"
1491-
finally:
1492-
sys.unraisablehook = orig_unraisable_hook
1499+
self.assertIs(cm.unraisable.object, d)
1500+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
14931501
self.assert_events([])
1494-
self.assertEqual(len(unraisables), 1)
1495-
unraisable = unraisables[0]
1496-
self.assertIs(unraisable.object, d)
1497-
self.assertEqual(str(unraisable.exc_value), "boom!")
1498-
# avoid leaking reference cycles
1499-
del unraisable
1500-
del unraisables
15011502

15021503
def test_two_watchers(self):
15031504
d1 = {}
@@ -1522,11 +1523,38 @@ def test_watch_out_of_range_watcher_id(self):
15221523
with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID 8"):
15231524
self.watch(8, d) # DICT_MAX_WATCHERS = 8
15241525

1525-
def test_unassigned_watcher_id(self):
1526+
def test_watch_unassigned_watcher_id(self):
15261527
d = {}
15271528
with self.assertRaisesRegex(ValueError, r"No dict watcher set for ID 1"):
15281529
self.watch(1, d)
15291530

1531+
def test_unwatch_non_dict(self):
1532+
with self.watcher() as wid:
1533+
with self.assertRaisesRegex(ValueError, r"Cannot watch non-dictionary"):
1534+
self.unwatch(wid, 1)
1535+
1536+
def test_unwatch_out_of_range_watcher_id(self):
1537+
d = {}
1538+
with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID -1"):
1539+
self.unwatch(-1, d)
1540+
with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID 8"):
1541+
self.unwatch(8, d) # DICT_MAX_WATCHERS = 8
1542+
1543+
def test_unwatch_unassigned_watcher_id(self):
1544+
d = {}
1545+
with self.assertRaisesRegex(ValueError, r"No dict watcher set for ID 1"):
1546+
self.unwatch(1, d)
1547+
1548+
def test_clear_out_of_range_watcher_id(self):
1549+
with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID -1"):
1550+
self.clear_watcher(-1)
1551+
with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID 8"):
1552+
self.clear_watcher(8) # DICT_MAX_WATCHERS = 8
1553+
1554+
def test_clear_unassigned_watcher_id(self):
1555+
with self.assertRaisesRegex(ValueError, r"No dict watcher set for ID 1"):
1556+
self.clear_watcher(1)
1557+
15301558

15311559
if __name__ == "__main__":
15321560
unittest.main()

Modules/_testcapimodule.c

+15
Original file line numberDiff line numberDiff line change
@@ -5296,6 +5296,20 @@ watch_dict(PyObject *self, PyObject *args)
52965296
Py_RETURN_NONE;
52975297
}
52985298

5299+
static PyObject *
5300+
unwatch_dict(PyObject *self, PyObject *args)
5301+
{
5302+
PyObject *dict;
5303+
int watcher_id;
5304+
if (!PyArg_ParseTuple(args, "iO", &watcher_id, &dict)) {
5305+
return NULL;
5306+
}
5307+
if (PyDict_Unwatch(watcher_id, dict)) {
5308+
return NULL;
5309+
}
5310+
Py_RETURN_NONE;
5311+
}
5312+
52995313
static PyObject *
53005314
get_dict_watcher_events(PyObject *self, PyObject *Py_UNUSED(args))
53015315
{
@@ -5904,6 +5918,7 @@ static PyMethodDef TestMethods[] = {
59045918
{"add_dict_watcher", add_dict_watcher, METH_O, NULL},
59055919
{"clear_dict_watcher", clear_dict_watcher, METH_O, NULL},
59065920
{"watch_dict", watch_dict, METH_VARARGS, NULL},
5921+
{"unwatch_dict", unwatch_dict, METH_VARARGS, NULL},
59075922
{"get_dict_watcher_events", get_dict_watcher_events, METH_NOARGS, NULL},
59085923
{NULL, NULL} /* sentinel */
59095924
};

Objects/dictobject.c

+30-11
Original file line numberDiff line numberDiff line change
@@ -5720,23 +5720,47 @@ uint32_t _PyDictKeys_GetVersionForCurrentState(PyDictKeysObject *dictkeys)
57205720
return v;
57215721
}
57225722

5723+
static inline int
5724+
validate_watcher_id(PyInterpreterState *interp, int watcher_id)
5725+
{
5726+
if (watcher_id < 0 || watcher_id >= DICT_MAX_WATCHERS) {
5727+
PyErr_Format(PyExc_ValueError, "Invalid dict watcher ID %d", watcher_id);
5728+
return -1;
5729+
}
5730+
if (!interp->dict_watchers[watcher_id]) {
5731+
PyErr_Format(PyExc_ValueError, "No dict watcher set for ID %d", watcher_id);
5732+
return -1;
5733+
}
5734+
return 0;
5735+
}
5736+
57235737
int
57245738
PyDict_Watch(int watcher_id, PyObject* dict)
57255739
{
57265740
if (!PyDict_Check(dict)) {
57275741
PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary");
57285742
return -1;
57295743
}
5730-
if (watcher_id < 0 || watcher_id >= DICT_MAX_WATCHERS) {
5731-
PyErr_Format(PyExc_ValueError, "Invalid dict watcher ID %d", watcher_id);
5744+
PyInterpreterState *interp = _PyInterpreterState_GET();
5745+
if (validate_watcher_id(interp, watcher_id)) {
5746+
return -1;
5747+
}
5748+
((PyDictObject*)dict)->ma_version_tag |= (1LL << watcher_id);
5749+
return 0;
5750+
}
5751+
5752+
int
5753+
PyDict_Unwatch(int watcher_id, PyObject* dict)
5754+
{
5755+
if (!PyDict_Check(dict)) {
5756+
PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary");
57325757
return -1;
57335758
}
57345759
PyInterpreterState *interp = _PyInterpreterState_GET();
5735-
if (!interp->dict_watchers[watcher_id]) {
5736-
PyErr_Format(PyExc_ValueError, "No dict watcher set for ID %d", watcher_id);
5760+
if (validate_watcher_id(interp, watcher_id)) {
57375761
return -1;
57385762
}
5739-
((PyDictObject*)dict)->ma_version_tag |= (1LL << watcher_id);
5763+
((PyDictObject*)dict)->ma_version_tag &= ~(1LL << watcher_id);
57405764
return 0;
57415765
}
57425766

@@ -5759,13 +5783,8 @@ PyDict_AddWatcher(PyDict_WatchCallback callback)
57595783
int
57605784
PyDict_ClearWatcher(int watcher_id)
57615785
{
5762-
if (watcher_id < 0 || watcher_id >= DICT_MAX_WATCHERS) {
5763-
PyErr_Format(PyExc_ValueError, "Invalid dict watcher ID %d", watcher_id);
5764-
return -1;
5765-
}
57665786
PyInterpreterState *interp = _PyInterpreterState_GET();
5767-
if (!interp->dict_watchers[watcher_id]) {
5768-
PyErr_Format(PyExc_ValueError, "No dict watcher set for ID %d", watcher_id);
5787+
if (validate_watcher_id(interp, watcher_id)) {
57695788
return -1;
57705789
}
57715790
interp->dict_watchers[watcher_id] = NULL;

0 commit comments

Comments
 (0)