@@ -30,7 +30,7 @@ Watchers
30
30
======================================
31
31
32
32
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 .
34
34
The callback is called with any event that occurs on the specific object.
35
35
36
36
Here is an example of a dictionary watcher.
@@ -54,9 +54,9 @@ Here is an example of a dictionary watcher.
54
54
Dictionary Watchers
55
55
---------------------------
56
56
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
58
58
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 ``.
60
60
61
61
.. code-block :: python
62
62
:linenos:
@@ -102,32 +102,43 @@ the arguments used to manipulate the dictionary:
102
102
103
103
\end {landscape}
104
104
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?
106
113
107
114
Low Level C Implementation
108
- ^^^^^^^^^^^^^^^^^^^^^^^^^^
115
+ --------------------------
109
116
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.
111
118
First a header file that provides the interface to our dictionary watcher code.
112
119
This declares two functions:
113
120
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.
115
122
- ``dict_watcher_verbose_remove() `` this removes a watcher ID from a dictionary.
116
123
117
124
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+ :
119
126
120
127
.. code-block :: c
121
128
122
129
#define PPY_SSIZE_T_CLEAN
123
130
#include "Python.h"
124
131
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
126
137
127
138
int dict_watcher_verbose_add(PyObject *dict);
128
139
int dict_watcher_verbose_remove(int watcher_id, PyObject *dict);
129
140
130
- #endif // #if PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 12
141
+ #endif // #if PY_VERSION_HEX >= 0x030C0000
131
142
132
143
So there are several moving parts in the implementation in ``src/cpy/Watchers/DictWatcher.c ``.
133
144
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
244
255
}
245
256
246
257
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:
249
261
250
262
.. code-block :: c
251
263
@@ -276,44 +288,36 @@ This has to respect NULL arguments:
276
288
}
277
289
278
290
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) :
280
292
281
293
.. code-block :: c
282
294
283
295
// Set watcher.
284
296
int dict_watcher_verbose_add(PyObject *dict) {
285
297
int watcher_id = PyDict_AddWatcher(&dict_watcher_verbose);
286
- int api_ret_val = PyDict_Watch(watcher_id, dict);
298
+ PyDict_Watch(watcher_id, dict);
287
299
return watcher_id;
288
300
}
289
301
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):
291
304
292
305
.. code-block :: c
293
306
294
307
// Remove watcher.
295
308
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);
304
311
return 0;
305
312
}
306
313
307
314
Exposing This to CPython
308
- ^^^^^^^^^^^^^^^^^^^^^^^^
315
+ ------------------------
309
316
310
317
Now we create a Python module ``cWatchers `` that exposes this low level C code to CPython.
311
318
This code is in ``src/cpy/Watchers/cWatchers.c ``.
312
319
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:
317
321
318
322
.. code-block :: c
319
323
@@ -322,6 +326,54 @@ Here is the definition which holds a watcher ID and a reference to the dictionar
322
326
323
327
#include "DictWatcher.h"
324
328
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
+
325
377
#pragma mark Dictionary Watcher Context Manager
326
378
327
379
typedef struct {
@@ -330,11 +382,6 @@ Here is the definition which holds a watcher ID and a reference to the dictionar
330
382
PyObject *dict;
331
383
} PyDictWatcher;
332
384
333
- /** Forward declaration. */
334
- static PyTypeObject PyDictWatcher_Type;
335
-
336
- #define PyDictWatcher_Check(v) (Py_TYPE(v) == &PyDictWatcher_Type)
337
-
338
385
Here is the creation code:
339
386
340
387
.. code-block :: c
@@ -356,6 +403,14 @@ Here is the creation code:
356
403
if (!PyArg_ParseTuple(args, "O", &self->dict)) {
357
404
return NULL;
358
405
}
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
+ }
359
414
Py_INCREF(self->dict);
360
415
return (PyObject *)self;
361
416
}
@@ -390,11 +445,11 @@ watcher from the dictionary:
390
445
391
446
static PyObject *
392
447
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);
394
449
if (result) {
395
450
PyErr_Format(
396
451
PyExc_RuntimeError,
397
- "dict_watcher_verbose_remove() returned %ld ",
452
+ "dict_watcher_verbose_remove() returned %d ",
398
453
result
399
454
);
400
455
Py_RETURN_TRUE;
@@ -491,6 +546,22 @@ And the result on ``stdout`` is something like:
491
546
492
547
watcher_example.py 14 dict_watcher_demo PyDict_EVENT_ADDED Dict: {} Key (str): age New value (int): 42
493
548
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
+
494
565
495
566
.. _PyType_AddWatcher() : https://docs.python.org/3/c-api/type.html#c.PyType_AddWatcher
496
567
.. _PyType_ClearWatcher() : https://docs.python.org/3/c-api/type.html#c.PyType_ClearWatcher
0 commit comments