Skip to content

Commit f193b6d

Browse files
committed
Complete documentation on dict watchers in watchers.rst.
1 parent dc53f79 commit f193b6d

File tree

3 files changed

+121
-37
lines changed

3 files changed

+121
-37
lines changed

doc/sphinx/source/watchers.rst

+106-35
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Watchers
3030
======================================
3131

3232
From Python 3.12 onwards *watchers* have been added [#]_.
33-
This allows registering a callback function on specific ``dict``, ``type``, ``code`` and ``function`` objects.
33+
This allows registering a callback function on specific ``dict``, ``type``, ``code`` or ``function`` object.
3434
The callback is called with any event that occurs on the specific object.
3535

3636
Here is an example of a dictionary watcher.
@@ -54,9 +54,9 @@ Here is an example of a dictionary watcher.
5454
Dictionary Watchers
5555
---------------------------
5656

57-
We have created a context manager to wrap the low level C code with a watcher called ``cWatchers.PyDictWatcher``
57+
Here is a context manager ``cWatchers.PyDictWatcher`` that wraps the low level CPython code with a watcher
5858
that reports every dictionary operation to ``stdout``.
59-
The code is in ``watcher_example.py``.
59+
The code is in ``src/cpy/Watchers/watcher_example.py``.
6060

6161
.. code-block:: python
6262
:linenos:
@@ -102,32 +102,43 @@ the arguments used to manipulate the dictionary:
102102

103103
\end{landscape}
104104

105-
So how does this work?
105+
There are some obvious variations here:
106+
107+
- Add some prefix to each watcher output line to discriminate it from the rest of stdout.
108+
- The ID of the dictionary could be added so different dictionaries using the same watcher callback could be
109+
discriminated.
110+
- Different outputs, such as JSON.
111+
112+
But how does this watcher work?
106113

107114
Low Level C Implementation
108-
^^^^^^^^^^^^^^^^^^^^^^^^^^
115+
--------------------------
109116

110-
First we need to create some low level C code that interacts with the watcher API.
117+
We need some low level C code that interacts with the CPython watcher API.
111118
First a header file that provides the interface to our dictionary watcher code.
112119
This declares two functions:
113120

114-
- ``dict_watcher_verbose_add()`` this adds a watcher to a dictionary. This returns the watcher ID.
121+
- ``dict_watcher_verbose_add()`` this adds a watcher to a dictionary and returns the watcher ID.
115122
- ``dict_watcher_verbose_remove()`` this removes a watcher ID from a dictionary.
116123

117124
The actual code is in ``src/cpy/Watchers/DictWatcher.h``.
118-
It looks like this:
125+
It looks like this, note the Python version guard to ensure this only works with Python 3.12+:
119126

120127
.. code-block:: c
121128
122129
#define PPY_SSIZE_T_CLEAN
123130
#include "Python.h"
124131
125-
#if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 12
132+
#if PY_VERSION_HEX < 0x030C0000
133+
134+
#error "Required version of Python is 3.12+ (PY_VERSION_HEX >= 0x030C0000)"
135+
136+
#else
126137
127138
int dict_watcher_verbose_add(PyObject *dict);
128139
int dict_watcher_verbose_remove(int watcher_id, PyObject *dict);
129140
130-
#endif // #if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 12
141+
#endif // #if PY_VERSION_HEX >= 0x030C0000
131142
132143
So there are several moving parts in the implementation in ``src/cpy/Watchers/DictWatcher.c``.
133144
First we have some general purpose functions that extract the file name, function name and line number from a Python
@@ -244,8 +255,9 @@ Then there is a simple little helper function that returns a string based on the
244255
}
245256
246257
Now we define the callback function that reports the dictionary event to ``stdout``.
247-
This uses the CPython API ``PyObject_Print`` to print the representation of each object to ``stdout``.
248-
This has to respect NULL arguments:
258+
This calles all the functionas above and uses the CPython API ``PyObject_Print`` to print the representation
259+
of each object to ``stdout``.
260+
This function has to respect NULL arguments:
249261

250262
.. code-block:: c
251263
@@ -276,44 +288,36 @@ This has to respect NULL arguments:
276288
}
277289
278290
Finally we have the two implementations that register and unregister the callback using the low level Python C API.
279-
The first registers the callback, returning the watcher ID:
291+
The first registers the callback, returning the watcher ID (error handling code omitted):
280292

281293
.. code-block:: c
282294
283295
// Set watcher.
284296
int dict_watcher_verbose_add(PyObject *dict) {
285297
int watcher_id = PyDict_AddWatcher(&dict_watcher_verbose);
286-
int api_ret_val = PyDict_Watch(watcher_id, dict);
298+
PyDict_Watch(watcher_id, dict);
287299
return watcher_id;
288300
}
289301
290-
The second de-registers the callback, with the watcher ID and the dictionary in question:
302+
The second de-registers the callback, with the watcher ID and the dictionary in question
303+
(error handling code omitted):
291304

292305
.. code-block:: c
293306
294307
// Remove watcher.
295308
int dict_watcher_verbose_remove(int watcher_id, PyObject *dict) {
296-
int api_ret_val = PyDict_Unwatch(watcher_id, dict);
297-
if (api_ret_val) {
298-
return -1;
299-
}
300-
api_ret_val = PyDict_ClearWatcher(watcher_id);
301-
if (api_ret_val) {
302-
return -2;
303-
}
309+
PyDict_Unwatch(watcher_id, dict);
310+
PyDict_ClearWatcher(watcher_id);
304311
return 0;
305312
}
306313
307314
Exposing This to CPython
308-
^^^^^^^^^^^^^^^^^^^^^^^^
315+
------------------------
309316

310317
Now we create a Python module ``cWatchers`` that exposes this low level C code to CPython.
311318
This code is in ``src/cpy/Watchers/cWatchers.c``.
312319

313-
314-
To be Pythonic we create a Context Manager (see :ref:`chapter_context_manager`) in C.
315-
The context manager holds a reference to the dictionary and the watcher ID.
316-
Here is the definition which holds a watcher ID and a reference to the dictionary:
320+
First some module level CPython wrappers around our underlying C code:
317321

318322
.. code-block:: c
319323
@@ -322,6 +326,54 @@ Here is the definition which holds a watcher ID and a reference to the dictionar
322326
323327
#include "DictWatcher.h"
324328
329+
static PyObject *
330+
py_dict_watcher_verbose_add(PyObject *Py_UNUSED(module), PyObject *arg) {
331+
if (!PyDict_Check(arg)) {
332+
PyErr_Format(PyExc_TypeError, "Argument must be a dict not type %s", Py_TYPE(arg)->tp_name);
333+
return NULL;
334+
}
335+
long watcher_id = dict_watcher_verbose_add(arg);
336+
return Py_BuildValue("l", watcher_id);
337+
}
338+
339+
static PyObject *
340+
py_dict_watcher_verbose_remove(PyObject *Py_UNUSED(module), PyObject *args) {
341+
long watcher_id;
342+
PyObject *dict = NULL;
343+
344+
if (!PyArg_ParseTuple(args, "lO", &watcher_id, &dict)) {
345+
return NULL;
346+
}
347+
348+
if (!PyDict_Check(dict)) {
349+
PyErr_Format(PyExc_TypeError, "Argument must be a dict not type %s", Py_TYPE(dict)->tp_name);
350+
return NULL;
351+
}
352+
long result = dict_watcher_verbose_remove(watcher_id, dict);
353+
return Py_BuildValue("l", result);
354+
}
355+
356+
static PyMethodDef module_methods[] = {
357+
{"py_dict_watcher_verbose_add",
358+
(PyCFunction) py_dict_watcher_verbose_add,
359+
METH_O,
360+
"Adds watcher to a dictionary. Returns the watcher ID."
361+
},
362+
{"py_dict_watcher_verbose_remove",
363+
(PyCFunction) py_dict_watcher_verbose_remove,
364+
METH_VARARGS,
365+
"Removes the watcher ID from the dictionary."
366+
},
367+
{NULL, NULL, 0, NULL} /* Sentinel */
368+
};
369+
370+
These are file but to be Pythonic it would be helpful to create a Context Manager
371+
(see :ref:`chapter_context_manager`) in C.
372+
The context manager holds a reference to the dictionary and the watcher ID.
373+
Here is the definition which holds a watcher ID and a reference to the dictionary:
374+
375+
.. code-block:: c
376+
325377
#pragma mark Dictionary Watcher Context Manager
326378
327379
typedef struct {
@@ -330,11 +382,6 @@ Here is the definition which holds a watcher ID and a reference to the dictionar
330382
PyObject *dict;
331383
} PyDictWatcher;
332384
333-
/** Forward declaration. */
334-
static PyTypeObject PyDictWatcher_Type;
335-
336-
#define PyDictWatcher_Check(v) (Py_TYPE(v) == &PyDictWatcher_Type)
337-
338385
Here is the creation code:
339386

340387
.. code-block:: c
@@ -356,6 +403,14 @@ Here is the creation code:
356403
if (!PyArg_ParseTuple(args, "O", &self->dict)) {
357404
return NULL;
358405
}
406+
if (!PyDict_Check(self->dict)) {
407+
PyErr_Format(
408+
PyExc_TypeError,
409+
"Argument must be a dictionary not a %s",
410+
Py_TYPE(self->dict)->tp_name
411+
);
412+
return NULL;
413+
}
359414
Py_INCREF(self->dict);
360415
return (PyObject *)self;
361416
}
@@ -390,11 +445,11 @@ watcher from the dictionary:
390445
391446
static PyObject *
392447
PyDictWatcher_exit(PyDictWatcher *self, PyObject *Py_UNUSED(args)) {
393-
long result = dict_watcher_verbose_remove(self->watcher_id, self->dict);
448+
int result = dict_watcher_verbose_remove(self->watcher_id, self->dict);
394449
if (result) {
395450
PyErr_Format(
396451
PyExc_RuntimeError,
397-
"dict_watcher_verbose_remove() returned %ld",
452+
"dict_watcher_verbose_remove() returned %d",
398453
result
399454
);
400455
Py_RETURN_TRUE;
@@ -491,6 +546,22 @@ And the result on ``stdout`` is something like:
491546
492547
watcher_example.py 14 dict_watcher_demo PyDict_EVENT_ADDED Dict: {} Key (str): age New value (int): 42
493548
549+
Without the Context Manager
550+
---------------------------
551+
552+
If you are putting in some debugging code then a context manager might not be convenient.
553+
``cWatchers`` provides two functions, ``py_dict_watcher_verbose_add()`` and
554+
``py_dict_watcher_verbose_remove`` that achieve the same aim:
555+
556+
.. code-block:: python
557+
558+
from cPyExtPatt import cWatchers
559+
560+
d = {}
561+
watcher_id = cWatchers.py_dict_watcher_verbose_add(d)
562+
d['age'] = 42
563+
cWatchers.py_dict_watcher_verbose_remove(watcher_id, d)
564+
494565
495566
.. _PyType_AddWatcher(): https://docs.python.org/3/c-api/type.html#c.PyType_AddWatcher
496567
.. _PyType_ClearWatcher(): https://docs.python.org/3/c-api/type.html#c.PyType_ClearWatcher

src/cpy/Watchers/cWatchers.c

+6-2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ PyDictWatcher_init(PyDictWatcher *self, PyObject *args) {
7878
if (!PyArg_ParseTuple(args, "O", &self->dict)) {
7979
return NULL;
8080
}
81+
if (!PyDict_Check(self->dict)) {
82+
PyErr_Format(PyExc_TypeError, "Argument must be a dictionary not a %s", Py_TYPE(self->dict)->tp_name);
83+
return NULL;
84+
}
8185
Py_INCREF(self->dict);
8286
return (PyObject *)self;
8387
}
@@ -97,9 +101,9 @@ PyDictWatcher_enter(PyDictWatcher *self, PyObject *Py_UNUSED(args)) {
97101

98102
static PyObject *
99103
PyDictWatcher_exit(PyDictWatcher *self, PyObject *Py_UNUSED(args)) {
100-
long result = dict_watcher_verbose_remove(self->watcher_id, self->dict);
104+
int result = dict_watcher_verbose_remove(self->watcher_id, self->dict);
101105
if (result) {
102-
PyErr_Format(PyExc_RuntimeError, "dict_watcher_verbose_remove() returned %ld", result);
106+
PyErr_Format(PyExc_RuntimeError, "dict_watcher_verbose_remove() returned %d", result);
103107
Py_RETURN_TRUE;
104108
}
105109
Py_RETURN_FALSE;

src/cpy/Watchers/watcher_example.py

+9
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ def dict_watcher_deallocated() -> None:
7272
del dd
7373

7474

75+
def dict_watcher_add_no_context_manager() -> None:
76+
print('dict_watcher_add_no_context_manager():')
77+
d = {}
78+
watcher_id = cWatchers.py_dict_watcher_verbose_add(d)
79+
d['age'] = 42
80+
cWatchers.py_dict_watcher_verbose_remove(watcher_id, d)
81+
82+
7583
# def temp() -> None:
7684
# d = {}
7785
# cm = cWatchers.PyDictWatcher(d)
@@ -101,6 +109,7 @@ def main() -> int:
101109
dict_watcher_del()
102110
dict_watcher_cloned()
103111
dict_watcher_deallocated()
112+
dict_watcher_add_no_context_manager()
104113
return 0
105114

106115

0 commit comments

Comments
 (0)