From 9d3360011c10c38b9765265d450adc3312efe9e2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 11 Oct 2025 18:44:39 +0200 Subject: [PATCH] gh-139772: Add PyDict_FromItems() function --- Doc/c-api/dict.rst | 16 ++++++ Doc/whatsnew/3.15.rst | 4 ++ Include/cpython/dictobject.h | 7 +++ Lib/test/test_capi/test_dict.py | 45 +++++++++++++++ ...-10-11-18-44-36.gh-issue-139772.GmNHrN.rst | 2 + Modules/_testcapi/dict.c | 56 ++++++++++++++++++- Objects/dictobject.c | 30 ++++++++++ 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-10-11-18-44-36.gh-issue-139772.GmNHrN.rst diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index b7f201811aad6c..80535f7e0e9d76 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -36,6 +36,22 @@ Dictionary Objects Return a new empty dictionary, or ``NULL`` on failure. +.. c:function:: PyObject* PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, PyObject *const *values, Py_ssize_t values_offset, Py_ssize_t length) + + Create a dictionary from *keys* and *values* of *length* items. + + *keys_offset* is the offset to access the *keys* array and + *values_offset* is the offset to access the *values* array. + *keys_offset* and *values_offset* must be greater than ``0``. + + If *length* is ``0``, *keys*, *keys_offset*, *values* and *values_offset* + arguments are ignored, and an empty dictionary is created. + + Return a new dictionary, or ``NULL`` on failure. + + .. versionadded:: next + + .. c:function:: PyObject* PyDictProxy_New(PyObject *mapping) Return a :class:`types.MappingProxyType` object for a mapping which diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index cf5bef15203b23..13a3e4a82a3398 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1084,6 +1084,10 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:func:`PyDict_FromItems` to create a dictionary from an array of keys + and an array of values. + (Contributed by Victor Stinner in :gh:`139772`.) + * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h index df9ec7050fca1a..6a61c1d085406f 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -103,3 +103,10 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id); // Mark given dictionary as "watched" (callback will be called if it is modified) PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict); PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict); + +PyAPI_FUNC(PyObject*) PyDict_FromItems( + PyObject *const *keys, + Py_ssize_t keys_offset, + PyObject *const *values, + Py_ssize_t values_offset, + Py_ssize_t length); diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index e726e3d813d888..e2e0b78642ab7c 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -545,6 +545,51 @@ def test_dict_popstring(self): # CRASHES dict_popstring({}, NULL) # CRASHES dict_popstring({"a": 1}, NULL) + def test_dict_fromitems(self): + # Test PyDict_FromItems() + dict_fromitems = _testcapi.dict_fromitems + + d = dict_fromitems((), 1, (), 1) + self.assertEqual(d, {}) + + d = dict_fromitems(tuple(range(1, 4)), 1, tuple('abc'), 1) + self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'}) + + # test unicode keys + d = dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 1) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + + # test "large" dict (1024 items) + d = dict_fromitems(tuple(range(1024)), 1, + tuple(map(str, range(1024))), 1) + self.assertEqual(d, {i: str(i) for i in range(1024)}) + + # same array for keys and values with keys_offset=values_offset=2 + array = ('a', 1, 'b', 2, 'c', 3) + d = dict_fromitems(array, 2) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + + array = ('a', 1, None, 'b', 2, None, 'c', 3, None) + d = dict_fromitems(array, 3) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + + # Test PyDict_FromItems(NULL, 0, NULL, 0, 0) + d = dict_fromitems() + self.assertEqual(d, {}) + + # test invalid arguments + errmsg = "keys_offset must be greater than 0" + with self.assertRaisesRegex(ValueError, errmsg): + dict_fromitems(tuple('abc'), 0, tuple(range(1, 4)), 1) + + errmsg = "values_offset must be greater than 0" + with self.assertRaisesRegex(ValueError, errmsg): + dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 0) + + errmsg = "length must be greater than or equal to 0" + with self.assertRaisesRegex(ValueError, errmsg): + dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 1, -1) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-10-11-18-44-36.gh-issue-139772.GmNHrN.rst b/Misc/NEWS.d/next/C_API/2025-10-11-18-44-36.gh-issue-139772.GmNHrN.rst new file mode 100644 index 00000000000000..d270063f2afc75 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-11-18-44-36.gh-issue-139772.GmNHrN.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyDict_FromItems` to create a dictionary from an array of keys and +an array of values. Patch by Victor Stinner. diff --git a/Modules/_testcapi/dict.c b/Modules/_testcapi/dict.c index b7c73d7332bd4e..434dc74981aae0 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -258,6 +258,59 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored)) } +static PyObject* +dict_fromitems(PyObject* self, PyObject *args) +{ + PyObject *keys_obj = UNINITIALIZED_PTR, *values_obj = UNINITIALIZED_PTR; + Py_ssize_t keys_offset = UNINITIALIZED_SIZE, values_offset = UNINITIALIZED_SIZE; + Py_ssize_t length = UNINITIALIZED_SIZE; + if (!PyArg_ParseTuple(args, "|O!nO!nn", + &PyTuple_Type, &keys_obj, &keys_offset, + &PyTuple_Type, &values_obj, &values_offset, + &length)) { + return NULL; + } + + PyObject **keys, **values; + if (keys_obj != UNINITIALIZED_PTR) { + keys = &PyTuple_GET_ITEM(keys_obj, 0); + if (values_obj != UNINITIALIZED_PTR) { + values = &PyTuple_GET_ITEM(values_obj, 0); + } + else { + values = keys + 1; + } + } + else { + keys = NULL; + values = NULL; + } + + if (keys_offset == UNINITIALIZED_SIZE) { + keys_offset = 0; + } + if (values_offset == UNINITIALIZED_SIZE) { + values_offset = keys_offset; + } + + if (length == UNINITIALIZED_SIZE) { + if (keys_obj != UNINITIALIZED_PTR) { + if (keys_offset >= 1) { + length = PyTuple_GET_SIZE(keys_obj) / keys_offset; + } + else { + length = PyTuple_GET_SIZE(keys_obj); + } + } + else { + length = 0; + } + } + + return PyDict_FromItems(keys, keys_offset, values, values_offset, length); +} + + static PyMethodDef test_methods[] = { {"dict_containsstring", dict_containsstring, METH_VARARGS}, {"dict_getitemref", dict_getitemref, METH_VARARGS}, @@ -268,7 +321,8 @@ static PyMethodDef test_methods[] = { {"dict_pop_null", dict_pop_null, METH_VARARGS}, {"dict_popstring", dict_popstring, METH_VARARGS}, {"dict_popstring_null", dict_popstring_null, METH_VARARGS}, - {"test_dict_iteration", test_dict_iteration, METH_NOARGS}, + {"test_dict_iteration", test_dict_iteration, METH_NOARGS}, + {"dict_fromitems", dict_fromitems, METH_VARARGS}, {NULL}, }; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 14de21f3c67210..fd8a6cff2544a7 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2230,6 +2230,10 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, PyObject *const *values, Py_ssize_t values_offset, Py_ssize_t length) { + assert(keys == NULL || keys_offset >= 1); + assert(values == NULL || values_offset >= 1); + assert(length >= 0); + bool unicode = true; PyObject *const *ks = keys; @@ -2263,6 +2267,32 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, return dict; } + +PyObject * +PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset, + PyObject *const *values, Py_ssize_t values_offset, + Py_ssize_t length) +{ + if (keys != NULL && keys_offset < 1) { + PyErr_SetString(PyExc_ValueError, + "keys_offset must be greater than 0"); + return NULL; + } + if (values != NULL && values_offset < 1) { + PyErr_SetString(PyExc_ValueError, + "values_offset must be greater than 0"); + return NULL; + } + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be greater than or equal to 0"); + return NULL; + } + + return _PyDict_FromItems(keys, keys_offset, values, values_offset, length); +} + + /* Note that, for historical reasons, PyDict_GetItem() suppresses all errors * that may occur (originally dicts supported only string keys, and exceptions * weren't possible). So, while the original intent was that a NULL return