Skip to content

Commit 22df94b

Browse files
committed
gh-153410: Add PyDict_AsFrozenDictAndClear C-API
1 parent f3fd9dc commit 22df94b

8 files changed

Lines changed: 110 additions & 7 deletions

File tree

Doc/c-api/dict.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,20 @@ Dictionary objects
507507
modified by another thread.
508508
509509
510+
.. c:function:: PyObject* PyDict_AsFrozenDictAndClear(PyObject *dict)
511+
512+
Return a new :class:`frozendict` created
513+
from the existing :c:type:`PyDictObject` instance.
514+
Expects *dict* to be an exact :class:`dict` instance.
515+
516+
Transfers all keys and values from *dict*
517+
to the newly allocated :c:type:`PyFrozenDict` with O(1) complexity.
518+
Clears the input *dict* on success.
519+
Returns ``NULL`` with the exception set on error.
520+
521+
.. versionadded:: next
522+
523+
510524
.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
511525
512526
Register *callback* as a dictionary watcher. Return a non-negative integer

Doc/whatsnew/3.16.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,9 @@ C API changes
668668
New features
669669
------------
670670

671-
* TODO
671+
* Add a new way to efficitly create :class:`frozendict` instance
672+
from existing :class:`dict` instances: :c:func:`PyDict_AsFrozenDictAndClear`.
673+
672674

673675
Porting to Python 3.16
674676
----------------------

Include/cpython/dictobject.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,7 @@ PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);
106106

107107
// Create a frozendict. Create an empty dictionary if iterable is NULL.
108108
PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable);
109+
110+
// Create a frozendict from existing `PyDictObject`.
111+
// Transfers existing keys and values.
112+
PyAPI_FUNC(PyObject*) PyDict_AsFrozenDictAndClear(PyObject *dict);

Lib/test/test_capi/test_dict.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,26 @@ def test_dict_popstring(self):
609609
# CRASHES dict_popstring({}, NULL)
610610
# CRASHES dict_popstring({"a": 1}, NULL)
611611

612+
def test_dict_as_frozendict_and_clear(self):
613+
# Test PyDict_AsFrozenDictAndClear()
614+
check = _testcapi.dict_as_frozendict_and_clear
615+
d = {1: 2, 'a': 'b'}
616+
f = check(d)
617+
self.assertIs(type(f), frozendict)
618+
self.assertEqual(f, {1: 2, 'a': 'b'})
619+
self.assertIs(type(d), dict)
620+
self.assertEqual(d, {})
621+
622+
d = {}
623+
f = check(d)
624+
self.assertIs(type(f), frozendict)
625+
self.assertEqual(f, {})
626+
self.assertIs(type(d), dict)
627+
self.assertEqual(d, {})
628+
# CRASHES check([])
629+
# CRASHES check(frozendict())
630+
# CRASHES check(NULL)
631+
612632
def test_frozendict_check(self):
613633
# Test PyFrozenDict_Check()
614634
check = _testcapi.frozendict_check
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :c:func:`PyDict_AsFrozenDictAndClear` C-API function to create
2+
:class:`frozendict` from existing :class:`dict` object and clear the input
3+
dict instance.

Modules/_testcapi/dict.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,13 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored))
257257
Py_RETURN_NONE;
258258
}
259259

260+
static PyObject *
261+
dict_as_frozendict_and_clear(PyObject *self, PyObject *obj)
262+
{
263+
NULLABLE(obj);
264+
return PyDict_AsFrozenDictAndClear(obj);
265+
}
266+
260267

261268
static PyObject *
262269
frozendict_check(PyObject *self, PyObject *obj)
@@ -306,6 +313,7 @@ static PyMethodDef test_methods[] = {
306313
{"dict_popstring", dict_popstring, METH_VARARGS},
307314
{"dict_popstring_null", dict_popstring_null, METH_VARARGS},
308315
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
316+
{"dict_as_frozendict_and_clear", dict_as_frozendict_and_clear, METH_O},
309317
{"frozendict_check", frozendict_check, METH_O},
310318
{"frozendict_checkexact", frozendict_checkexact, METH_O},
311319
{"anydict_check", anydict_check, METH_O},

Objects/dictobject.c

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3120,6 +3120,18 @@ clear_embedded_values(PyDictValues *values, Py_ssize_t nentries)
31203120
}
31213121
}
31223122

3123+
// This function is used in both `PyDict_Clear`
3124+
// and `PyDict_AsFrozenDictAndClear`, place all the common parts here.
3125+
static void
3126+
clear_common(PyDictObject *mp)
3127+
{
3128+
/* Empty the dict... */
3129+
_PyDict_NotifyEvent(PyDict_EVENT_CLEARED, mp, NULL, NULL);
3130+
// We don't inc ref empty keys because they're immortal
3131+
ensure_shared_on_resize(mp);
3132+
STORE_USED(mp, 0);
3133+
}
3134+
31233135
static void
31243136
clear_lock_held(PyObject *op)
31253137
{
@@ -3138,11 +3150,7 @@ clear_lock_held(PyObject *op)
31383150
if (oldkeys == Py_EMPTY_KEYS) {
31393151
return;
31403152
}
3141-
/* Empty the dict... */
3142-
_PyDict_NotifyEvent(PyDict_EVENT_CLEARED, mp, NULL, NULL);
3143-
// We don't inc ref empty keys because they're immortal
3144-
ensure_shared_on_resize(mp);
3145-
STORE_USED(mp, 0);
3153+
clear_common(mp);
31463154
if (oldvalues == NULL) {
31473155
set_keys(mp, Py_EMPTY_KEYS);
31483156
assert(oldkeys->dk_refcnt == 1);
@@ -8521,6 +8529,50 @@ frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
85218529
return d;
85228530
}
85238531

8532+
static void
8533+
transfer_keys_and_values_lock_held(PyObject *res, PyObject *dict)
8534+
{
8535+
8536+
PyDictObject *new = (PyDictObject *)res;
8537+
PyDictObject *old = (PyDictObject *)dict;
8538+
// Fast path: do nothing on an empty dict:
8539+
if (old->ma_keys == Py_EMPTY_KEYS) {
8540+
return;
8541+
}
8542+
8543+
// Transfer keys and values from dict to res:
8544+
STORE_USED(new, old->ma_used);
8545+
set_keys(new, old->ma_keys);
8546+
set_values(new, old->ma_values);
8547+
ASSERT_CONSISTENT(new);
8548+
8549+
// Now, clear the old dict keys and values, but do not decref them:
8550+
clear_common((PyDictObject *)dict);
8551+
set_keys(old, Py_EMPTY_KEYS);
8552+
set_values(old, NULL);
8553+
ASSERT_CONSISTENT(dict);
8554+
}
8555+
8556+
PyObject *
8557+
PyDict_AsFrozenDictAndClear(PyObject *dict)
8558+
{
8559+
assert(dict != NULL);
8560+
assert(PyDict_CheckExact(dict));
8561+
assert(can_modify_dict((PyDictObject *)dict));
8562+
PyObject *res = frozendict_new_untracked(&PyFrozenDict_Type);
8563+
if (res == NULL) {
8564+
return NULL;
8565+
}
8566+
8567+
Py_BEGIN_CRITICAL_SECTION(dict);
8568+
transfer_keys_and_values_lock_held(res, dict);
8569+
Py_END_CRITICAL_SECTION();
8570+
8571+
_PyObject_GC_TRACK(res);
8572+
assert(_PyFrozenDictObject_CAST(res)->ma_hash == -1);
8573+
return res;
8574+
}
8575+
85248576

85258577
PyObject*
85268578
PyFrozenDict_New(PyObject *iterable)

Python/marshal.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,7 @@ r_object(RFILE *p)
15011501
Py_CLEAR(v);
15021502
}
15031503
if (type == TYPE_FROZENDICT && v != NULL) {
1504-
Py_SETREF(v, PyFrozenDict_New(v));
1504+
Py_SETREF(v, PyDict_AsFrozenDictAndClear(v));
15051505
}
15061506
retval = v;
15071507
break;

0 commit comments

Comments
 (0)