Skip to content

Commit 9d33600

Browse files
committed
pythongh-139772: Add PyDict_FromItems() function
1 parent cc6b62a commit 9d33600

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

Doc/c-api/dict.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ Dictionary Objects
3636
Return a new empty dictionary, or ``NULL`` on failure.
3737
3838
39+
.. 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)
40+
41+
Create a dictionary from *keys* and *values* of *length* items.
42+
43+
*keys_offset* is the offset to access the *keys* array and
44+
*values_offset* is the offset to access the *values* array.
45+
*keys_offset* and *values_offset* must be greater than ``0``.
46+
47+
If *length* is ``0``, *keys*, *keys_offset*, *values* and *values_offset*
48+
arguments are ignored, and an empty dictionary is created.
49+
50+
Return a new dictionary, or ``NULL`` on failure.
51+
52+
.. versionadded:: next
53+
54+
3955
.. c:function:: PyObject* PyDictProxy_New(PyObject *mapping)
4056
4157
Return a :class:`types.MappingProxyType` object for a mapping which

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,10 @@ New features
10841084

10851085
(Contributed by Victor Stinner in :gh:`129813`.)
10861086

1087+
* Add :c:func:`PyDict_FromItems` to create a dictionary from an array of keys
1088+
and an array of values.
1089+
(Contributed by Victor Stinner in :gh:`139772`.)
1090+
10871091
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
10881092
(Contributed by Victor Stinner in :gh:`111489`.)
10891093

Include/cpython/dictobject.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,10 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
103103
// Mark given dictionary as "watched" (callback will be called if it is modified)
104104
PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
105105
PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);
106+
107+
PyAPI_FUNC(PyObject*) PyDict_FromItems(
108+
PyObject *const *keys,
109+
Py_ssize_t keys_offset,
110+
PyObject *const *values,
111+
Py_ssize_t values_offset,
112+
Py_ssize_t length);

Lib/test/test_capi/test_dict.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,51 @@ def test_dict_popstring(self):
545545
# CRASHES dict_popstring({}, NULL)
546546
# CRASHES dict_popstring({"a": 1}, NULL)
547547

548+
def test_dict_fromitems(self):
549+
# Test PyDict_FromItems()
550+
dict_fromitems = _testcapi.dict_fromitems
551+
552+
d = dict_fromitems((), 1, (), 1)
553+
self.assertEqual(d, {})
554+
555+
d = dict_fromitems(tuple(range(1, 4)), 1, tuple('abc'), 1)
556+
self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'})
557+
558+
# test unicode keys
559+
d = dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 1)
560+
self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3})
561+
562+
# test "large" dict (1024 items)
563+
d = dict_fromitems(tuple(range(1024)), 1,
564+
tuple(map(str, range(1024))), 1)
565+
self.assertEqual(d, {i: str(i) for i in range(1024)})
566+
567+
# same array for keys and values with keys_offset=values_offset=2
568+
array = ('a', 1, 'b', 2, 'c', 3)
569+
d = dict_fromitems(array, 2)
570+
self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3})
571+
572+
array = ('a', 1, None, 'b', 2, None, 'c', 3, None)
573+
d = dict_fromitems(array, 3)
574+
self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3})
575+
576+
# Test PyDict_FromItems(NULL, 0, NULL, 0, 0)
577+
d = dict_fromitems()
578+
self.assertEqual(d, {})
579+
580+
# test invalid arguments
581+
errmsg = "keys_offset must be greater than 0"
582+
with self.assertRaisesRegex(ValueError, errmsg):
583+
dict_fromitems(tuple('abc'), 0, tuple(range(1, 4)), 1)
584+
585+
errmsg = "values_offset must be greater than 0"
586+
with self.assertRaisesRegex(ValueError, errmsg):
587+
dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 0)
588+
589+
errmsg = "length must be greater than or equal to 0"
590+
with self.assertRaisesRegex(ValueError, errmsg):
591+
dict_fromitems(tuple('abc'), 1, tuple(range(1, 4)), 1, -1)
592+
548593

549594
if __name__ == "__main__":
550595
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyDict_FromItems` to create a dictionary from an array of keys and
2+
an array of values. Patch by Victor Stinner.

Modules/_testcapi/dict.c

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,59 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored))
258258
}
259259

260260

261+
static PyObject*
262+
dict_fromitems(PyObject* self, PyObject *args)
263+
{
264+
PyObject *keys_obj = UNINITIALIZED_PTR, *values_obj = UNINITIALIZED_PTR;
265+
Py_ssize_t keys_offset = UNINITIALIZED_SIZE, values_offset = UNINITIALIZED_SIZE;
266+
Py_ssize_t length = UNINITIALIZED_SIZE;
267+
if (!PyArg_ParseTuple(args, "|O!nO!nn",
268+
&PyTuple_Type, &keys_obj, &keys_offset,
269+
&PyTuple_Type, &values_obj, &values_offset,
270+
&length)) {
271+
return NULL;
272+
}
273+
274+
PyObject **keys, **values;
275+
if (keys_obj != UNINITIALIZED_PTR) {
276+
keys = &PyTuple_GET_ITEM(keys_obj, 0);
277+
if (values_obj != UNINITIALIZED_PTR) {
278+
values = &PyTuple_GET_ITEM(values_obj, 0);
279+
}
280+
else {
281+
values = keys + 1;
282+
}
283+
}
284+
else {
285+
keys = NULL;
286+
values = NULL;
287+
}
288+
289+
if (keys_offset == UNINITIALIZED_SIZE) {
290+
keys_offset = 0;
291+
}
292+
if (values_offset == UNINITIALIZED_SIZE) {
293+
values_offset = keys_offset;
294+
}
295+
296+
if (length == UNINITIALIZED_SIZE) {
297+
if (keys_obj != UNINITIALIZED_PTR) {
298+
if (keys_offset >= 1) {
299+
length = PyTuple_GET_SIZE(keys_obj) / keys_offset;
300+
}
301+
else {
302+
length = PyTuple_GET_SIZE(keys_obj);
303+
}
304+
}
305+
else {
306+
length = 0;
307+
}
308+
}
309+
310+
return PyDict_FromItems(keys, keys_offset, values, values_offset, length);
311+
}
312+
313+
261314
static PyMethodDef test_methods[] = {
262315
{"dict_containsstring", dict_containsstring, METH_VARARGS},
263316
{"dict_getitemref", dict_getitemref, METH_VARARGS},
@@ -268,7 +321,8 @@ static PyMethodDef test_methods[] = {
268321
{"dict_pop_null", dict_pop_null, METH_VARARGS},
269322
{"dict_popstring", dict_popstring, METH_VARARGS},
270323
{"dict_popstring_null", dict_popstring_null, METH_VARARGS},
271-
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
324+
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
325+
{"dict_fromitems", dict_fromitems, METH_VARARGS},
272326
{NULL},
273327
};
274328

Objects/dictobject.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,6 +2230,10 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset,
22302230
PyObject *const *values, Py_ssize_t values_offset,
22312231
Py_ssize_t length)
22322232
{
2233+
assert(keys == NULL || keys_offset >= 1);
2234+
assert(values == NULL || values_offset >= 1);
2235+
assert(length >= 0);
2236+
22332237
bool unicode = true;
22342238
PyObject *const *ks = keys;
22352239

@@ -2263,6 +2267,32 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset,
22632267
return dict;
22642268
}
22652269

2270+
2271+
PyObject *
2272+
PyDict_FromItems(PyObject *const *keys, Py_ssize_t keys_offset,
2273+
PyObject *const *values, Py_ssize_t values_offset,
2274+
Py_ssize_t length)
2275+
{
2276+
if (keys != NULL && keys_offset < 1) {
2277+
PyErr_SetString(PyExc_ValueError,
2278+
"keys_offset must be greater than 0");
2279+
return NULL;
2280+
}
2281+
if (values != NULL && values_offset < 1) {
2282+
PyErr_SetString(PyExc_ValueError,
2283+
"values_offset must be greater than 0");
2284+
return NULL;
2285+
}
2286+
if (length < 0) {
2287+
PyErr_SetString(PyExc_ValueError,
2288+
"length must be greater than or equal to 0");
2289+
return NULL;
2290+
}
2291+
2292+
return _PyDict_FromItems(keys, keys_offset, values, values_offset, length);
2293+
}
2294+
2295+
22662296
/* Note that, for historical reasons, PyDict_GetItem() suppresses all errors
22672297
* that may occur (originally dicts supported only string keys, and exceptions
22682298
* weren't possible). So, while the original intent was that a NULL return

0 commit comments

Comments
 (0)