8000 gh-111262: Add PyDict_Pop() function · vstinner/cpython@b3e7de4 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit b3e7de4

Browse files
vstinnerscoder
andcommitted
pythongh-111262: Add PyDict_Pop() function
Change the API of the internal _PyDict_Pop_KnownHash() function to return an int. Co-Authored-By: Stefan Behnel <stefan_ml@behnel.de>
1 parent d7cef7b commit b3e7de4

File tree

14 files changed

+165
-34
lines changed

14 files changed

+165
-34
lines changed

Doc/c-api/dict.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ Dictionary Objects
173173
174174
.. versionadded:: 3.4
175175
176+
177+
.. c:function:: int PyDict_Pop(PyObject *p, PyObject *key, PyObject *default_value, PyObject *result)
178+
179+
Remove *key* from dictionary *p* and return the removed value.
180+
181+
- If the key is present, set *\*result* to a :term:`strong reference` to the
182+
removed value, and return ``1``.
183+
- If the key is missing and *default_value* is not NULL, set *\*result*
184+
to a :term:`strong reference` to *default_value*, and return ``0``.
185+
- If the key is missing and *default_value* is NULL, raise :exc:`KeyError`,
186+
and return -1.
187+
- On error, raise an exception and return ``-1``.
188+
189+
This is the same as :meth:`dict.pop`.
190+
191+
.. versionadded:: 3.13
192+
176193
.. c:function:: PyObject* PyDict_Items(PyObject *p)
177194
178195
Return a :c:type:`PyListObject` containing all the items from the dictionary.

Doc/data/stable_abi.dat

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,10 @@ New Features
11641164
:c:func:`PyErr_WriteUnraisable`, but allow to customize the warning mesage.
11651165
(Contributed by Serhiy Storchaka in :gh:`108082`.)
11661166

1167+
* Add :c:func:`PyDict_Pop` function: remove a key from a dictionary and return
1168+
the removed value. This is the same as :meth:`dict.pop`.
1169+
(Contributed by Victor Stinner in :gh:`111262`.)
1170+
11671171

11681172
Porting to Python 3.13
11691173
----------------------

Include/dictobject.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ PyAPI_FUNC(int) PyDict_DelItemString(PyObject *dp, const char *key);
6666
// - On error, raise an exception and return -1.
6767
PyAPI_FUNC(int) PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result);
6868
PyAPI_FUNC(int) PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result);
69+
70+
PyAPI_FUNC(int) PyDict_Pop(PyObject *dict,
71+
PyObject *key,
72+
PyObject *default_value,
73+
PyObject **result);
6974
#endif
7075

7176
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000

Include/internal/pycore_dict.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ extern PyObject *_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *);
116116
extern int _PyDict_SetItem_Take2(PyDictObject *op, PyObject *key, PyObject *value);
117117
extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject **dictptr, PyObject *name, PyObject *value);
118118

119-
extern PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *);
119+
extern int _PyDict_Pop_KnownHash(
120+
PyDictObject *dict,
121+
PyObject *key,
122+
Py_hash_t hash,
123+
PyObject *default_value,
124+
PyObject **result);
120125

121126
#define DKIX_EMPTY (-1)
122127
#define DKIX_DUMMY (-2) /* Used internally */

Lib/test/test_capi/test_dict.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,49 @@ def test_dict_mergefromseq2(self):
432432
# CRASHES mergefromseq2({}, NULL, 0)
433433
# CRASHES mergefromseq2(NULL, {}, 0)
434434

435+
def test_dict_pop(self):
436+
# Test PyDict_Pop()
437+
dict_pop = _testcapi.dict_pop
438+
default = object()
439+
440+
def expect_value(mydict, key, expected_value):
441+
self.assertEqual(dict_pop(mydict.copy(), key, default),
442+
(1, expected_value))
443+
self.assertEqual(dict_pop(mydict.copy(), key, NULL),
444+
(1, expected_value))
445+
446+
def check_default(mydict, key):
447+
result, value = dict_pop(mydict, key, default)
448+
self.assertIs(value, default)
449+
self.assertEqual(result, 0)
450+
451+
# key present
452+
mydict = {"key": 2, "key2": 5}
453+
expect_value(mydict, "key", 2)
454+
expect_value(mydict, "key2", 5)
455+
456+
# key missing
457+
check_default({}, "key") # empty dict has a fast path
458+
check_default({"a": 1}, "key")
459+
self.assertRaises(KeyError, dict_pop, {}, "key", NULL)
460+
self.assertRaises(KeyError, dict_pop, {"a": 1}, "key", NULL)
461+
462+
# dict error
463+
not_dict = "string"
464+
self.assertRaises(SystemError, dict_pop, not_dict, "key", default)
465+
466+
# key error
467+
not_hashable_key = ["list"]
468+
check_default({}, not_hashable_key) # don't hash key if dict is empty
469+
with self.assertRaises(TypeError):
470+
dict_pop({'key': 1}, not_hashable_key, NULL)
471+
with self.assertRaises(TypeError):
472+
dict_pop({'key': 1}, not_hashable_key, default)
473+
dict_pop({}, NULL, default) # don't check key if dict is empty
474+
475+
# CRASHES dict_pop(NULL, "key")
476+
# CRASHES dict_pop({"a": 1}, NULL, default)
477+
435478

436479
if __name__ == "__main__":
437480
unittest.main()

Lib/test/test_stable_abi_ctypes.py

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :c:func:`PyDict_Pop` function: remove a key from a dictionary and return
2+
the removed value. This is the same as :meth:`dict.pop`. Patch by Stefan
3+
Behnel and Victor Stinner.

Misc/stable_abi.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2481,3 +2481,5 @@
24812481
[function._Py_SetRefcnt]
24822482
added = '3.13'
24832483
abi_only = true
2484+
[function.PyDict_Pop]
2485+
added = '3.13'

Modules/_functoolsmodule.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,8 +1087,8 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
10871087
The cache dict holds one reference to the link.
10881088
We created one other reference when the link was created.
10891089
The linked list only has borrowed references. */
1090-
popresult = _PyDict_Pop_KnownHash(self->cache, link->key,
1091-
link->hash, Py_None);
1090+
(void)_PyDict_Pop_KnownHash((PyDictObject*)self->cache, link->key,
1091+
link->hash, Py_None, &popresult);
10921092
if (popresult == Py_None) {
10931093
/* Getting here means that the user function call or another
10941094
thread has already removed the old key from the dictionary.

Modules/_testcapi/dict.c

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,25 @@ dict_mergefromseq2(PyObject *self, PyObject *args)
331331
}
332332

333333

334+
static PyObject *
335+
dict_pop(PyObject *self, PyObject *args)
336+
{
337+
PyObject *dict, *key, *default_value;
338+
if (!PyArg_ParseTuple(args, "OOO", &dict, &key, &default_value)) {
339+
return NULL;
340+
}
341+
NULLABLE(dict);
342+
NULLABLE(key);
343+
NULLABLE(default_value);
344+
PyObject *result = UNINITIALIZED_PTR;
345+
int res = PyDict_Pop(dict, key, default_value, &result);
346+
if (result == NULL) {
347+
return NULL;
348+
}
349+
return Py_BuildValue("iN", res, result);
350+
}
351+
352+
334353
static PyMethodDef test_methods[] = {
335354
{"dict_check", dict_check, METH_O},
336355
{"dict_checkexact", dict_checkexact, METH_O},
@@ -358,7 +377,7 @@ static PyMethodDef test_methods[] = {
358377
{"dict_merge", dict_merge, METH_VARARGS},
359378
{"dict_update", dict_update, METH_VARARGS},
360379
{"dict_mergefromseq2", dict_mergefromseq2, METH_VARARGS},
361-
380+
{"dict_pop", dict_pop, METH_VARARGS},
362381
{NULL},
363382
};
364383

Objects/dictobject.c

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,64 +2226,94 @@ PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
22262226
return _PyDict_Next(op, ppos, pkey, pvalue, NULL);
22272227
}
22282228

2229+
22292230
/* Internal version of dict.pop(). */
2230-
PyObject *
2231-
_PyDict_Pop_KnownHash(PyObject *dict, PyObject *key, Py_hash_t hash, PyObject *deflt)
2231+
int
2232+
_PyDict_Pop_KnownHash(PyDictObject *mp, PyObject *key, Py_hash_t hash,
2233+
PyObject *default_value, PyObject **result)
22322234
{
2233-
Py_ssize_t ix;
2234-
PyObject *old_value;
2235-
PyDictObject *mp;
2236-
PyInterpreterState *interp = _PyInterpreterState_GET();
2237-
2238-
assert(PyDict_Check(dict));
2239-
mp = (PyDictObject *)dict;
2235+
assert(PyDict_Check(mp));
22402236

22412237
if (mp->ma_used == 0) {
2242-
if (deflt) {
2243-
return Py_NewRef(deflt);
2238+
if (default_value) {
2239+
*result = Py_NewRef(default_value);
2240+
return 0;
22442241
}
2242+
*result = NULL;
22452243
_PyErr_SetKeyError(key);
2246-
return NULL;
2244+
return -1;
22472245
}
2248-
ix = _Py_dict_lookup(mp, key, hash, &old_value);
2249-
if (ix == DKIX_ERROR)
2250-
return NULL;
2246+
2247+
PyObject *old_value;
2248+
Py_ssize_t ix = _Py_dict_lookup(mp, key, hash, &old_value);
2249+
if (ix == DKIX_ERROR) {
2250+
*result = NULL;
2251+
return -1;
2252+
}
2253+
22512254
if (ix == DKIX_EMPTY || old_value == NULL) {
2252-
if (deflt) {
2253-
return Py_NewRef(deflt);
2255+
if (default_value) {
2256+
*result = Py_NewRef(default_value);
2257+
return 0;
22542258
}
2259+
*result = NULL;
22552260
_PyErr_SetKeyError(key);
2256-
return NULL;
2261+
return -1;
22572262
}
2263+
22582264
assert(old_value != NULL);
2265+
PyInterpreterState *interp = _PyInterpreterState_GET();
22592266
uint64_t new_version = _PyDict_NotifyEvent(
22602267
interp, PyDict_EVENT_DELETED, mp, key, NULL);
22612268
delitem_common(mp, hash, ix, Py_NewRef(old_value), new_version);
22622269

22632270
ASSERT_CONSISTENT(mp);
2264-
return old_value;
2271+
*result = old_value;
2272+
return 1;
22652273
}
22662274

2267-
PyObject *
2268-
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
2275+
2276+
int
2277+
PyDict_Pop(PyObject *op, PyObject *key, PyObject *default_value, PyObject **result)
22692278
{
2270-
Py_hash_t hash;
2279+
if (!PyDict_Check(op)) {
2280+
*result = NULL;
2281+
PyErr_BadInternalCall();
2282+
return -1;
2283+
}
2284+
PyDictObject *dict = (PyDictObject *)op;
22712285

2272-
if (((PyDictObject *)dict)->ma_used == 0) {
2273-
if (deflt) {
2274-
return Py_NewRef(deflt);
2286+
if (dict->ma_used == 0) {
2287+
if (default_value) {
2288+
*result = Py_NewRef(default_value);
2289+
return 0;
22752290
}
2291+
*result = NULL;
22762292
_PyErr_SetKeyError(key);
2277-
return NULL;
2293+
return -1;
22782294
}
2295+
2296+
Py_hash_t hash;
22792297
if (!PyUnicode_CheckExact(key) || (hash = unicode_get_hash(key)) == -1) {
22802298
hash = PyObject_Hash(key);
2281-
if (hash == -1)
2282-
return NULL;
2299+
if (hash == -1) {
2300+
*result = NULL;
2301+
return -1;
2302+
}
22832303
}
2284-
return _PyDict_Pop_KnownHash(dict, key, hash, deflt);
2304+
return _PyDict_Pop_KnownHash(dict, key, hash, default_value, result);
22852305
}
22862306

2307+
2308+
PyObject *
2309+
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *default_value)
2310+
{
2311+
PyObject *result;
2312+
(void)PyDict_Pop(dict, key, default_value, &result);
2313+
return result;
2314+
}
2315+
2316+
22872317
/* Internal version of dict.from_keys(). It is subclass-friendly. */
22882318
PyObject *
22892319
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)

Objects/odictobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ _odict_popkey_hash(PyObject *od, PyObject *key, PyObject *failobj,
10491049
return NULL;
10501050
}
10511051
/* Now delete the value from the dict. */
1052-
value = _PyDict_Pop_KnownHash(od, key, hash, failobj);
1052+
(void)_PyDict_Pop_KnownHash((PyDictObject *)od, key, hash, failobj, &value);
10531053
}
10541054
else if (value == NULL && !PyErr_Occurred()) {
10551055
/* Apply the fallback value, if necessary. */

PC/python3dll.c

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
0