]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-125608: Trigger dictionary watchers when inline values change (#125611)
authorSam Gross <colesbury@gmail.com>
Mon, 21 Oct 2024 12:23:38 +0000 (08:23 -0400)
committerGitHub <noreply@github.com>
Mon, 21 Oct 2024 12:23:38 +0000 (08:23 -0400)
Dictionary watchers on an object's attributes dictionary
(`object.__dict__`) were not triggered when the managed dictionary used
the object's inline values.

Lib/test/test_capi/test_watchers.py
Misc/NEWS.d/next/C_API/2024-10-16-19-28-23.gh-issue-125608.gTsU2g.rst [new file with mode: 0644]
Objects/dictobject.c

index 4bb764bf9d0963e0f8a6d51d17570c3ee8c675a4..e578a622a03487b92f313bc7cb3f581ad52b35f3 100644 (file)
@@ -97,6 +97,23 @@ class TestDictWatchers(unittest.TestCase):
             del d
             self.assert_events(["dealloc"])
 
+    def test_object_dict(self):
+        class MyObj: pass
+        o = MyObj()
+
+        with self.watcher() as wid:
+            self.watch(wid, o.__dict__)
+            o.foo = "bar"
+            o.foo = "baz"
+            del o.foo
+            self.assert_events(["new:foo:bar", "mod:foo:baz", "del:foo"])
+
+        with self.watcher() as wid:
+            self.watch(wid, o.__dict__)
+            for _ in range(100):
+                o.foo = "bar"
+            self.assert_events(["new:foo:bar"] + ["mod:foo:bar"] * 99)
+
     def test_unwatch(self):
         d = {}
         with self.watcher() as wid:
diff --git a/Misc/NEWS.d/next/C_API/2024-10-16-19-28-23.gh-issue-125608.gTsU2g.rst b/Misc/NEWS.d/next/C_API/2024-10-16-19-28-23.gh-issue-125608.gTsU2g.rst
new file mode 100644 (file)
index 0000000..e70f9f1
--- /dev/null
@@ -0,0 +1,3 @@
+Fix a bug where dictionary watchers (e.g., :c:func:`PyDict_Watch`) on an
+object's attribute dictionary (:attr:`~object.__dict__`) were not triggered
+when the object's attributes were modified.
index b27599d2815c82e75e763ab463621a60db353cf7..806096f5814062cfe9364507b2623c1ba5d4eab0 100644 (file)
@@ -6835,15 +6835,24 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values,
     }
 
     PyObject *old_value = values->values[ix];
+    if (old_value == NULL && value == NULL) {
+        PyErr_Format(PyExc_AttributeError,
+                        "'%.100s' object has no attribute '%U'",
+                        Py_TYPE(obj)->tp_name, name);
+        return -1;
+    }
+
+    if (dict) {
+        PyInterpreterState *interp = _PyInterpreterState_GET();
+        PyDict_WatchEvent event = (old_value == NULL ? PyDict_EVENT_ADDED :
+                                   value == NULL ? PyDict_EVENT_DELETED :
+                                   PyDict_EVENT_MODIFIED);
+        _PyDict_NotifyEvent(interp, event, dict, name, value);
+    }
+
     FT_ATOMIC_STORE_PTR_RELEASE(values->values[ix], Py_XNewRef(value));
 
     if (old_value == NULL) {
-        if (value == NULL) {
-            PyErr_Format(PyExc_AttributeError,
-                         "'%.100s' object has no attribute '%U'",
-                         Py_TYPE(obj)->tp_name, name);
-            return -1;
-        }
         _PyDictValues_AddToInsertionOrder(values, ix);
         if (dict) {
             assert(dict->ma_values == values);