summaryrefslogtreecommitdiffstats
path: root/Objects/dictobject.c
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 /Objects/dictobject.c
parent683ab859554c34831fcecc854de35745d7fd603c (diff)
downloadcpython-a4b7794887929f82c532fcd055326954ff1197ce.zip
cpython-a4b7794887929f82c532fcd055326954ff1197ce.tar.gz
cpython-a4b7794887929f82c532fcd055326954ff1197ce.tar.bz2
GH-91052: Add C API for watching dictionaries (GH-31787)
Diffstat (limited to 'Objects/dictobject.c')
-rw-r--r--Objects/dictobject.c119
1 files changed, 104 insertions, 15 deletions
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;
+ }
+}