Skip to content

Commit e4ff754

Browse files
committed
pythongh-139772: Add PyDict_FromItems() function
Add PyDict_FromKeysAndValues() and PyDict_FromItems() functions.
1 parent cc6b62a commit e4ff754

File tree

8 files changed

+205
-1
lines changed

8 files changed

+205
-1
lines changed

Doc/c-api/dict.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,31 @@ Dictionary Objects
3636
Return a new empty dictionary, or ``NULL`` on failure.
3737
3838
39+
.. c:function:: PyObject* PyDict_FromKeysAndValues(PyObject *const *keys, PyObject *const *values, Py_ssize_t length)
40+
41+
Create a dictionary from *keys* and *values* of *length* items.
42+
43+
If *length* is ``0``, *keys* and *values* can be NULL.
44+
45+
Return a new dictionary, or ``NULL`` on failure with an exception set.
46+
47+
.. versionadded:: next
48+
49+
50+
.. c:function:: PyObject* PyDict_FromItems(PyObject *const *items, Py_ssize_t length)
51+
52+
Create a dictionary from *items* of *length* pairs (``(key, value)``).
53+
54+
*items* is an array made of keys and values such as:
55+
``key1, value1, key2, value2, ..., keyN, valueN``.
56+
57+
If *length* is ``0``, *items* can be NULL.
58+
59+
Return a new dictionary, or ``NULL`` on failure with an exception set.
60+
61+
.. versionadded:: next
62+
63+
3964
.. c:function:: PyObject* PyDictProxy_New(PyObject *mapping)
4065
4166
Return a :class:`types.MappingProxyType` object for a mapping which

Doc/whatsnew/3.15.rst

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

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

1087+
* Add :c:func:`PyDict_FromKeysAndValues` to create a dictionary from an array
1088+
of keys and an array of values.
1089+
(Contributed by Victor Stinner in :gh:`139772`.)
1090+
1091+
* Add :c:func:`PyDict_FromItems` to create a dictionary from an array of items.
1092+
(Contributed by Victor Stinner in :gh:`139772`.)
1093+
10871094
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
10881095
(Contributed by Victor Stinner in :gh:`111489`.)
10891096

Include/cpython/dictobject.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,12 @@ 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 *items,
109+
Py_ssize_t length);
110+
111+
PyAPI_FUNC(PyObject*) PyDict_FromKeysAndValues(
112+
PyObject *const *keys,
113+
PyObject *const *values,
114+
Py_ssize_t length);

Lib/test/test_capi/test_dict.py

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

548+
def test_dict_fromkeysandvalues(self):
549+
# Test PyDict_FromKeysAndValues()
550+
dict_fromkeysandvalues = _testcapi.dict_fromkeysandvalues
551+
552+
d = dict_fromkeysandvalues((), ())
553+
self.assertEqual(d, {})
554+
555+
d = dict_fromkeysandvalues(tuple(range(1, 4)), tuple('abc'))
556+
self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'})
557+
558+
# test unicode keys
559+
d = dict_fromkeysandvalues(tuple('abc'), tuple(range(1, 4)))
560+
self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3})
561+
562+
# test "large" dict (1024 items)
563+
d = dict_fromkeysandvalues(tuple(range(1024)),
564+
tuple(map(str, range(1024))))
565+
self.assertEqual(d, {i: str(i) for i in range(1024)})
566+
567+
# Test PyDict_FromItems(NULL, NULL, 0)
568+
d = dict_fromkeysandvalues()
569+
self.assertEqual(d, {})
570+
571+
errmsg = "length must be greater than or equal to 0"
572+
with self.assertRaisesRegex(ValueError, errmsg):
573+
dict_fromkeysandvalues(tuple(range(1, 4)), tuple('abc'), -1)
574+
575+
def test_dict_fromitems(self):
576+
# Test PyDict_FromItems()
577+
dict_fromitems = _testcapi.dict_fromitems
578+
579+
d = dict_fromitems(())
580+
self.assertEqual(d, {})
581+
582+
d = dict_fromitems((1, 'a', 2, 'b', 3, 'c'))
583+
self.assertEqual(d, {1: 'a', 2: 'b', 3: 'c'})
584+
585+
# test unicode keys
586+
d = dict_fromitems(('a', 1, 'b', 2, 'c', 3))
587+
self.assertEqual(d, {'a': 1, 'b': 2, 'c': 3})
588+
589+
# test "large" dict (1024 items)
590+
items = []
591+
for key, value in zip(range(1024), map(str, range(1024))):
592+
items.extend((key, value))
593+
d = dict_fromitems(tuple(items))
594+
self.assertEqual(d, {i: str(i) for i in range(1024)})
595+
596+
# Test PyDict_FromItems(NULL, 0)
597+
d = dict_fromitems()
598+
self.assertEqual(d, {})
599+
600+
# test invalid arguments
601+
errmsg = "length must be greater than or equal to 0"
602+
with self.assertRaisesRegex(ValueError, errmsg):
603+
dict_fromitems(('x', 1), -1)
604+
548605

549606
if __name__ == "__main__":
550607
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 items.
2+
Patch by Victor Stinner.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyDict_FromKeysAndValues` to create a dictionary from an array
2+
of keys and an array of values. Patch by Victor Stinner.

Modules/_testcapi/dict.c

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

260260

261+
static PyObject*
262+
dict_fromkeysandvalues(PyObject* self, PyObject *args)
263+
{
264+
PyObject *keys_obj = UNINITIALIZED_PTR, *values_obj = UNINITIALIZED_PTR;
265+
Py_ssize_t length = UNINITIALIZED_SIZE;
266+
if (!PyArg_ParseTuple(args, "|O!O!n",
267+
&PyTuple_Type, &keys_obj,
268+
&PyTuple_Type, &values_obj,
269+
&length)) {
270+
return NULL;
271+
}
272+
273+
PyObject **keys, **values;
274+
if (keys_obj != UNINITIALIZED_PTR) {
275+
keys = &PyTuple_GET_ITEM(keys_obj, 0);
276+
if (values_obj != UNINITIALIZED_PTR) {
277+
values = &PyTuple_GET_ITEM(values_obj, 0);
278+
}
279+
else {
280+
values = keys + 1;
281+
}
282+
}
283+
else {
284+
keys = NULL;
285+
values = NULL;
286+
}
287+
288+
if (length == UNINITIALIZED_SIZE) {
289+
if (keys_obj != UNINITIALIZED_PTR) {
290+
length = PyTuple_GET_SIZE(keys_obj);
291+
}
292+
else {
293+
length = 0;
294+
}
295+
}
296+
297+
return PyDict_FromKeysAndValues(keys, values, length);
298+
}
299+
300+
301+
static PyObject*
302+
dict_fromitems(PyObject* self, PyObject *args)
303+
{
304+
PyObject *items_obj = UNINITIALIZED_PTR;
305+
Py_ssize_t length = UNINITIALIZED_SIZE;
306+
if (!PyArg_ParseTuple(args, "|O!n", &PyTuple_Type, &items_obj, &length)) {
307+
return NULL;
308+
}
309+
310+
PyObject **items;
311+
if (items_obj != UNINITIALIZED_PTR) {
312+
items = &PyTuple_GET_ITEM(items_obj, 0);
313+
}
314+
else {
315+
items = NULL;
316+
}
317+
318+
if (length == UNINITIALIZED_SIZE) {
319+
if (items_obj != UNINITIALIZED_PTR) {
320+
length = PyTuple_GET_SIZE(items_obj) / 2;
321+
}
322+
else {
323+
length = 0;
324+
}
325+
}
326+
327+
return PyDict_FromItems(items, length);
328+
}
329+
330+
261331
static PyMethodDef test_methods[] = {
262332
{"dict_containsstring", dict_containsstring, METH_VARARGS},
263333
{"dict_getitemref", dict_getitemref, METH_VARARGS},
@@ -268,7 +338,9 @@ static PyMethodDef test_methods[] = {
268338
{"dict_pop_null", dict_pop_null, METH_VARARGS},
269339
{"dict_popstring", dict_popstring, METH_VARARGS},
270340
{"dict_popstring_null", dict_popstring_null, METH_VARARGS},
271-
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
341+
{"test_dict_iteration", test_dict_iteration, METH_NOARGS},
342+
{"dict_fromkeysandvalues", dict_fromkeysandvalues, METH_VARARGS},
343+
{"dict_fromitems", dict_fromitems, METH_VARARGS},
272344
{NULL},
273345
};
274346

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_offset >= 1);
2234+
assert(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 *items, Py_ssize_t length)
2273+
{
2274+
if (length < 0) {
2275+
PyErr_SetString(PyExc_ValueError,
2276+
"length must be greater than or equal to 0");
2277+
return NULL;
2278+
}
2279+
return _PyDict_FromItems(items, 2, items + 1, 2, length);
2280+
}
2281+
2282+
2283+
PyObject*
2284+
PyDict_FromKeysAndValues(PyObject *const *keys, PyObject *const *values,
2285+
Py_ssize_t length)
2286+
{
2287+
if (length < 0) {
2288+
PyErr_SetString(PyExc_ValueError,
2289+
"length must be greater than or equal to 0");
2290+
return NULL;
2291+
}
2292+
return _PyDict_FromItems(keys, 1, values, 1, 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)