Skip to content

Commit 886ab4f

Browse files
author
Carl Meyer
committed
bpo-46896: Add C API for watching dictionaries
1 parent 46a116c commit 886ab4f

File tree

8 files changed

+433
-17
lines changed

8 files changed

+433
-17
lines changed

Doc/c-api/dict.rst

+53
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,56 @@ Dictionary Objects
238238
for key, value in seq2:
239239
if override or key not in a:
240240
a[key] = value
241+
242+
.. c:function:: void PyDict_Watch(PyObject *dict)
243+
244+
Mark dictionary *dict* as watched. The callback set via
245+
:c:func:`PyDict_SetWatchCallback` will be called when *dict* is modified or
246+
deallocated.
247+
248+
.. c:function:: int PyDict_IsWatched(PyObject *dict)
249+
250+
Return ``1`` if *dict* is marked as watched, ``0`` otherwise.
251+
252+
.. c:type:: PyDict_WatchEvent
253+
254+
Enumeration of possible dictionary watcher events: ``PyDict_EVENT_ADDED``,
255+
``PyDict_EVENT_MODIFIED``, ``PyDict_EVENT_DELETED``, ``PyDict_EVENT_CLONED``,
256+
``PyDict_EVENT_CLEARED``, or ``PyDict_EVENT_DEALLOCED``.
257+
258+
.. c:type:: void (*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *new_value)
259+
260+
Type of a dict watcher callback function.
261+
262+
If *event* is ``PyDict_EVENT_CLEARED`` or ``PyDict_EVENT_DEALLOCED``, both
263+
*key* and *new_value* will be ``NULL``. If *event* is
264+
``PyDict_EVENT_ADDED`` or ``PyDict_EVENT_MODIFIED``, *new_value* will be the
265+
new value for *key*. If *event* is ``PyDict_EVENT_DELETED``, *key* is being
266+
deleted from the dictionary and *new_value* will be ``NULL``.
267+
268+
``PyDict_EVENT_CLONED`` occurs when *dict* was previously empty and another
269+
dict is merged into it. To maintain efficiency of this operation, per-key
270+
``PyDict_EVENT_ADDED`` events are not issued in this case; instead a
271+
single ``PyDict_EVENT_CLONED`` is issued, and *key* will be the source
272+
dictionary.
273+
274+
.. c:function:: void PyDict_SetWatchCallback(PyDict_WatchCallback callback)
275+
276+
Set a callback for modification events on dictionaries watched via
277+
:c:func:`PyDict_Watch`.
278+
279+
There is only one callback per interpreter. Before setting the callback, you
280+
must check if there is one already set (use
281+
:c:func:`PyDict_GetWatchCallback`) and if so, call it from your own new
282+
callback. Failure to do this is a critical bug in your callback and may break
283+
other dict-watching clients.
284+
285+
The callback may inspect but should not modify *dict*; doing so could have
286+
unpredictable effects, including infinite recursion.
287+
288+
Callbacks occur before the notified modification to *dict* takes place, so
289+
the prior state of *dict* can be inspected.
290+
291+
.. c:function:: PyDict_WatchCallback PyDict_GetWatchCallback(void)
292+
293+
Return the existing dictionary watcher callback, or ``NULL`` if none has been set.

Include/cpython/dictobject.h

+28
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,31 @@ typedef struct {
7676

7777
PyAPI_FUNC(PyObject *) _PyDictView_New(PyObject *, PyTypeObject *);
7878
PyAPI_FUNC(PyObject *) _PyDictView_Intersect(PyObject* self, PyObject *other);
79+
80+
/* Dictionary watchers */
81+
82+
// Mark given dictionary as "watched" (callback will be called if it is modified)
83+
PyAPI_FUNC(int) PyDict_Watch(PyObject* dict);
84+
85+
// Check if given dictionary is watched
86+
PyAPI_FUNC(int) PyDict_IsWatched(PyObject* dict);
87+
88+
typedef enum {
89+
PyDict_EVENT_ADDED,
90+
PyDict_EVENT_MODIFIED,
91+
PyDict_EVENT_DELETED,
92+
PyDict_EVENT_CLONED,
93+
PyDict_EVENT_CLEARED,
94+
PyDict_EVENT_DEALLOCED,
95+
} PyDict_WatchEvent;
96+
97+
// Callback to be invoked when a watched dict is cleared, dealloced, or modified.
98+
// In clear/dealloc case, key and new_value will be NULL. Otherwise, new_value will be the
99+
// new value for key, NULL if key is being deleted.
100+
typedef void(*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject* dict, PyObject* key, PyObject* new_value);
101+
102+
// Set new global watch callback; supply NULL to clear callback
103+
PyAPI_FUNC(void) PyDict_SetWatchCallback(PyDict_WatchCallback callback);
104+
105+
// Get existing global watch callback
106+
PyAPI_FUNC(PyDict_WatchCallback) PyDict_GetWatchCallback(void);

Include/internal/pycore_dict.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ struct _dictvalues {
160160
#define DK_IS_UNICODE(dk) ((dk)->dk_kind != DICT_KEYS_GENERAL)
161161

162162
extern uint64_t _pydict_global_version;
163+
#define DICT_VERSION_WATCHED_TAG 1
163164

164-
#define DICT_NEXT_VERSION() (++_pydict_global_version)
165+
#define DICT_NEXT_VERSION() (_pydict_global_version += 2)
165166

166167
extern PyObject *_PyObject_MakeDictFromInstanceAttributes(PyObject *obj, PyDictValues *values);
167168
extern PyObject *_PyDict_FromItems(

Include/internal/pycore_interp.h

+2
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ struct _is {
147147
// Initialized to _PyEval_EvalFrameDefault().
148148
_PyFrameEvalFunction eval_frame;
149149

150+
void *dict_watch_callback;
151+
150152
Py_ssize_t co_extra_user_count;
151153
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
152154

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add API for subscribing to modification events on selected dictionaries.

Modules/_testcapimodule.c

+213
Original file line numberDiff line numberDiff line change
@@ -5775,6 +5775,218 @@ test_tstate_capi(PyObject *self, PyObject *Py_UNUSED(args))
57755775
}
57765776

57775777

5778+
// Test dict watching
5779+
static PyObject *g_dict_watch_events;
5780+
static PyDict_WatchCallback g_prev_callback;
5781+
5782+
static void
5783+
dict_watch_callback(PyDict_WatchEvent event,
5784+
PyObject *dict,
5785+
PyObject *key,
5786+
PyObject *new_value)
5787+
{
5788+
PyObject *msg;
5789+
switch(event) {
5790+
case PyDict_EVENT_CLEARED:
5791+
msg = PyUnicode_FromString("clear");
5792+
break;
5793+
case PyDict_EVENT_DEALLOCED:
5794+
msg = PyUnicode_FromString("dealloc");
5795+
break;
5796+
case PyDict_EVENT_CLONED:
5797+
msg = PyUnicode_FromString("clone");
5798+
break;
5799+
case PyDict_EVENT_ADDED:
5800+
msg = PyUnicode_FromFormat("new:%S:%S", key, new_value);
5801+
break;
5802+
case PyDict_EVENT_MODIFIED:
5803+
msg = PyUnicode_FromFormat("mod:%S:%S", key, new_value);
5804+
break;
5805+
case PyDict_EVENT_DELETED:
5806+
msg = PyUnicode_FromFormat("del:%S", key);
5807+
break;
5808+
default:
5809+
msg = PyUnicode_FromString("unknown");
5810+
}
5811+
assert(PyList_Check(g_dict_watch_events));
5812+
PyList_Append(g_dict_watch_events, msg);
5813+
if (g_prev_callback != NULL) {
5814+
g_prev_callback(event, dict, key, new_value);
5815+
}
5816+
}
5817+
5818+
static int
5819+
dict_watch_assert(Py_ssize_t expected_num_events,
5820+
const char *expected_last_msg)
5821+
{
5822+
char buf[512];
5823+
Py_ssize_t actual_num_events = PyList_Size(g_dict_watch_events);
5824+
if (expected_num_events != actual_num_events) {
5825+
snprintf(buf,
5826+
512,
5827+
"got %d dict watch events, expected %d",
5828+
(int)actual_num_events,
5829+
(int)expected_num_events);
5830+
raiseTestError("test_watch_dict", (const char *)&buf);
5831+
return -1;
5832+
}
5833+
PyObject *last_msg = PyList_GetItem(g_dict_watch_events,
5834+
PyList_Size(g_dict_watch_events)-1);
5835+
if (PyUnicode_CompareWithASCIIString(last_msg, expected_last_msg)) {
5836+
snprintf(buf,
5837+
512,
5838+
"last event is '%s', expected '%s'",
5839+
PyUnicode_AsUTF8(last_msg),
5840+
expected_last_msg);
5841+
raiseTestError("test_watch_dict", (const char *)&buf);
5842+
return -1;
5843+
}
5844+
return 0;
5845+
}
5846+
5847+
static int
5848+
try_watch(PyObject *obj) {
5849+
if (PyDict_Watch(obj)) {
5850+
raiseTestError("test_watch_dict", "PyDict_Watch() failed on dict");
5851+
return -1;
5852+
}
5853+
return 0;
5854+
}
5855+
5856+
static PyObject *
5857+
test_watch_dict(PyObject *self, PyObject *Py_UNUSED(args))
5858+
{
5859+
PyObject *watched = PyDict_New();
5860+
PyObject *unwatched = PyDict_New();
5861+
PyObject *one = PyLong_FromLong(1);
5862+
PyObject *two = PyLong_FromLong(2);
5863+
PyObject *key1 = PyUnicode_FromString("key1");
5864+
PyObject *key2 = PyUnicode_FromString("key2");
5865+
5866+
g_dict_watch_events = PyList_New(0);
5867+
g_prev_callback = PyDict_GetWatchCallback();
5868+
5869+
PyDict_SetWatchCallback(dict_watch_callback);
5870+
if (PyDict_GetWatchCallback() != dict_watch_callback) {
5871+
return raiseTestError("test_watch_dict", "GetWatchCallback did not return set callback");
5872+
}
5873+
if (try_watch(watched)) {
5874+
return NULL;
5875+
}
5876+
5877+
if (!PyDict_IsWatched(watched)) {
5878+
return raiseTestError("test_watch_dict", "IsWatched returned false for watched dict");
5879+
}
5880+
if (PyDict_IsWatched(unwatched)) {
5881+
return raiseTestError("test_watch_dict", "IsWatched returned true for unwatched dict");
5882+
}
5883+
5884+
PyDict_SetItem(unwatched, key1, two);
5885+
PyDict_Merge(watched, unwatched, 1);
5886+
5887+
if (dict_watch_assert(1, "clone")) {
5888+
return NULL;
5889+
}
5890+
5891+
PyDict_SetItem(watched, key1, one);
5892+
PyDict_SetItem(unwatched, key1, one);
5893+
5894+
if (dict_watch_assert(2, "mod:key1:1")) {
5895+
return NULL;
5896+
}
5897+
5898+
PyDict_SetItemString(watched, "key1", two);
5899+
PyDict_SetItemString(unwatched, "key1", two);
5900+
5901+
if (dict_watch_assert(3, "mod:key1:2")) {
5902+
return NULL;
5903+
}
5904+
5905+
PyDict_SetItem(watched, key2, one);
5906+
PyDict_SetItem(unwatched, key2, one);
5907+
5908+
if (dict_watch_assert(4, "new:key2:1")) {
5909+
return NULL;
5910+
}
5911+
5912+
_PyDict_Pop(watched, key2, Py_None);
5913+
_PyDict_Pop(unwatched, key2, Py_None);
5914+
5915+
if (dict_watch_assert(5, "del:key2")) {
5916+
return NULL;
5917+
}
5918+
5919+
PyDict_DelItemString(watched, "key1");
5920+
PyDict_DelItemString(unwatched, "key1");
5921+
5922+
if (dict_watch_assert(6, "del:key1")) {
5923+
return NULL;
5924+
}
5925+
5926+
PyDict_SetDefault(watched, key1, one);
5927+
PyDict_SetDefault(unwatched, key1, one);
5928+
5929+
if (dict_watch_assert(7, "new:key1:1")) {
5930+
return NULL;
5931+
}
5932+
5933+
PyDict_Clear(watched);
5934+
PyDict_Clear(unwatched);
5935+
5936+
if (dict_watch_assert(8, "clear")) {
5937+
return NULL;
5938+
}
5939+
5940+
PyObject *copy = PyDict_Copy(watched);
5941+
if (PyDict_IsWatched(copy)) {
5942+
return raiseTestError("test_watch_dict", "copying a watched dict should not watch the copy");
5943+
}
5944+
Py_CLEAR(copy);
5945+
5946+
Py_CLEAR(watched);
5947+
Py_CLEAR(unwatched);
5948+
5949+
if (dict_watch_assert(9, "dealloc")) {
5950+
return NULL;
5951+
}
5952+
5953+
PyDict_SetWatchCallback(g_prev_callback);
5954+
g_prev_callback = NULL;
5955+
5956+
// no events after callback unset
5957+
watched = PyDict_New();
5958+
if (try_watch(watched)) {
5959+
return NULL;
5960+
}
5961+
5962+
PyDict_SetItem(watched, key1, one);
5963+
Py_CLEAR(watched);
5964+
5965+
if (dict_watch_assert(9, "dealloc")) {
5966+
return NULL;
5967+
}
5968+
5969+
// it is an error to try to watch a non-dict
5970+
if (!PyDict_Watch(one)) {
5971+
raiseTestError("test_watch_dict", "PyDict_Watch() succeeded on non-dict");
5972+
return NULL;
5973+
} else if (!PyErr_Occurred()) {
5974+
raiseTestError("test_watch_dict", "PyDict_Watch() returned error code without exception set");
5975+
return NULL;
5976+
} else {
5977+
PyErr_Clear();
5978+
}
5979+
5980+
5981+
Py_CLEAR(g_dict_watch_events);
5982+
Py_DECREF(one);
5983+
Py_DECREF(two);
5984+
Py_DECREF(key1);
5985+
Py_DECREF(key2);
5986+
Py_RETURN_NONE;
5987+
}
5988+
5989+
57785990
static PyObject *negative_dictoffset(PyObject *, PyObject *);
57795991
static PyObject *test_buildvalue_issue38913(PyObject *, PyObject *);
57805992
static PyObject *getargs_s_hash_int(PyObject *, PyObject *, PyObject*);
@@ -6061,6 +6273,7 @@ static PyMethodDef TestMethods[] = {
60616273
PyDoc_STR("fatal_error(message, release_gil=False): call Py_FatalError(message)")},
60626274
{"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")},
60636275
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
6276+
{"test_watch_dict", test_watch_dict, METH_NOARGS, NULL},
60646277
{NULL, NULL} /* sentinel */
60656278
};
60666279

0 commit comments

Comments
 (0)