Skip to content

Commit bb8358a

Browse files
vstinnerscoderpitrou
committed
pythongh-111262: Add PyDict_Pop() function
_PyDict_Pop_KnownHash(): remove the default value and the return type becomes an int. Co-Authored-By: Stefan Behnel <stefan_ml@behnel.de> Co-authored-by: Antoine Pitrou <pitrou@free.fr>
1 parent 1c7ed7e commit bb8358a

19 files changed

+199
-67
lines changed

Doc/c-api/dict.rst

+15
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,21 @@ Dictionary Objects
173173
174174
.. versionadded:: 3.4
175175
176+
177+
.. c:function:: int PyDict_Pop(PyObject *p, PyObject *key, PyObject **result)
178+
179+
Remove *key* from dictionary *p* and save the removed value.
180+
181+
- If the key is present, set *\*result* to a new reference to the removed
182+
value (if *result* is not ``NULL``), and return ``1``.
183+
- If the key is missing, set *\*result* to ``NULL`` (if *result* is not
184+
``NULL``), and return ``0``.
185+
- On error, raise an exception and return ``-1``.
186+
187+
This is the similar to :meth:`dict.pop` without the default value.
188+
189+
.. versionadded:: 3.13
190+
176191
.. c:function:: PyObject* PyDict_Items(PyObject *p)
177192
178193
Return a :c:type:`PyListObject` containing all the items from the dictionary.

Doc/data/stable_abi.dat

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.13.rst

+5
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,11 @@ 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 similar to :meth:`dict.pop` without the
1169+
default value.
1170+
(Contributed by Victor Stinner in :gh:`111262`.)
1171+
11671172

11681173
Porting to Python 3.13
11691174
----------------------

Include/dictobject.h

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ 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, PyObject *key, PyObject **result);
6971
#endif
7072

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

Include/internal/pycore_dict.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ 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 **result);
120124

121125
#define DKIX_EMPTY (-1)
122126
#define DKIX_DUMMY (-2) /* Used internally */

Lib/test/test_capi/test_dict.py

+34
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,40 @@ 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+
439+
def expect_value(mydict, key, expected_value):
440+
self.assertEqual(dict_pop(mydict.copy(), key),
441+
(1, expected_value))
442+
443+
def expect_missing(mydict, key):
444+
self.assertEqual(dict_pop(mydict, key), (0, None))
445+
446+
# key present
447+
mydict = {"key": 2, "key2": 5}
448+
expect_value(mydict, "key", 2)
449+
expect_value(mydict, "key2", 5)
450+
451+
# key missing
452+
expect_missing({}, "key") # empty dict has a fast path
453+
expect_missing({"a": 1}, "key")
454+
455+
# dict error
456+
not_dict = "string"
457+
self.assertRaises(SystemError, dict_pop, not_dict, "key")
458+
459+
# key error
460+
not_hashable_key = ["list"]
461+
expect_missing({}, not_hashable_key) # don't hash key if dict is empty
462+
with self.assertRaises(TypeError):
463+
dict_pop({'key': 1}, not_hashable_key)
464+
dict_pop({}, NULL) # key is not checked if dict is empty
465+
466+
# CRASHES dict_pop(NULL, "key")
467+
# CRASHES dict_pop({"a": 1}, NULL, default)
468+
435469

436470
if __name__ == "__main__":
437471
unittest.main()

Lib/test/test_stable_abi_ctypes.py

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
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 similar to :meth:`dict.pop` without the default
3+
value. Patch by Stefan Behnel and Victor Stinner.

Misc/stable_abi.toml

+2
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

+15-13
Original file line numberDiff line numberDiff line change
@@ -1087,19 +1087,9 @@ 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);
1092-
if (popresult == Py_None) {
1093-
/* Getting here means that the user function call or another
1094-
thread has already removed the old key from the dictionary.
1095-
This link is now an orphan. Since we don't want to leave the
1096-
cache in an inconsistent state, we don't restore the link. */
1097-
Py_DECREF(popresult);
1098-
Py_DECREF(link);
1099-
Py_DECREF(key);
1100-
return result;
1101-
}
1102-
if (popresult == NULL) {
1090+
int res = _PyDict_Pop_KnownHash((PyDictObject*)self->cache, link->key,
1091+
link->hash, &popresult);
1092+
if (res < 0) {
11031093
/* An error arose while trying to remove the oldest key (the one
11041094
being evicted) from the cache. We restore the link to its
11051095
original position as the oldest link. Then we allow the
@@ -1110,10 +1100,22 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
11101100
Py_DECREF(result);
11111101
return NULL;
11121102
}
1103+
if (res == 0) {
1104+
/* Getting here means that the user function call or another
1105+
thread has already removed the old key from the dictionary.
1106+
This link is now an orphan. Since we don't want to leave the
1107+
cache in an inconsistent state, we don't restore the link. */
1108+
assert(popresult == NULL);
1109+
Py_DECREF(link);
1110+
Py_DECREF(key);
1111+
return result;
1112+
}
1113+
11131114
/* Keep a reference to the old key and old result to prevent their
11141115
ref counts from going to zero during the update. That will
11151116
prevent potentially arbitrary object clean-up code (i.e. __del__)
11161117
from running while we're still adjusting the links. */
1118+
assert(popresult != NULL);
11171119
oldkey = link->key;
11181120
oldresult = link->result;
11191121

Modules/_testcapi/dict.c

+22-1
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,27 @@ dict_mergefromseq2(PyObject *self, PyObject *args)
331331
}
332332

333333

334+
static PyObject *
335+
dict_pop(PyObject *self, PyObject *args)
336+
{
337+
PyObject *dict, *key;
338+
if (!PyArg_ParseTuple(args, "OO", &dict, &key)) {
339+
return NULL;
340+
}
341+
NULLABLE(dict);
342+
NULLABLE(key);
343+
PyObject *result = UNINITIALIZED_PTR;
344+
int res = PyDict_Pop(dict, key, &result);
345+
if (res < 0) {
346+
return NULL;
347+
}
348+
if (result == NULL) {
349+
result = Py_NewRef(Py_None);
350+
}
351+
return Py_BuildValue("iN", res, result);
352+
}
353+
354+
334355
static PyMethodDef test_methods[] = {
335356
{"dict_check", dict_check, METH_O},
336357
{"dict_checkexact", dict_checkexact, METH_O},
@@ -358,7 +379,7 @@ static PyMethodDef test_methods[] = {
358379
{"dict_merge", dict_merge, METH_VARARGS},
359380
{"dict_update", dict_update, METH_VARARGS},
360381
{"dict_mergefromseq2", dict_mergefromseq2, METH_VARARGS},
361-
382+
{"dict_pop", dict_pop, METH_VARARGS},
362383
{NULL},
363384
};
364385

Modules/_threadmodule.c

+2-5
Original file line numberDiff line numberDiff line change
@@ -967,11 +967,8 @@ local_clear(localobject *self)
967967
HEAD_UNLOCK(runtime);
968968
while (tstate) {
969969
if (tstate->dict) {
970-
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
971-
if (v != NULL) {
972-
Py_DECREF(v);
973-
}
974-
else {
970+
if (PyDict_Pop(tstate->dict, self->key, NULL) < 0) {
971+
// Silently ignore error
975972
PyErr_Clear();
976973
}
977974
}

Modules/socketmodule.c

+3-4
Original file line numberDiff line numberDiff line change
@@ -396,12 +396,11 @@ remove_unusable_flags(PyObject *m)
396396
if (flag_name == NULL) {
397397
return -1;
398398
}
399-
PyObject *v = _PyDict_Pop(dict, flag_name, Py_None);
400-
Py_DECREF(flag_name);
401-
if (v == NULL) {
399+
if (PyDict_Pop(dict, flag_name, NULL) < 0) {
400+
Py_DECREF(flag_name);
402401
return -1;
403402
}
404-
Py_DECREF(v);
403+
Py_DECREF(flag_name);
405404
}
406405
}
407406
return 0;

Objects/dictobject.c

+70-32
Original file line numberDiff line numberDiff line change
@@ -2226,64 +2226,102 @@ 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 **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 (result) {
2239+
*result = NULL;
22442240
}
2245-
_PyErr_SetKeyError(key);
2246-
return NULL;
2241+
return 0;
22472242
}
2248-
ix = _Py_dict_lookup(mp, key, hash, &old_value);
2249-
if (ix == DKIX_ERROR)
2250-
return NULL;
2243+
2244+
PyObject *old_value;
2245+
Py_ssize_t ix = _Py_dict_lookup(mp, key, hash, &old_value);
2246+
if (ix == DKIX_ERROR) {
2247+
if (result) {
2248+
*result = NULL;
2249+
}
2250+
return -1;
2251+
}
2252+
22512253
if (ix == DKIX_EMPTY || old_value == NULL) {
2252-
if (deflt) {
2253-
return Py_NewRef(deflt);
2254+
if (result) {
2255+
*result = NULL;
22542256
}
2255-
_PyErr_SetKeyError(key);
2256-
return NULL;
2257+
return 0;
22572258
}
2259+
22582260
assert(old_value != NULL);
2261+
PyInterpreterState *interp = _PyInterpreterState_GET();
22592262
uint64_t new_version = _PyDict_NotifyEvent(
22602263
interp, PyDict_EVENT_DELETED, mp, key, NULL);
22612264
delitem_common(mp, hash, ix, Py_NewRef(old_value), new_version);
22622265

22632266
ASSERT_CONSISTENT(mp);
2264-
return old_value;
2267+
if (result) {
2268+
*result = old_value;
2269+
}
2270+
else {
2271+
Py_DECREF(old_value);
2272+
}
2273+
return 1;
22652274
}
22662275

2267-
PyObject *
2268-
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
2276+
2277+
int
2278+
PyDict_Pop(PyObject *op, PyObject *key, PyObject **result)
22692279
{
2270-
Py_hash_t hash;
2280+
if (!PyDict_Check(op)) {
2281+
if (result) {
2282+
*result = NULL;
2283+
}
2284+
PyErr_BadInternalCall();
2285+
return -1;
2286+
}
2287+
PyDictObject *dict = (PyDictObject *)op;
22712288

2272-
if (((PyDictObject *)dict)->ma_used == 0) {
2273-
if (deflt) {
2274-
return Py_NewRef(deflt);
2289+
if (dict->ma_used == 0) {
2290+
if (result) {
2291+
*result = NULL;
22752292
}
2276-
_PyErr_SetKeyError(key);
2277-
return NULL;
2293+
return 0;
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+
if (result) {
2301+
*result = NULL;
2302+
}
2303+
return -1;
2304+
}
22832305
}
2284-
return _PyDict_Pop_KnownHash(dict, key, hash, deflt);
2306+
return _PyDict_Pop_KnownHash(dict, key, hash, result);
22852307
}
22862308

2309+
2310+
PyObject *
2311+
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *default_value)
2312+
{
2313+
PyObject *result;
2314+
if (PyDict_Pop(dict, key, &result) == 0) {
2315+
if (default_value != NULL) {
2316+
return Py_NewRef(default_value);
2317+
}
2318+
_PyErr_SetKeyError(key);
2319+
return NULL;
2320+
}
2321+
return result;
2322+
}
2323+
2324+
22872325
/* Internal version of dict.from_keys(). It is subclass-friendly. */
22882326
PyObject *
22892327
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)

Objects/odictobject.c

+4-1
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,10 @@ _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+
if (_PyDict_Pop_KnownHash((PyDictObject *)od, key, hash,
1053+
&value) == 0) {
1054+
value = Py_NewRef(failobj);
1055+
}
10531056
}
10541057
else if (value == NULL && !PyErr_Occurred()) {
10551058
/* Apply the fallback value, if necessary. */

0 commit comments

Comments
 (0)