Skip to content

Commit

Permalink
Update to multiple-watchers API.
Browse files Browse the repository at this point in the history
  • Loading branch information
carljm committed Oct 3, 2022
1 parent d22463b commit 2c74443
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 170 deletions.
30 changes: 9 additions & 21 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,15 +239,18 @@ Dictionary Objects
if override or key not in a:
a[key] = value
.. c:function:: void PyDict_Watch(PyObject *dict)
.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
Mark dictionary *dict* as watched. The callback set via
:c:func:`PyDict_SetWatchCallback` will be called when *dict* is modified or
deallocated.
Register *callback* as a dictionary watcher. Return a non-negative integer
id which must be passed to future calls to e.g. :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_IsWatched(PyObject *dict)
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)
Return ``1`` if *dict* is marked as watched, ``0`` otherwise.
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
Expand All @@ -271,23 +274,8 @@ Dictionary Objects
single ``PyDict_EVENT_CLONED`` is issued, and *key* will be the source
dictionary.
.. c:function:: void PyDict_SetWatchCallback(PyDict_WatchCallback callback)
Set a callback for modification events on dictionaries watched via
:c:func:`PyDict_Watch`.
There is only one callback per interpreter. Before setting the callback, you
must check if there is one already set (use
:c:func:`PyDict_GetWatchCallback`) and if so, call it from your own new
callback. Failure to do this is a critical bug in your callback and may break
other dict-watching clients.
The callback may inspect but should 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.
.. c:function:: PyDict_WatchCallback PyDict_GetWatchCallback(void)
Return the existing dictionary watcher callback, or ``NULL`` if none has been set.
15 changes: 5 additions & 10 deletions Include/cpython/dictobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# error "this header file must not be included directly"
#endif


typedef struct _dictkeysobject PyDictKeysObject;
typedef struct _dictvalues PyDictValues;

Expand Down Expand Up @@ -79,12 +80,6 @@ PyAPI_FUNC(PyObject *) _PyDictView_Intersect(PyObject* self, PyObject *other);

/* Dictionary watchers */

// Mark given dictionary as "watched" (callback will be called if it is modified)
PyAPI_FUNC(int) PyDict_Watch(PyObject* dict);

// Check if given dictionary is watched
PyAPI_FUNC(int) PyDict_IsWatched(PyObject* dict);

typedef enum {
PyDict_EVENT_ADDED,
PyDict_EVENT_MODIFIED,
Expand All @@ -99,8 +94,8 @@ typedef enum {
// new value for key, NULL if key is being deleted.
typedef void(*PyDict_WatchCallback)(PyDict_WatchEvent event, PyObject* dict, PyObject* key, PyObject* new_value);

// Set new global watch callback; supply NULL to clear callback
PyAPI_FUNC(void) PyDict_SetWatchCallback(PyDict_WatchCallback callback);
// Register a dict-watcher callback
PyAPI_FUNC(int) PyDict_AddWatcher(PyDict_WatchCallback callback);

// Get existing global watch callback
PyAPI_FUNC(PyDict_WatchCallback) PyDict_GetWatchCallback(void);
// Mark given dictionary as "watched" (callback will be called if it is modified)
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
28 changes: 26 additions & 2 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,33 @@ struct _dictvalues {
#define DK_IS_UNICODE(dk) ((dk)->dk_kind != DICT_KEYS_GENERAL)

extern uint64_t _pydict_global_version;
#define DICT_VERSION_WATCHED_TAG 1

#define DICT_NEXT_VERSION() (_pydict_global_version += 2)
#define DICT_MAX_WATCHERS 8
#define DICT_VERSION_MASK 255
#define DICT_VERSION_INCREMENT 256

#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(
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ struct _is {
// Initialized to _PyEval_EvalFrameDefault().
_PyFrameEvalFunction eval_frame;

void *dict_watch_callback;
void *dict_watchers[8];

Py_ssize_t co_extra_user_count;
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
Expand Down
46 changes: 6 additions & 40 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -5777,7 +5777,6 @@ test_tstate_capi(PyObject *self, PyObject *Py_UNUSED(args))

// Test dict watching
static PyObject *g_dict_watch_events;
static PyDict_WatchCallback g_prev_callback;

static void
dict_watch_callback(PyDict_WatchEvent event,
Expand Down Expand Up @@ -5810,9 +5809,6 @@ dict_watch_callback(PyDict_WatchEvent event,
}
assert(PyList_Check(g_dict_watch_events));
PyList_Append(g_dict_watch_events, msg);
if (g_prev_callback != NULL) {
g_prev_callback(event, dict, key, new_value);
}
}

static int
Expand Down Expand Up @@ -5845,8 +5841,8 @@ dict_watch_assert(Py_ssize_t expected_num_events,
}

static int
try_watch(PyObject *obj) {
if (PyDict_Watch(obj)) {
try_watch(int watcher_id, PyObject *obj) {
if (PyDict_Watch(watcher_id, obj)) {
raiseTestError("test_watch_dict", "PyDict_Watch() failed on dict");
return -1;
}
Expand All @@ -5864,23 +5860,12 @@ test_watch_dict(PyObject *self, PyObject *Py_UNUSED(args))
PyObject *key2 = PyUnicode_FromString("key2");

g_dict_watch_events = PyList_New(0);
g_prev_callback = PyDict_GetWatchCallback();

PyDict_SetWatchCallback(dict_watch_callback);
if (PyDict_GetWatchCallback() != dict_watch_callback) {
return raiseTestError("test_watch_dict", "GetWatchCallback did not return set callback");
}
if (try_watch(watched)) {
int wid = PyDict_AddWatcher(dict_watch_callback);
if (try_watch(wid, watched)) {
return NULL;
}

if (!PyDict_IsWatched(watched)) {
return raiseTestError("test_watch_dict", "IsWatched returned false for watched dict");
}
if (PyDict_IsWatched(unwatched)) {
return raiseTestError("test_watch_dict", "IsWatched returned true for unwatched dict");
}

PyDict_SetItem(unwatched, key1, two);
PyDict_Merge(watched, unwatched, 1);

Expand Down Expand Up @@ -5938,9 +5923,7 @@ test_watch_dict(PyObject *self, PyObject *Py_UNUSED(args))
}

PyObject *copy = PyDict_Copy(watched);
if (PyDict_IsWatched(copy)) {
return raiseTestError("test_watch_dict", "copying a watched dict should not watch the copy");
}
// copied dict is not watched, so this does not add an event
Py_CLEAR(copy);

Py_CLEAR(watched);
Expand All @@ -5950,24 +5933,8 @@ test_watch_dict(PyObject *self, PyObject *Py_UNUSED(args))
return NULL;
}

PyDict_SetWatchCallback(g_prev_callback);
g_prev_callback = NULL;

// no events after callback unset
watched = PyDict_New();
if (try_watch(watched)) {
return NULL;
}

PyDict_SetItem(watched, key1, one);
Py_CLEAR(watched);

if (dict_watch_assert(9, "dealloc")) {
return NULL;
}

// it is an error to try to watch a non-dict
if (!PyDict_Watch(one)) {
if (!PyDict_Watch(wid, one)) {
raiseTestError("test_watch_dict", "PyDict_Watch() succeeded on non-dict");
return NULL;
} else if (!PyErr_Occurred()) {
Expand All @@ -5977,7 +5944,6 @@ test_watch_dict(PyObject *self, PyObject *Py_UNUSED(args))
PyErr_Clear();
}


Py_CLEAR(g_dict_watch_events);
Py_DECREF(one);
Py_DECREF(two);
Expand Down
Loading

0 comments on commit 2c74443

Please sign in to comment.