GH-91052: Add C API for watching dictionaries (GH-31787)

This commit is contained in:
Carl Meyer 2022-10-06 17:08:00 -07:00 committed by GitHub
parent 683ab85955
commit a4b7794887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 487 additions and 17 deletions

View File

@ -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``.

View File

@ -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);

View File

@ -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(

View File

@ -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];

View File

@ -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()

View File

@ -0,0 +1 @@
Add API for subscribing to modification events on selected dictionaries.

View File

@ -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 */
};

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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.