Skip to content

Commit 6b702cf

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

File tree

4 files changed

+57
-6
lines changed

4 files changed

+57
-6
lines changed

Lib/test/test_descr.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3570,6 +3570,35 @@ 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+
35733602
def test_binary_operator_override(self):
35743603
# Testing overrides of binary operations...
35753604
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7064,6 +7064,17 @@ int
70647064
_PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
70657065
{
70667066
if (!PyDict_Check(dict)) {
7067+
if (PyFrozenDict_Check((PyObject *)dict)) {
7068+
if (value == NULL) {
7069+
PyErr_SetString(PyExc_TypeError,
7070+
"'frozendict' object does not support item deletion");
7071+
}
7072+
else {
7073+
PyErr_SetString(PyExc_TypeError,
7074+
"'frozendict' object does not support item assignment");
7075+
}
7076+
return -1;
7077+
}
70677078
PyErr_BadInternalCall();
70687079
return -1;
70697080
}

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)