Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ Dictionary objects

Return a new dictionary that contains the same key-value pairs as *p*.

If the argument is a :class:`frozendict`, convert it to a :class:`dict`
(create a copy).

.. versionchanged:: next
If *p* is a subclass of :class:`frozendict`, the result will be a
:class:`frozendict` instance instead of a :class:`dict` instance.
Accept also :class:`frozendict` type.

.. c:function:: int PyDict_SetItem(PyObject *p, PyObject *key, PyObject *val)

Expand Down
2 changes: 0 additions & 2 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,6 @@ extern void _PyDict_Clear_LockHeld(PyObject *op);
PyAPI_FUNC(void) _PyDict_EnsureSharedOnRead(PyDictObject *mp);
#endif

extern PyObject* _PyDict_CopyAsDict(PyObject *op);

#define DKIX_EMPTY (-1)
#define DKIX_DUMMY (-2) /* Used internally */
#define DKIX_ERROR (-3)
Expand Down
12 changes: 2 additions & 10 deletions Lib/test/test_capi/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,8 @@ def test_dict_copy(self):
for dict_type in ANYDICT_TYPES:
dct = dict_type({1: 2})
dct_copy = copy(dct)
if dict_type == frozendict:
expected_type = frozendict
self.assertIs(dct_copy, dct)
else:
if issubclass(dict_type, frozendict):
expected_type = frozendict
else:
expected_type = dict
self.assertIs(type(dct_copy), expected_type)
self.assertEqual(dct_copy, dct)
self.assertIs(type(dct_copy), dict)
self.assertEqual(dct_copy, dct)

for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
self.assertRaises(SystemError, copy, test_type())
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1848,11 +1848,19 @@ def test_merge(self):
frozendict({'x': 1, 'y': 2}))
self.assertEqual(frozendict(x=1, y=2) | frozendict(y=5),
frozendict({'x': 1, 'y': 5}))
self.assertEqual(FrozenDict(x=1, y=2) | FrozenDict(y=5),
frozendict({'x': 1, 'y': 5}))

fd = frozendict(x=1, y=2)
self.assertIs(fd | frozendict(), fd)
self.assertIs(fd | {}, fd)
self.assertIs(frozendict() | fd, fd)

fd = FrozenDict(x=1, y=2)
self.assertEqual(fd | frozendict(), fd)
self.assertEqual(fd | {}, fd)
self.assertEqual(frozendict() | fd, fd)

def test_update(self):
# test "a |= b" operator
d = frozendict(x=1)
Expand All @@ -1863,6 +1871,11 @@ def test_update(self):
self.assertEqual(d, frozendict({'x': 1, 'y': 2}))
self.assertEqual(copy, frozendict({'x': 1}))

def test_items_xor(self):
# test "a ^ b" operator on items views
res = frozendict(a=1, b=2).items() ^ frozendict(b=2, c=3).items()
self.assertEqual(res, {('a', 1), ('c', 3)})

def test_repr(self):
d = frozendict()
self.assertEqual(repr(d), "frozendict()")
Expand Down
20 changes: 19 additions & 1 deletion Objects/clinic/dictobject.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 33 additions & 25 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,9 @@ static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override);

/*[clinic input]
class dict "PyDictObject *" "&PyDict_Type"
class frozendict "PyFrozenDictObject *" "&PyFrozenDict_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=f157a5a0ce9589d6]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=5dfa93bac68e7c54]*/


/*
Expand Down Expand Up @@ -4384,19 +4385,6 @@ copy_lock_held(PyObject *o, int as_frozendict)
return NULL;
}

// Similar to PyDict_Copy(), but copy also frozendict.
static PyObject *
_PyDict_Copy(PyObject *o)
{
assert(PyAnyDict_Check(o));

PyObject *res;
Py_BEGIN_CRITICAL_SECTION(o);
res = copy_lock_held(o, PyFrozenDict_Check(o));
Py_END_CRITICAL_SECTION();
return res;
}

PyObject *
PyDict_Copy(PyObject *o)
{
Expand All @@ -4405,22 +4393,23 @@ PyDict_Copy(PyObject *o)
return NULL;
}

if (PyFrozenDict_CheckExact(o)) {
return Py_NewRef(o);
}

return _PyDict_Copy(o);
PyObject *res;
Py_BEGIN_CRITICAL_SECTION(o);
Copy link
Member

@corona10 corona10 Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even we want to copy as dict from frozendict, critical section is actually needed for frozendict?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to skip the critical section for frozendict, at least for the PyFrozenDict_CheckExact() case. But I would prefer to work on such optimization in a separated PR since this PR (and PR gh-145531) is already quite complex.

res = copy_lock_held(o, 0);
Py_END_CRITICAL_SECTION();
return res;
}

// Similar to PyDict_Copy(), but return a dict if the argument is a frozendict.
PyObject*
_PyDict_CopyAsDict(PyObject *o)
// Similar to PyDict_Copy(), but return a frozendict if the argument
// is a frozendict.
static PyObject *
_PyDict_Copy(PyObject *o)
{
assert(PyAnyDict_Check(o));

PyObject *res;
Py_BEGIN_CRITICAL_SECTION(o);
res = copy_lock_held(o, 0);
res = copy_lock_held(o, PyFrozenDict_Check(o));
Py_END_CRITICAL_SECTION();
return res;
}
Expand Down Expand Up @@ -6523,7 +6512,7 @@ dictitems_xor_lock_held(PyObject *d1, PyObject *d2)
ASSERT_DICT_LOCKED(d1);
ASSERT_DICT_LOCKED(d2);

PyObject *temp_dict = copy_lock_held(d1, PyFrozenDict_Check(d1));
PyObject *temp_dict = copy_lock_held(d1, 0);
if (temp_dict == NULL) {
return NULL;
}
Expand Down Expand Up @@ -8057,7 +8046,7 @@ static PyMethodDef frozendict_methods[] = {
DICT_ITEMS_METHODDEF
DICT_VALUES_METHODDEF
DICT_FROMKEYS_METHODDEF
DICT_COPY_METHODDEF
FROZENDICT_COPY_METHODDEF
DICT___REVERSED___METHODDEF
{"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See PEP 585")},
{"__getnewargs__", frozendict_getnewargs, METH_NOARGS},
Expand Down Expand Up @@ -8182,6 +8171,25 @@ PyFrozenDict_New(PyObject *iterable)
}
}

/*[clinic input]
frozendict.copy

Return a shallow copy of the frozendict.
[clinic start generated code]*/

static PyObject *
frozendict_copy_impl(PyFrozenDictObject *self)
/*[clinic end generated code: output=e580fd91d9fc2cf7 input=35f6abeaa08fd4bc]*/
{
assert(PyFrozenDict_Check(self));

if (PyFrozenDict_CheckExact(self)) {
return Py_NewRef(self);
}

return _PyDict_Copy((PyObject*)self);
}


PyTypeObject PyFrozenDict_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
Expand Down
2 changes: 1 addition & 1 deletion Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4850,7 +4850,7 @@ type_new_get_slots(type_new_ctx *ctx, PyObject *dict)
static PyTypeObject*
type_new_init(type_new_ctx *ctx)
{
PyObject *dict = _PyDict_CopyAsDict(ctx->orig_dict);
PyObject *dict = PyDict_Copy(ctx->orig_dict);
if (dict == NULL) {
goto error;
}
Expand Down
Loading