Skip to content

Commit 8d6ec8d

Browse files
committed
gh-145119: Allow frozendict to be assigned to instance __dict__
1 parent 5944a53 commit 8d6ec8d

File tree

5 files changed

+89
-10
lines changed

5 files changed

+89
-10
lines changed

Lib/test/test_capi/test_dict.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,9 @@ def test_dict_setitem(self):
273273
self.assertEqual(dct, {'a': 5, '\U0001f40d': 8})
274274

275275
self.assertRaises(TypeError, setitem, {}, [], 5) # unhashable
276-
for test_type in NOT_DICT_TYPES + OTHER_TYPES:
276+
for test_type in FROZENDICT_TYPES:
277+
self.assertRaises(TypeError, setitem, test_type(), 'a', 5)
278+
for test_type in MAPPING_TYPES + OTHER_TYPES:
277279
self.assertRaises(SystemError, setitem, test_type(), 'a', 5)
278280
# CRASHES setitem({}, NULL, 5)
279281
# CRASHES setitem({}, 'a', NULL)
@@ -290,7 +292,9 @@ def test_dict_setitemstring(self):
290292
self.assertEqual(dct, {'a': 5, '\U0001f40d': 8})
291293

292294
self.assertRaises(UnicodeDecodeError, setitemstring, {}, INVALID_UTF8, 5)
293-
for test_type in NOT_DICT_TYPES + OTHER_TYPES:
295+
for test_type in FROZENDICT_TYPES:
296+
self.assertRaises(TypeError, setitemstring, test_type(), b'a', 5)
297+
for test_type in MAPPING_TYPES + OTHER_TYPES:
294298
self.assertRaises(SystemError, setitemstring, test_type(), b'a', 5)
295299
# CRASHES setitemstring({}, NULL, 5)
296300
# CRASHES setitemstring({}, b'a', NULL)
@@ -308,7 +312,9 @@ def test_dict_delitem(self):
308312
self.assertEqual(dct, {'c': 2})
309313

310314
self.assertRaises(TypeError, delitem, {}, []) # unhashable
311-
for test_type in NOT_DICT_TYPES:
315+
for test_type in FROZENDICT_TYPES:
316+
self.assertRaises(TypeError, delitem, test_type({'a': 1}), 'a')
317+
for test_type in MAPPING_TYPES:
312318
self.assertRaises(SystemError, delitem, test_type({'a': 1}), 'a')
313319
for test_type in OTHER_TYPES:
314320
self.assertRaises(SystemError, delitem, test_type(), 'a')
@@ -327,7 +333,9 @@ def test_dict_delitemstring(self):
327333
self.assertEqual(dct, {'c': 2})
328334

329335
self.assertRaises(UnicodeDecodeError, delitemstring, {}, INVALID_UTF8)
330-
for test_type in NOT_DICT_TYPES:
336+
for test_type in FROZENDICT_TYPES:
337+
self.assertRaises(TypeError, delitemstring, test_type({'a': 1}), b'a')
338+
for test_type in MAPPING_TYPES:
331339
self.assertRaises(SystemError, delitemstring, test_type({'a': 1}), b'a')
332340
for test_type in OTHER_TYPES:
333341
self.assertRaises(SystemError, delitemstring, test_type(), b'a')

Lib/test/test_descr.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3570,6 +3570,45 @@ class Exception2(Base, Exception):
35703570
self.assertEqual(e.a, 1)
35713571
self.assertEqual(can_delete_dict(e), can_delete_dict(ValueError()))
35723572

3573+
def test_set_dict_to_frozendict(self):
3574+
# gh-145119: __dict__ accepts frozendict.
3575+
class C:
3576+
pass
3577+
3578+
obj = C()
3579+
obj.__dict__ = frozendict(x=1, y=2)
3580+
self.assertEqual(obj.x, 1)
3581+
self.assertEqual(obj.y, 2)
3582+
self.assertIn("x", dir(obj))
3583+
self.assertIn("y", dir(obj))
3584+
self.assertEqual(type(vars(obj)), frozendict)
3585+
3586+
with self.assertRaises(TypeError):
3587+
obj.z = 3
3588+
with self.assertRaises(TypeError):
3589+
del obj.x
3590+
3591+
class MyFrozenDict(frozendict):
3592+
pass
3593+
3594+
obj.__dict__ = MyFrozenDict(a=10)
3595+
self.assertEqual(obj.a, 10)
3596+
self.assertIn("a", dir(obj))
3597+
3598+
obj.__dict__ = {"w": 50}
3599+
obj.q = 99
3600+
self.assertEqual(obj.q, 99)
3601+
3602+
# Ensure internal PyDict_SetItem/DelItem paths raise TypeError,
3603+
# not SystemError, when __dict__ is a frozendict.
3604+
cm = classmethod(lambda: None)
3605+
cm.__dict__ = frozendict()
3606+
with self.assertRaises(TypeError):
3607+
cm.__annotations__ = {"x": int}
3608+
cm.__dict__ = frozendict(__annotations__={"x": int})
3609+
with self.assertRaises(TypeError):
3610+
del cm.__annotations__
3611+
35733612
def test_binary_operator_override(self):
35743613
# Testing overrides of binary operations...
35753614
class I(int):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow :class:`frozendict` to be assigned to an instance's
2+
:attr:`~object.__dict__`, enabling immutable instances.

Objects/dictobject.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,6 +2743,11 @@ int
27432743
PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value)
27442744
{
27452745
if (!PyDict_Check(op)) {
2746+
if (PyFrozenDict_Check(op)) {
2747+
PyErr_SetString(PyExc_TypeError,
2748+
"'frozendict' object does not support item assignment");
2749+
return -1;
2750+
}
27462751
PyErr_BadInternalCall();
27472752
return -1;
27482753
}
@@ -2883,6 +2888,11 @@ _PyDict_DelItem_KnownHash_LockHeld(PyObject *op, PyObject *key, Py_hash_t hash)
28832888
PyObject *old_value;
28842889

28852890
if (!PyDict_Check(op)) {
2891+
if (PyFrozenDict_Check(op)) {
2892+
PyErr_SetString(PyExc_TypeError,
2893+
"'frozendict' object does not support item deletion");
2894+
return -1;
2895+
}
28862896
PyErr_BadInternalCall();
28872897
return -1;
28882898
}
@@ -7064,6 +7074,17 @@ int
70647074
_PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
70657075
{
70667076
if (!PyDict_Check(dict)) {
7077+
if (PyFrozenDict_Check((PyObject *)dict)) {
7078+
if (value == NULL) {
7079+
PyErr_SetString(PyExc_TypeError,
7080+
"'frozendict' object does not support item deletion");
7081+
}
7082+
else {
7083+
PyErr_SetString(PyExc_TypeError,
7084+
"'frozendict' object does not support item assignment");
7085+
}
7086+
return -1;
7087+
}
70677088
PyErr_BadInternalCall();
70687089
return -1;
70697090
}

Objects/typeobject.c

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3999,7 +3999,7 @@ subtype_dict(PyObject *obj, void *context)
39993999
int
40004000
_PyObject_SetDict(PyObject *obj, PyObject *value)
40014001
{
4002-
if (value != NULL && !PyDict_Check(value)) {
4002+
if (value != NULL && !PyAnyDict_Check(value)) {
40034003
PyErr_Format(PyExc_TypeError,
40044004
"__dict__ must be set to a dictionary, "
40054005
"not a '%.200s'", Py_TYPE(value)->tp_name);
@@ -8305,15 +8305,24 @@ object___dir___impl(PyObject *self)
83058305
if (dict == NULL) {
83068306
dict = PyDict_New();
83078307
}
8308-
else if (!PyDict_Check(dict)) {
8309-
Py_DECREF(dict);
8310-
dict = PyDict_New();
8311-
}
8312-
else {
8308+
else if (PyDict_Check(dict)) {
83138309
/* Copy __dict__ to avoid mutating it. */
83148310
PyObject *temp = PyDict_Copy(dict);
83158311
Py_SETREF(dict, temp);
83168312
}
8313+
else if (PyFrozenDict_Check(dict)) {
8314+
/* Convert frozendict to a mutable dict for merging. */
8315+
PyObject *temp = PyDict_New();
8316+
if (temp != NULL && PyDict_Update(temp, dict) < 0) {
8317+
Py_DECREF(temp);
8318+
temp = NULL;
8319+
}
8320+
Py_SETREF(dict, temp);
8321+
}
8322+
else {
8323+
Py_DECREF(dict);
8324+
dict = PyDict_New();
8325+
}
83178326

83188327
if (dict == NULL)
83198328
goto error;

0 commit comments

Comments
 (0)