Skip to content

Commit bb2c546

Browse files
committed
gh-141510: Add frozendict_check_mutable()
frozendict.fromkeys() now checks if it can mutate the newly created frozendict.
1 parent 7a7521b commit bb2c546

File tree

2 files changed

+42
-0
lines changed

2 files changed

+42
-0
lines changed

Lib/test/test_dict.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,6 +1787,24 @@ def test_hash(self):
17871787
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
17881788
hash(fd)
17891789

1790+
def test_fromkeys(self):
1791+
self.assertEqual(frozendict.fromkeys('abc'),
1792+
frozendict(a=None, b=None, c=None))
1793+
1794+
# special class which keeps a reference to the created dictionary
1795+
fd = None
1796+
class SpecialDict(frozendict):
1797+
def __new__(self):
1798+
nonlocal fd
1799+
fd = frozendict()
1800+
return fd
1801+
1802+
errmsg = "cannot mutate frozendict already exposed in Python"
1803+
with self.assertRaisesRegex(RuntimeError, errmsg):
1804+
SpecialDict.fromkeys(frozendict(x=1))
1805+
with self.assertRaisesRegex(RuntimeError, errmsg):
1806+
SpecialDict.fromkeys("def")
1807+
17901808

17911809
if __name__ == "__main__":
17921810
unittest.main()

Objects/dictobject.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ As a consequence of this, split keys have a maximum size of 16.
138138
// Forward declarations
139139
static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
140140
PyObject *kwds);
141+
static int frozendict_check_mutable(PyObject *self);
141142

142143

143144
/*[clinic input]
@@ -3317,6 +3318,11 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)
33173318
}
33183319
}
33193320
else if (PyFrozenDict_CheckExact(d)) {
3321+
// Check if the class constructor kept a reference to the frozendict
3322+
if (frozendict_check_mutable(d) < 0) {
3323+
return NULL;
3324+
}
3325+
33203326
if (PyDict_CheckExact(iterable)) {
33213327
PyDictObject *mp = (PyDictObject *)d;
33223328

@@ -3360,6 +3366,11 @@ dict_iter_exit:;
33603366
Py_END_CRITICAL_SECTION();
33613367
}
33623368
else if (PyFrozenDict_CheckExact(d)) {
3369+
// Check if the class constructor kept a reference to the frozendict
3370+
if (frozendict_check_mutable(d) < 0) {
3371+
goto Fail;
3372+
}
3373+
33633374
while ((key = PyIter_Next(it)) != NULL) {
33643375
// anydict_setitem_take2 consumes a reference to key
33653376
status = anydict_setitem_take2((PyDictObject *)d,
@@ -7889,6 +7900,17 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj)
78897900

78907901
// --- frozendict implementation ---------------------------------------------
78917902

7903+
static int
7904+
frozendict_check_mutable(PyObject *self)
7905+
{
7906+
if (Py_REFCNT(self) > 1) {
7907+
PyErr_SetString(PyExc_RuntimeError,
7908+
"cannot mutate frozendict already exposed in Python");
7909+
return -1;
7910+
}
7911+
return 0;
7912+
}
7913+
78927914
static PyNumberMethods frozendict_as_number = {
78937915
.nb_or = frozendict_or,
78947916
};
@@ -7994,6 +8016,8 @@ frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
79948016
if (d == NULL) {
79958017
return NULL;
79968018
}
8019+
assert(Py_REFCNT(self) == 1);
8020+
79978021
PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d);
79988022
self->ma_hash = -1;
79998023

0 commit comments

Comments
 (0)