diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index b7f201811aad6c..ac3ad340c233c1 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -36,6 +36,31 @@ Dictionary Objects Return a new empty dictionary, or ``NULL`` on failure. +.. c:function:: PyObject* PyDict_FromKeysAndValues(PyObject *const *keys, PyObject *const *values, Py_ssize_t length) + + Create a dictionary from *keys* and *values* of *length* items. + + If *length* is ``0``, *keys* and *values* can be NULL. + + Return a new dictionary, or ``NULL`` on failure with an exception set. + + .. versionadded:: next + + +.. c:function:: PyObject* PyDict_FromItems(PyObject *const *items, Py_ssize_t length) + + Create a dictionary from *items* of *length* pairs (``(key, value)``). + + *items* is an array made of keys and values such as: + ``key1, value1, key2, value2, ..., keyN, valueN``. + + If *length* is ``0``, *items* can be NULL. + + Return a new dictionary, or ``NULL`` on failure with an exception set. + + .. 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..513dc49b297d0c 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1084,6 +1084,13 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:func:`PyDict_FromKeysAndValues` to create a dictionary from an array + of keys and an array of values. + (Contributed by Victor Stinner in :gh:`139772`.) + +* Add :c:func:`PyDict_FromItems` to create a dictionary from an array of items. + (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..b08b5b7db6248a 100644 --- a/Include/cpython/dictobject.h +++ b/Include/cpython/dictobject.h @@ -103,3 +103,12 @@ 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 *items, + Py_ssize_t length); + +PyAPI_FUNC(PyObject*) PyDict_FromKeysAndValues( + PyObject *const *keys, + PyObject *const *values, + Py_ssize_t length); diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index e726e3d813d888..39c4f46bd0e4bf 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -545,6 +545,63 @@ def test_dict_popstring(self): # CRASHES dict_popstring({}, NULL) # CRASHES dict_popstring({"a": 1}, NULL) + def test_dict_fromkeysandvalues(self): + # Test PyDict_FromKeysAndValues() + dict_fromkeysandvalues = _testcapi.dict_fromkeysandvalues + + d = dict_fromkeysandvalues((), ()) + self.assertEqual(d, {}) + + d = dict_fromkeysandvalues(tuple(range(1, 4)), tuple('abc')) + self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'}) + + # test unicode keys + d = dict_fromkeysandvalues(tuple('abc'), tuple(range(1, 4))) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + + # test "large" dict (1024 items) + d = dict_fromkeysandvalues(tuple(range(1024)), + tuple(map(str, range(1024)))) + self.assertEqual(d, {i: str(i) for i in range(1024)}) + + # Test PyDict_FromItems(NULL, NULL, 0) + d = dict_fromkeysandvalues() + self.assertEqual(d, {}) + + errmsg = "length must be greater than or equal to 0" + with self.assertRaisesRegex(ValueError, errmsg): + dict_fromkeysandvalues(tuple(range(1, 4)), tuple('abc'), -1) + + def test_dict_fromitems(self): + # Test PyDict_FromItems() + dict_fromitems = _testcapi.dict_fromitems + + d = dict_fromitems(()) + self.assertEqual(d, {}) + + d = dict_fromitems((1, 'a', 2, 'b', 3, 'c')) + self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'}) + + # test unicode keys + d = dict_fromitems(('a', 1, 'b', 2, 'c', 3)) + self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3}) + + # test "large" dict (1024 items) + items = [] + for key, value in zip(range(1024), map(str, range(1024))): + items.extend((key, value)) + d = dict_fromitems(tuple(items)) + self.assertEqual(d, {i: str(i) for i in range(1024)}) + + # Test PyDict_FromItems(NULL, 0) + d = dict_fromitems() + self.assertEqual(d, {}) + + # test invalid arguments + errmsg = "length must be greater than or equal to 0" + with self.assertRaisesRegex(ValueError, errmsg): + dict_fromitems(('x', 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..d99bcc84dd1290 --- /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 items. +Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/C_API/2025-11-17-22-48-02.gh-issue-139772.YwtOFa.rst b/Misc/NEWS.d/next/C_API/2025-11-17-22-48-02.gh-issue-139772.YwtOFa.rst new file mode 100644 index 00000000000000..5f8ef60cd56573 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-11-17-22-48-02.gh-issue-139772.YwtOFa.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyDict_FromKeysAndValues` 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..d02c67abc5fbde 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -258,6 +258,76 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored)) } +static PyObject* +dict_fromkeysandvalues(PyObject* self, PyObject *args) +{ + PyObject *keys_obj = UNINITIALIZED_PTR, *values_obj = UNINITIALIZED_PTR; + Py_ssize_t length = UNINITIALIZED_SIZE; + if (!PyArg_ParseTuple(args, "|O!O!n", + &PyTuple_Type, &keys_obj, + &PyTuple_Type, &values_obj, + &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 (length == UNINITIALIZED_SIZE) { + if (keys_obj != UNINITIALIZED_PTR) { + length = PyTuple_GET_SIZE(keys_obj); + } + else { + length = 0; + } + } + + return PyDict_FromKeysAndValues(keys, values, length); +} + + +static PyObject* +dict_fromitems(PyObject* self, PyObject *args) +{ + PyObject *items_obj = UNINITIALIZED_PTR; + Py_ssize_t length = UNINITIALIZED_SIZE; + if (!PyArg_ParseTuple(args, "|O!n", &PyTuple_Type, &items_obj, &length)) { + return NULL; + } + + PyObject **items; + if (items_obj != UNINITIALIZED_PTR) { + items = &PyTuple_GET_ITEM(items_obj, 0); + } + else { + items = NULL; + } + + if (length == UNINITIALIZED_SIZE) { + if (items_obj != UNINITIALIZED_PTR) { + length = PyTuple_GET_SIZE(items_obj) / 2; + } + else { + length = 0; + } + } + + return PyDict_FromItems(items, length); +} + + static PyMethodDef test_methods[] = { {"dict_containsstring", dict_containsstring, METH_VARARGS}, {"dict_getitemref", dict_getitemref, METH_VARARGS}, @@ -268,7 +338,9 @@ 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_fromkeysandvalues", dict_fromkeysandvalues, METH_VARARGS}, + {"dict_fromitems", dict_fromitems, METH_VARARGS}, {NULL}, }; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 14de21f3c67210..6987f1490194a8 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_offset >= 1); + assert(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 *items, Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be greater than or equal to 0"); + return NULL; + } + return _PyDict_FromItems(items, 2, items + 1, 2, length); +} + + +PyObject* +PyDict_FromKeysAndValues(PyObject *const *keys, PyObject *const *values, + Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be greater than or equal to 0"); + return NULL; + } + return _PyDict_FromItems(keys, 1, values, 1, 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