summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCarl Meyer <carl@oddbird.net>2022-10-07 00:08:00 (GMT)
committerGitHub <noreply@github.com>2022-10-07 00:08:00 (GMT)
commita4b7794887929f82c532fcd055326954ff1197ce (patch)
tree257e2dc783858251f893d75c17663913b05a0fad
parent683ab859554c34831fcecc854de35745d7fd603c (diff)
downloadcpython-a4b7794887929f82c532fcd055326954ff1197ce.zip
cpython-a4b7794887929f82c532fcd055326954ff1197ce.tar.gz
cpython-a4b7794887929f82c532fcd055326954ff1197ce.tar.bz2
GH-91052: Add C API for watching dictionaries (GH-31787)
-rw-r--r--Doc/c-api/dict.rst51
-rw-r--r--Include/cpython/dictobject.h23
-rw-r--r--Include/internal/pycore_dict.h27
-rw-r--r--Include/internal/pycore_interp.h2
-rw-r--r--Lib/test/test_capi.py132
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2022-10-03-16-12-39.gh-issue-91052.MsYL9d.rst1
-rw-r--r--Modules/_testcapimodule.c140
-rw-r--r--Objects/dictobject.c119
-rw-r--r--Python/ceval.c5
-rw-r--r--Python/pystate.c4
10 files changed, 487 insertions, 17 deletions
diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst
index 819168d..7bebea0 100644
--- a/Doc/c-api/dict.rst
+++ b/Doc/c-api/dict.rst
@@ -238,3 +238,54 @@ Dictionary Objects
for key, value in seq2:
if override or key not in a:
a[key] = value
+
+.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
+
+ Register *callback* as a dictionary watcher. Return a non-negative integer
+ id which must be passed to future calls to :c:func:`PyDict_Watch`. In case
+ of error (e.g. no more watcher IDs available), return ``-1`` and set an
+ exception.
+
+.. c:function:: int PyDict_ClearWatcher(int watcher_id)
+
+ Clear watcher identified by *watcher_id* previously returned from
+ :c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
+ if the given *watcher_id* was never registered.)
+
+.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)
+
+ Mark dictionary *dict* as watched. The callback granted *watcher_id* by
+ :c:func:`PyDict_AddWatcher` will be called when *dict* is modified or
+ deallocated.
+
+.. c:type:: PyDict_WatchEvent
+
+ Enumeration of possible dictionary watcher events: ``PyDict_EVENT_ADDED``,
+ ``PyDict_EVENT_MODIFIED``, ``PyDict_EVENT_DELETED``, ``PyDict_EVENT_CLONED``,
+ ``PyDict_EVENT_CLEARED``, or ``PyDict_EVENT_DEALLOCATED``.
+
+.. c:type:: int (*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *new_value)
+
+ Type of a dict watcher callback function.
+
+ If *event* is ``PyDict_EVENT_CLEARED`` or ``PyDict_EVENT_DEALLOCATED``, both
+ *key* and *new_value* will be ``NULL``. If *event* is ``PyDict_EVENT_ADDED``
+ or ``PyDict_EVENT_MODIFIED``, *new_value* will be the new value for *key*.
+ If *event* is ``PyDict_EVENT_DELETED``, *key* is being deleted from the
+ dictionary and *new_value* will be ``NULL``.
+
+ ``PyDict_EVENT_CLONED`` occurs when *dict* was previously empty and another
+ dict is merged into it. To maintain efficiency of this operation, per-key
+ ``PyDict_EVENT_ADDED`` events are not issued in this case; instead a
+ single ``PyDict_EVENT_CLONED`` is issued, and *key* will be the source
+ dictionary.
+
+ The callback may inspect but must not modify *dict*; doing so could have
+ unpredictable effects, including infinite recursion.
+
+ Callbacks occur before the notified modification to *dict* takes place, so
+ the prior state of *dict* can be inspected.
+
+ If the callback returns with an exception set, it must return ``-1``; this
+ exception will be printed as an unraisable exception using
+ :c:func:`PyErr_WriteUnraisable`. Otherwise it should return ``0``.
diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h
index 565ad791..f8a74a5 100644
--- a/Include/cpython/dictobject.h
+++ b/Include/cpython/dictobject.h
@@ -83,3 +83,26 @@ typedef struct {
PyAPI_FUNC(PyObject *) _PyDictView_New(PyObject *, PyTypeObject *);
PyAPI_FUNC(PyObject *) _PyDictView_Intersect(PyObject* self, PyObject *other);
+
+/* Dictionary watchers */
+
+typedef enum {
+ PyDict_EVENT_ADDED,
+ PyDict_EVENT_MODIFIED,
+ PyDict_EVENT_DELETED,
+ PyDict_EVENT_CLONED,
+ PyDict_EVENT_CLEARED,
+ PyDict_EVENT_DEALLOCATED,
+} PyDict_WatchEvent;
+
+// Callback to be invoked when a watched dict is cleared, dealloced, or modified.
+// In clear/dealloc case, key and new_value will be NULL. Otherwise, new_value will be the
+// new value for key, NULL if key is being deleted.
+typedef int(*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject* dict, PyObject* key, PyObject* new_value);
+
+// Register/unregister a dict-watcher callback
+PyAPI_FUNC(int) PyDict_AddWatcher(PyDict_WatchCallback callback);
+PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
+
+// Mark given dictionary as "watched" (callback will be called if it is modified)
+PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h
index 4640929..ae4094a 100644
--- a/Include/internal/pycore_dict.h
+++ b/Include/internal/pycore_dict.h
@@ -154,7 +154,32 @@ struct _dictvalues {
extern uint64_t _pydict_global_version;
-#define DICT_NEXT_VERSION() (++_pydict_global_version)
+#define DICT_MAX_WATCHERS 8
+#define DICT_VERSION_INCREMENT (1 << DICT_MAX_WATCHERS)
+#define DICT_VERSION_MASK (DICT_VERSION_INCREMENT - 1)
+
+#define DICT_NEXT_VERSION() (_pydict_global_version += DICT_VERSION_INCREMENT)
+
+void
+_PyDict_SendEvent(int watcher_bits,
+ PyDict_WatchEvent event,
+ PyDictObject *mp,
+ PyObject *key,
+ PyObject *value);
+
+static inline uint64_t
+_PyDict_NotifyEvent(PyDict_WatchEvent event,
+ PyDictObject *mp,
+ PyObject *key,
+ PyObject *value)
+{
+ int watcher_bits = mp->ma_version_tag & DICT_VERSION_MASK;
+ if (watcher_bits) {
+ _PyDict_SendEvent(watcher_bits, event, mp, key, value);
+ return DICT_NEXT_VERSION() | watcher_bits;
+ }
+ return DICT_NEXT_VERSION();
+}
extern PyObject *_PyObject_MakeDictFromInstanceAttributes(PyObject *obj, PyDictValues *values);
extern PyObject *_PyDict_FromItems(
diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h
index b21708a..8cecd5a 100644
--- a/Include/internal/pycore_interp.h
+++ b/Include/internal/pycore_interp.h
@@ -144,6 +144,8 @@ struct _is {
// Initialized to _PyEval_EvalFrameDefault().
_PyFrameEvalFunction eval_frame;
+ PyDict_WatchCallback dict_watchers[DICT_MAX_WATCHERS];
+
Py_ssize_t co_extra_user_count;
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py
index 2c6fe34..cb90d55 100644
--- a/Lib/test/test_capi.py
+++ b/Lib/test/test_capi.py
@@ -2,6 +2,7 @@
# these are all functions _testcapi exports whose name begins with 'test_'.
from collections import OrderedDict
+from contextlib import contextmanager
import _thread
import importlib.machinery
import importlib.util
@@ -1393,5 +1394,136 @@ class Test_Pep523API(unittest.TestCase):
self.do_test(func2)
+class TestDictWatchers(unittest.TestCase):
+ # types of watchers testcapimodule can add:
+ EVENTS = 0 # appends dict events as strings to global event list
+ ERROR = 1 # unconditionally sets and signals a RuntimeException
+ SECOND = 2 # always appends "second" to global event list
+
+ def add_watcher(self, kind=EVENTS):
+ return _testcapi.add_dict_watcher(kind)
+
+ def clear_watcher(self, watcher_id):
+ _testcapi.clear_dict_watcher(watcher_id)
+
+ @contextmanager
+ def watcher(self, kind=EVENTS):
+ wid = self.add_watcher(kind)
+ try:
+ yield wid
+ finally:
+ self.clear_watcher(wid)
+
+ def assert_events(self, expected):
+ actual = _testcapi.get_dict_watcher_events()
+ self.assertEqual(actual, expected)
+
+ def watch(self, wid, d):
+ _testcapi.watch_dict(wid, d)
+
+ def test_set_new_item(self):
+ d = {}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ d["foo"] = "bar"
+ self.assert_events(["new:foo:bar"])
+
+ def test_set_existing_item(self):
+ d = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ d["foo"] = "baz"
+ self.assert_events(["mod:foo:baz"])
+
+ def test_clone(self):
+ d = {}
+ d2 = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ d.update(d2)
+ self.assert_events(["clone"])
+
+ def test_no_event_if_not_watched(self):
+ d = {}
+ with self.watcher() as wid:
+ d["foo"] = "bar"
+ self.assert_events([])
+
+ def test_del(self):
+ d = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ del d["foo"]
+ self.assert_events(["del:foo"])
+
+ def test_pop(self):
+ d = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ d.pop("foo")
+ self.assert_events(["del:foo"])
+
+ def test_clear(self):
+ d = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ d.clear()
+ self.assert_events(["clear"])
+
+ def test_dealloc(self):
+ d = {"foo": "bar"}
+ with self.watcher() as wid:
+ self.watch(wid, d)
+ del d
+ self.assert_events(["dealloc"])
+
+ def test_error(self):
+ d = {}
+ unraisables = []
+ def unraisable_hook(unraisable):
+ unraisables.append(unraisable)
+ with self.watcher(kind=self.ERROR) as wid:
+ self.watch(wid, d)
+ orig_unraisable_hook = sys.unraisablehook
+ sys.unraisablehook = unraisable_hook
+ try:
+ d["foo"] = "bar"
+ finally:
+ sys.unraisablehook = orig_unraisable_hook
+ self.assert_events([])
+ self.assertEqual(len(unraisables), 1)
+ unraisable = unraisables[0]
+ self.assertIs(unraisable.object, d)
+ self.assertEqual(str(unraisable.exc_value), "boom!")
+
+ def test_two_watchers(self):
+ d1 = {}
+ d2 = {}
+ with self.watcher() as wid1:
+ with self.watcher(kind=self.SECOND) as wid2:
+ self.watch(wid1, d1)
+ self.watch(wid2, d2)
+ d1["foo"] = "bar"
+ d2["hmm"] = "baz"
+ self.assert_events(["new:foo:bar", "second"])
+
+ def test_watch_non_dict(self):
+ with self.watcher() as wid:
+ with self.assertRaisesRegex(ValueError, r"Cannot watch non-dictionary"):
+ self.watch(wid, 1)
+
+ def test_watch_out_of_range_watcher_id(self):
+ d = {}
+ with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID -1"):
+ self.watch(-1, d)
+ with self.assertRaisesRegex(ValueError, r"Invalid dict watcher ID 8"):
+ self.watch(8, d) # DICT_MAX_WATCHERS = 8
+
+ def test_unassigned_watcher_id(self):
+ d = {}
+ with self.assertRaisesRegex(ValueError, r"No dict watcher set for ID 1"):
+ self.watch(1, d)
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-10-03-16-12-39.gh-issue-91052.MsYL9d.rst b/Misc/NEWS.d/next/Core and Builtins/2022-10-03-16-12-39.gh-issue-91052.MsYL9d.rst
new file mode 100644
index 0000000..c7db4da
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-10-03-16-12-39.gh-issue-91052.MsYL9d.rst
@@ -0,0 +1 @@
+Add API for subscribing to modification events on selected dictionaries.
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index 3d6535f..c57dba4 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -5169,6 +5169,142 @@ test_tstate_capi(PyObject *self, PyObject *Py_UNUSED(args))
}
+// Test dict watching
+static PyObject *g_dict_watch_events;
+static int g_dict_watchers_installed;
+
+static int
+dict_watch_callback(PyDict_WatchEvent event,
+ PyObject *dict,
+ PyObject *key,
+ PyObject *new_value)
+{
+ PyObject *msg;
+ switch(event) {
+ case PyDict_EVENT_CLEARED:
+ msg = PyUnicode_FromString("clear");
+ break;
+ case PyDict_EVENT_DEALLOCATED:
+ msg = PyUnicode_FromString("dealloc");
+ break;
+ case PyDict_EVENT_CLONED:
+ msg = PyUnicode_FromString("clone");
+ break;
+ case PyDict_EVENT_ADDED:
+ msg = PyUnicode_FromFormat("new:%S:%S", key, new_value);
+ break;
+ case PyDict_EVENT_MODIFIED:
+ msg = PyUnicode_FromFormat("mod:%S:%S", key, new_value);
+ break;
+ case PyDict_EVENT_DELETED:
+ msg = PyUnicode_FromFormat("del:%S", key);
+ break;
+ default:
+ msg = PyUnicode_FromString("unknown");
+ }
+ if (!msg) {
+ return -1;
+ }
+ assert(PyList_Check(g_dict_watch_events));
+ if (PyList_Append(g_dict_watch_events, msg) < 0) {
+ Py_DECREF(msg);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+dict_watch_callback_second(PyDict_WatchEvent event,
+ PyObject *dict,
+ PyObject *key,
+ PyObject *new_value)
+{
+ PyObject *msg = PyUnicode_FromString("second");
+ if (!msg) {
+ return -1;
+ }
+ if (PyList_Append(g_dict_watch_events, msg) < 0) {
+ return -1;
+ }
+ return 0;
+}
+
+static int
+dict_watch_callback_error(PyDict_WatchEvent event,
+ PyObject *dict,
+ PyObject *key,
+ PyObject *new_value)
+{
+ PyErr_SetString(PyExc_RuntimeError, "boom!");
+ return -1;
+}
+
+static PyObject *
+add_dict_watcher(PyObject *self, PyObject *kind)
+{
+ int watcher_id;
+ assert(PyLong_Check(kind));
+ long kind_l = PyLong_AsLong(kind);
+ if (kind_l == 2) {
+ watcher_id = PyDict_AddWatcher(dict_watch_callback_second);
+ } else if (kind_l == 1) {
+ watcher_id = PyDict_AddWatcher(dict_watch_callback_error);
+ } else {
+ watcher_id = PyDict_AddWatcher(dict_watch_callback);
+ }
+ if (watcher_id < 0) {
+ return NULL;
+ }
+ if (!g_dict_watchers_installed) {
+ assert(!g_dict_watch_events);
+ if (!(g_dict_watch_events = PyList_New(0))) {
+ return NULL;
+ }
+ }
+ g_dict_watchers_installed++;
+ return PyLong_FromLong(watcher_id);
+}
+
+static PyObject *
+clear_dict_watcher(PyObject *self, PyObject *watcher_id)
+{
+ if (PyDict_ClearWatcher(PyLong_AsLong(watcher_id))) {
+ return NULL;
+ }
+ g_dict_watchers_installed--;
+ if (!g_dict_watchers_installed) {
+ assert(g_dict_watch_events);
+ Py_CLEAR(g_dict_watch_events);
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+watch_dict(PyObject *self, PyObject *args)
+{
+ PyObject *dict;
+ int watcher_id;
+ if (!PyArg_ParseTuple(args, "iO", &watcher_id, &dict)) {
+ return NULL;
+ }
+ if (PyDict_Watch(watcher_id, dict)) {
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+get_dict_watcher_events(PyObject *self, PyObject *Py_UNUSED(args))
+{
+ if (!g_dict_watch_events) {
+ PyErr_SetString(PyExc_RuntimeError, "no watchers active");
+ return NULL;
+ }
+ Py_INCREF(g_dict_watch_events);
+ return g_dict_watch_events;
+}
+
+
// Test PyFloat_Pack2(), PyFloat_Pack4() and PyFloat_Pack8()
static PyObject *
test_float_pack(PyObject *self, PyObject *args)
@@ -5762,6 +5898,10 @@ static PyMethodDef TestMethods[] = {
{"settrace_to_record", settrace_to_record, METH_O, NULL},
{"test_macros", test_macros, METH_NOARGS, NULL},
{"clear_managed_dict", clear_managed_dict, METH_O, NULL},
+ {"add_dict_watcher", add_dict_watcher, METH_O, NULL},
+ {"clear_dict_watcher", clear_dict_watcher, METH_O, NULL},
+ {"watch_dict", watch_dict, METH_VARARGS, NULL},
+ {"get_dict_watcher_events", get_dict_watcher_events, METH_NOARGS, NULL},
{NULL, NULL} /* sentinel */
};
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index fecdfa8..6542b18 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -1240,6 +1240,7 @@ insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
MAINTAIN_TRACKING(mp, key, value);
if (ix == DKIX_EMPTY) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_ADDED, mp, key, value);
/* Insert into new slot. */
mp->ma_keys->dk_version = 0;
assert(old_value == NULL);
@@ -1274,7 +1275,7 @@ insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
ep->me_value = value;
}
mp->ma_used++;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
mp->ma_keys->dk_usable--;
mp->ma_keys->dk_nentries++;
assert(mp->ma_keys->dk_usable >= 0);
@@ -1283,6 +1284,7 @@ insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
}
if (old_value != value) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_MODIFIED, mp, key, value);
if (_PyDict_HasSplitTable(mp)) {
mp->ma_values->values[ix] = value;
if (old_value == NULL) {
@@ -1299,7 +1301,7 @@ insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
DK_ENTRIES(mp->ma_keys)[ix].me_value = value;
}
}
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
}
Py_XDECREF(old_value); /* which **CAN** re-enter (see issue #22653) */
ASSERT_CONSISTENT(mp);
@@ -1320,6 +1322,8 @@ insert_to_emptydict(PyDictObject *mp, PyObject *key, Py_hash_t hash,
{
assert(mp->ma_keys == Py_EMPTY_KEYS);
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_ADDED, mp, key, value);
+
int unicode = PyUnicode_CheckExact(key);
PyDictKeysObject *newkeys = new_keys_object(PyDict_LOG_MINSIZE, unicode);
if (newkeys == NULL) {
@@ -1347,7 +1351,7 @@ insert_to_emptydict(PyDictObject *mp, PyObject *key, Py_hash_t hash,
ep->me_value = value;
}
mp->ma_used++;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
mp->ma_keys->dk_usable--;
mp->ma_keys->dk_nentries++;
return 0;
@@ -1910,7 +1914,7 @@ delete_index_from_values(PyDictValues *values, Py_ssize_t ix)
static int
delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix,
- PyObject *old_value)
+ PyObject *old_value, uint64_t new_version)
{
PyObject *old_key;
@@ -1918,7 +1922,7 @@ delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix,
assert(hashpos >= 0);
mp->ma_used--;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
if (mp->ma_values) {
assert(old_value == mp->ma_values->values[ix]);
mp->ma_values->values[ix] = NULL;
@@ -1987,7 +1991,8 @@ _PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash)
return -1;
}
- return delitem_common(mp, hash, ix, old_value);
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_DELETED, mp, key, NULL);
+ return delitem_common(mp, hash, ix, old_value, new_version);
}
/* This function promises that the predicate -> deletion sequence is atomic
@@ -2028,10 +2033,12 @@ _PyDict_DelItemIf(PyObject *op, PyObject *key,
hashpos = lookdict_index(mp->ma_keys, hash, ix);
assert(hashpos >= 0);
- if (res > 0)
- return delitem_common(mp, hashpos, ix, old_value);
- else
+ if (res > 0) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_DELETED, mp, key, NULL);
+ return delitem_common(mp, hashpos, ix, old_value, new_version);
+ } else {
return 0;
+ }
}
@@ -2052,11 +2059,12 @@ PyDict_Clear(PyObject *op)
return;
}
/* Empty the dict... */
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_CLEARED, mp, NULL, NULL);
dictkeys_incref(Py_EMPTY_KEYS);
mp->ma_keys = Py_EMPTY_KEYS;
mp->ma_values = NULL;
mp->ma_used = 0;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
/* ...then clear the keys and values */
if (oldvalues != NULL) {
n = oldkeys->dk_nentries;
@@ -2196,7 +2204,8 @@ _PyDict_Pop_KnownHash(PyObject *dict, PyObject *key, Py_hash_t hash, PyObject *d
}
assert(old_value != NULL);
Py_INCREF(old_value);
- delitem_common(mp, hash, ix, old_value);
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_DELETED, mp, key, NULL);
+ delitem_common(mp, hash, ix, old_value, new_version);
ASSERT_CONSISTENT(mp);
return old_value;
@@ -2321,6 +2330,7 @@ Fail:
static void
dict_dealloc(PyDictObject *mp)
{
+ _PyDict_NotifyEvent(PyDict_EVENT_DEALLOCATED, mp, NULL, NULL);
PyDictValues *values = mp->ma_values;
PyDictKeysObject *keys = mp->ma_keys;
Py_ssize_t i, n;
@@ -2809,6 +2819,7 @@ dict_merge(PyObject *a, PyObject *b, int override)
other->ma_used == okeys->dk_nentries &&
(DK_LOG_SIZE(okeys) == PyDict_LOG_MINSIZE ||
USABLE_FRACTION(DK_SIZE(okeys)/2) < other->ma_used)) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_CLONED, mp, b, NULL);
PyDictKeysObject *keys = clone_combined_dict_keys(other);
if (keys == NULL) {
return -1;
@@ -2822,7 +2833,7 @@ dict_merge(PyObject *a, PyObject *b, int override)
}
mp->ma_used = other->ma_used;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
ASSERT_CONSISTENT(mp);
if (_PyObject_GC_IS_TRACKED(other) && !_PyObject_GC_IS_TRACKED(mp)) {
@@ -3294,6 +3305,7 @@ PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *defaultobj)
return NULL;
if (ix == DKIX_EMPTY) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_ADDED, mp, key, defaultobj);
mp->ma_keys->dk_version = 0;
value = defaultobj;
if (mp->ma_keys->dk_usable <= 0) {
@@ -3328,12 +3340,13 @@ PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *defaultobj)
Py_INCREF(value);
MAINTAIN_TRACKING(mp, key, value);
mp->ma_used++;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
mp->ma_keys->dk_usable--;
mp->ma_keys->dk_nentries++;
assert(mp->ma_keys->dk_usable >= 0);
}
else if (value == NULL) {
+ uint64_t new_version = _PyDict_NotifyEvent(PyDict_EVENT_ADDED, mp, key, defaultobj);
value = defaultobj;
assert(_PyDict_HasSplitTable(mp));
assert(mp->ma_values->values[ix] == NULL);
@@ -3342,7 +3355,7 @@ PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *defaultobj)
mp->ma_values->values[ix] = value;
_PyDictValues_AddToInsertionOrder(mp->ma_values, ix);
mp->ma_used++;
- mp->ma_version_tag = DICT_NEXT_VERSION();
+ mp->ma_version_tag = new_version;
}
ASSERT_CONSISTENT(mp);
@@ -3415,6 +3428,7 @@ dict_popitem_impl(PyDictObject *self)
{
Py_ssize_t i, j;
PyObject *res;
+ uint64_t new_version;
/* Allocate the result tuple before checking the size. Believe it
* or not, this allocation could trigger a garbage collection which
@@ -3454,6 +3468,7 @@ dict_popitem_impl(PyDictObject *self)
assert(i >= 0);
key = ep0[i].me_key;
+ new_version = _PyDict_NotifyEvent(PyDict_EVENT_DELETED, self, key, NULL);
hash = unicode_get_hash(key);
value = ep0[i].me_value;
ep0[i].me_key = NULL;
@@ -3468,6 +3483,7 @@ dict_popitem_impl(PyDictObject *self)
assert(i >= 0);
key = ep0[i].me_key;
+ new_version = _PyDict_NotifyEvent(PyDict_EVENT_DELETED, self, key, NULL);
hash = ep0[i].me_hash;
value = ep0[i].me_value;
ep0[i].me_key = NULL;
@@ -3485,7 +3501,7 @@ dict_popitem_impl(PyDictObject *self)
/* We can't dk_usable++ since there is DKIX_DUMMY in indices */
self->ma_keys->dk_nentries = i;
self->ma_used--;
- self->ma_version_tag = DICT_NEXT_VERSION();
+ self->ma_version_tag = new_version;
ASSERT_CONSISTENT(self);
return res;
}
@@ -5703,3 +5719,76 @@ uint32_t _PyDictKeys_GetVersionForCurrentState(PyDictKeysObject *dictkeys)
dictkeys->dk_version = v;
return v;
}
+
+int
+PyDict_Watch(int watcher_id, PyObject* dict)
+{
+ if (!PyDict_Check(dict)) {
+ PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary");
+ return -1;
+ }
+ if (watcher_id < 0 || watcher_id >= DICT_MAX_WATCHERS) {
+ PyErr_Format(PyExc_ValueError, "Invalid dict watcher ID %d", watcher_id);
+ return -1;
+ }
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ if (!interp->dict_watchers[watcher_id]) {
+ PyErr_Format(PyExc_ValueError, "No dict watcher set for ID %d", watcher_id);
+ return -1;
+ }
+ ((PyDictObject*)dict)->ma_version_tag |= (1LL << watcher_id);
+ return 0;
+}
+
+int
+PyDict_AddWatcher(PyDict_WatchCallback callback)
+{
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+
+ for (int i = 0; i < DICT_MAX_WATCHERS; i++) {
+ if (!interp->dict_watchers[i]) {
+ interp->dict_watchers[i] = callback;
+ return i;
+ }
+ }
+
+ PyErr_SetString(PyExc_RuntimeError, "no more dict watcher IDs available");
+ return -1;
+}
+
+int
+PyDict_ClearWatcher(int watcher_id)
+{
+ if (watcher_id < 0 || watcher_id >= DICT_MAX_WATCHERS) {
+ PyErr_Format(PyExc_ValueError, "Invalid dict watcher ID %d", watcher_id);
+ return -1;
+ }
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ if (!interp->dict_watchers[watcher_id]) {
+ PyErr_Format(PyExc_ValueError, "No dict watcher set for ID %d", watcher_id);
+ return -1;
+ }
+ interp->dict_watchers[watcher_id] = NULL;
+ return 0;
+}
+
+void
+_PyDict_SendEvent(int watcher_bits,
+ PyDict_WatchEvent event,
+ PyDictObject *mp,
+ PyObject *key,
+ PyObject *value)
+{
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ for (int i = 0; i < DICT_MAX_WATCHERS; i++) {
+ if (watcher_bits & 1) {
+ PyDict_WatchCallback cb = interp->dict_watchers[i];
+ if (cb && (cb(event, (PyObject*)mp, key, value) < 0)) {
+ // some dict modification paths (e.g. PyDict_Clear) can't raise, so we
+ // can't propagate exceptions from dict watchers.
+ PyErr_WriteUnraisable((PyObject *)mp);
+ }
+ }
+ watcher_bits >>= 1;
+ }
+}
diff --git a/Python/ceval.c b/Python/ceval.c
index c08c794..ee1baba 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -3252,6 +3252,7 @@ handle_eval_breaker:
uint16_t hint = cache->index;
DEOPT_IF(hint >= (size_t)dict->ma_keys->dk_nentries, STORE_ATTR);
PyObject *value, *old_value;
+ uint64_t new_version;
if (DK_IS_UNICODE(dict->ma_keys)) {
PyDictUnicodeEntry *ep = DK_UNICODE_ENTRIES(dict->ma_keys) + hint;
DEOPT_IF(ep->me_key != name, STORE_ATTR);
@@ -3259,6 +3260,7 @@ handle_eval_breaker:
DEOPT_IF(old_value == NULL, STORE_ATTR);
STACK_SHRINK(1);
value = POP();
+ new_version = _PyDict_NotifyEvent(PyDict_EVENT_MODIFIED, dict, name, value);
ep->me_value = value;
}
else {
@@ -3268,6 +3270,7 @@ handle_eval_breaker:
DEOPT_IF(old_value == NULL, STORE_ATTR);
STACK_SHRINK(1);
value = POP();
+ new_version = _PyDict_NotifyEvent(PyDict_EVENT_MODIFIED, dict, name, value);
ep->me_value = value;
}
Py_DECREF(old_value);
@@ -3277,7 +3280,7 @@ handle_eval_breaker:
_PyObject_GC_TRACK(dict);
}
/* PEP 509 */
- dict->ma_version_tag = DICT_NEXT_VERSION();
+ dict->ma_version_tag = new_version;
Py_DECREF(owner);
JUMPBY(INLINE_CACHE_ENTRIES_STORE_ATTR);
DISPATCH();
diff --git a/Python/pystate.c b/Python/pystate.c
index 50ae0ce..c74868d 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -451,6 +451,10 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
Py_CLEAR(interp->sysdict);
Py_CLEAR(interp->builtins);
+ for (int i=0; i < DICT_MAX_WATCHERS; i++) {
+ interp->dict_watchers[i] = NULL;
+ }
+
// XXX Once we have one allocator per interpreter (i.e.
// per-interpreter GC) we must ensure that all of the interpreter's
// objects have been cleaned up at the point.