Skip to content

Commit 15f7bd4

Browse files
[3.14] gh-132617: Fix dict.update() mutation check (gh-134815) (gh-135581)
Use `ma_used` instead of `ma_keys->dk_nentries` for modification check so that we only check if the dictionary is modified, not if new keys are added to a different dictionary that shared the same keys object. (cherry picked from commit d8994b0) Co-authored-by: Sam Gross <colesbury@gmail.com>
1 parent 027f584 commit 15f7bd4

File tree

3 files changed

+37
-2
lines changed

3 files changed

+37
-2
lines changed

Lib/test/test_dict.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,38 @@ def badgen():
290290
['Cannot convert dictionary update sequence element #0 to a sequence'],
291291
)
292292

293+
def test_update_shared_keys(self):
294+
class MyClass: pass
295+
296+
# Subclass str to enable us to create an object during the
297+
# dict.update() call.
298+
class MyStr(str):
299+
def __hash__(self):
300+
return super().__hash__()
301+
302+
def __eq__(self, other):
303+
# Create an object that shares the same PyDictKeysObject as
304+
# obj.__dict__.
305+
obj2 = MyClass()
306+
obj2.a = "a"
307+
obj2.b = "b"
308+
obj2.c = "c"
309+
return super().__eq__(other)
310+
311+
obj = MyClass()
312+
obj.a = "a"
313+
obj.b = "b"
314+
315+
x = {}
316+
x[MyStr("a")] = MyStr("a")
317+
318+
# gh-132617: this previously raised "dict mutated during update" error
319+
x.update(obj.__dict__)
320+
321+
self.assertEqual(x, {
322+
MyStr("a"): "a",
323+
"b": "b",
324+
})
293325

294326
def test_fromkeys(self):
295327
self.assertEqual(dict.fromkeys('abc'), {'a':None, 'b':None, 'c':None})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :meth:`dict.update` modification check that could incorrectly raise a
2+
"dict mutated during update" error when a different dictionary was modified
3+
that happens to share the same underlying keys object.

Objects/dictobject.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3858,7 +3858,7 @@ dict_dict_merge(PyInterpreterState *interp, PyDictObject *mp, PyDictObject *othe
38583858
}
38593859
}
38603860

3861-
Py_ssize_t orig_size = other->ma_keys->dk_nentries;
3861+
Py_ssize_t orig_size = other->ma_used;
38623862
Py_ssize_t pos = 0;
38633863
Py_hash_t hash;
38643864
PyObject *key, *value;
@@ -3892,7 +3892,7 @@ dict_dict_merge(PyInterpreterState *interp, PyDictObject *mp, PyDictObject *othe
38923892
if (err != 0)
38933893
return -1;
38943894

3895-
if (orig_size != other->ma_keys->dk_nentries) {
3895+
if (orig_size != other->ma_used) {
38963896
PyErr_SetString(PyExc_RuntimeError,
38973897
"dict mutated during update");
38983898
return -1;

0 commit comments

Comments
 (0)