Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-111262: Add PyDict_Pop() function [with default value] #111939

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ Dictionary Objects

.. versionadded:: 3.4


.. c:function:: int PyDict_Pop(PyObject *p, PyObject *key, PyObject *default_value, PyObject **result)

Remove *key* from dictionary *p* and return the removed value.

- If the key is present, set *\*result* to a new reference to the removed
value, and return ``1``.
- If the key is missing and *default_value* is not NULL, set *\*result*
to a new reference to *default_value*, and return ``0``.
- If the key is missing and *default_value* is NULL, raise :exc:`KeyError`,
and return -1.
- On error, raise an exception and return ``-1``.

This is the same as :meth:`dict.pop`.

.. versionadded:: 3.13

.. c:function:: PyObject* PyDict_Items(PyObject *p)

Return a :c:type:`PyListObject` containing all the items from the dictionary.
Expand Down
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,10 @@ New Features
:c:func:`PyErr_WriteUnraisable`, but allow to customize the warning mesage.
(Contributed by Serhiy Storchaka in :gh:`108082`.)

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


Porting to Python 3.13
----------------------
Expand Down
5 changes: 5 additions & 0 deletions Include/dictobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ PyAPI_FUNC(int) PyDict_DelItemString(PyObject *dp, const char *key);
// - On error, raise an exception and return -1.
PyAPI_FUNC(int) PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result);
PyAPI_FUNC(int) PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result);

PyAPI_FUNC(int) PyDict_Pop(PyObject *dict,
PyObject *key,
PyObject *default_value,
PyObject **result);
#endif

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000
Expand Down
7 changes: 6 additions & 1 deletion Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ extern PyObject *_PyDict_LoadGlobal(PyDictObject *, PyDictObject *, PyObject *);
extern int _PyDict_SetItem_Take2(PyDictObject *op, PyObject *key, PyObject *value);
extern int _PyObjectDict_SetItem(PyTypeObject *tp, PyObject **dictptr, PyObject *name, PyObject *value);

extern PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *);
extern int _PyDict_Pop_KnownHash(
PyDictObject *dict,
PyObject *key,
Py_hash_t hash,
PyObject *default_value,
PyObject **result);

#define DKIX_EMPTY (-1)
#define DKIX_DUMMY (-2) /* Used internally */
Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_capi/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,49 @@ def test_dict_mergefromseq2(self):
# CRASHES mergefromseq2({}, NULL, 0)
# CRASHES mergefromseq2(NULL, {}, 0)

def test_dict_pop(self):
# Test PyDict_Pop()
dict_pop = _testcapi.dict_pop
default = object()

def expect_value(mydict, key, expected_value):
self.assertEqual(dict_pop(mydict.copy(), key, default),
(1, expected_value))
self.assertEqual(dict_pop(mydict.copy(), key, NULL),
(1, expected_value))

def check_default(mydict, key):
result, value = dict_pop(mydict, key, default)
self.assertIs(value, default)
self.assertEqual(result, 0)

# key present
mydict = {"key": 2, "key2": 5}
expect_value(mydict, "key", 2)
expect_value(mydict, "key2", 5)

# key missing
check_default({}, "key") # empty dict has a fast path
check_default({"a": 1}, "key")
self.assertRaises(KeyError, dict_pop, {}, "key", NULL)
self.assertRaises(KeyError, dict_pop, {"a": 1}, "key", NULL)

# dict error
not_dict = "string"
self.assertRaises(SystemError, dict_pop, not_dict, "key", default)

# key error
not_hashable_key = ["list"]
check_default({}, not_hashable_key) # don't hash key if dict is empty
with self.assertRaises(TypeError):
dict_pop({'key': 1}, not_hashable_key, NULL)
with self.assertRaises(TypeError):
dict_pop({'key': 1}, not_hashable_key, default)
dict_pop({}, NULL, default) # don't check key if dict is empty

# CRASHES dict_pop(NULL, "key")
# CRASHES dict_pop({"a": 1}, NULL, default)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyDict_Pop` function: remove a key from a dictionary and return
the removed value. This is the same as :meth:`dict.pop`. Patch by Stefan
Behnel and Victor Stinner.
2 changes: 2 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2481,3 +2481,5 @@
[function._Py_SetRefcnt]
added = '3.13'
abi_only = true
[function.PyDict_Pop]
added = '3.13'
26 changes: 13 additions & 13 deletions Modules/_functoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1087,19 +1087,8 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
The cache dict holds one reference to the link.
We created one other reference when the link was created.
The linked list only has borrowed references. */
popresult = _PyDict_Pop_KnownHash(self->cache, link->key,
link->hash, Py_None);
if (popresult == Py_None) {
/* Getting here means that the user function call or another
thread has already removed the old key from the dictionary.
This link is now an orphan. Since we don't want to leave the
cache in an inconsistent state, we don't restore the link. */
Py_DECREF(popresult);
Py_DECREF(link);
Py_DECREF(key);
return result;
}
if (popresult == NULL) {
if (_PyDict_Pop_KnownHash((PyDictObject*)self->cache, link->key,
link->hash, Py_None, &popresult) < 0) {
/* An error arose while trying to remove the oldest key (the one
being evicted) from the cache. We restore the link to its
original position as the oldest link. Then we allow the
Expand All @@ -1110,6 +1099,17 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
Py_DECREF(result);
return NULL;
}

if (popresult == Py_None) {
/* Getting here means that the user function call or another
thread has already removed the old key from the dictionary.
This link is now an orphan. Since we don't want to leave the
cache in an inconsistent state, we don't restore the link. */
Py_DECREF(popresult);
Py_DECREF(link);
Py_DECREF(key);
return result;
}
/* Keep a reference to the old key and old result to prevent their
ref counts from going to zero during the update. That will
prevent potentially arbitrary object clean-up code (i.e. __del__)
Expand Down
21 changes: 20 additions & 1 deletion Modules/_testcapi/dict.c
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,25 @@ dict_mergefromseq2(PyObject *self, PyObject *args)
}


static PyObject *
dict_pop(PyObject *self, PyObject *args)
{
PyObject *dict, *key, *default_value;
if (!PyArg_ParseTuple(args, "OOO", &dict, &key, &default_value)) {
return NULL;
}
NULLABLE(dict);
NULLABLE(key);
NULLABLE(default_value);
PyObject *result = UNINITIALIZED_PTR;
int res = PyDict_Pop(dict, key, default_value, &result);
if (result == NULL) {
return NULL;
}
return Py_BuildValue("iN", res, result);
}


static PyMethodDef test_methods[] = {
{"dict_check", dict_check, METH_O},
{"dict_checkexact", dict_checkexact, METH_O},
Expand Down Expand Up @@ -358,7 +377,7 @@ static PyMethodDef test_methods[] = {
{"dict_merge", dict_merge, METH_VARARGS},
{"dict_update", dict_update, METH_VARARGS},
{"dict_mergefromseq2", dict_mergefromseq2, METH_VARARGS},

{"dict_pop", dict_pop, METH_VARARGS},
{NULL},
};

Expand Down
9 changes: 5 additions & 4 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -967,12 +967,13 @@ local_clear(localobject *self)
HEAD_UNLOCK(runtime);
while (tstate) {
if (tstate->dict) {
PyObject *v = _PyDict_Pop(tstate->dict, self->key, Py_None);
if (v != NULL) {
Py_DECREF(v);
PyObject *v;
if (PyDict_Pop(tstate->dict, self->key, Py_None, &v) < 0) {
// Silently ignore error
PyErr_Clear();
}
else {
PyErr_Clear();
Py_DECREF(v);
}
}
HEAD_LOCK(runtime);
Expand Down
7 changes: 4 additions & 3 deletions Modules/socketmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -396,11 +396,12 @@ remove_unusable_flags(PyObject *m)
if (flag_name == NULL) {
return -1;
}
PyObject *v = _PyDict_Pop(dict, flag_name, Py_None);
Py_DECREF(flag_name);
if (v == NULL) {
PyObject *v;
if (PyDict_Pop(dict, flag_name, Py_None, &v) < 0) {
Py_DECREF(flag_name);
return -1;
}
Py_DECREF(flag_name);
Py_DECREF(v);
}
}
Expand Down
92 changes: 62 additions & 30 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2226,64 +2226,94 @@ PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
return _PyDict_Next(op, ppos, pkey, pvalue, NULL);
}


/* Internal version of dict.pop(). */
PyObject *
_PyDict_Pop_KnownHash(PyObject *dict, PyObject *key, Py_hash_t hash, PyObject *deflt)
int
_PyDict_Pop_KnownHash(PyDictObject *mp, PyObject *key, Py_hash_t hash,
PyObject *default_value, PyObject **result)
{
Py_ssize_t ix;
PyObject *old_value;
PyDictObject *mp;
PyInterpreterState *interp = _PyInterpreterState_GET();

assert(PyDict_Check(dict));
mp = (PyDictObject *)dict;
assert(PyDict_Check(mp));

if (mp->ma_used == 0) {
if (deflt) {
return Py_NewRef(deflt);
if (default_value) {
*result = Py_NewRef(default_value);
return 0;
}
*result = NULL;
_PyErr_SetKeyError(key);
return NULL;
return -1;
}
ix = _Py_dict_lookup(mp, key, hash, &old_value);
if (ix == DKIX_ERROR)
return NULL;

PyObject *old_value;
Py_ssize_t ix = _Py_dict_lookup(mp, key, hash, &old_value);
if (ix == DKIX_ERROR) {
*result = NULL;
return -1;
}

if (ix == DKIX_EMPTY || old_value == NULL) {
if (deflt) {
return Py_NewRef(deflt);
if (default_value) {
*result = Py_NewRef(default_value);
return 0;
}
*result = NULL;
_PyErr_SetKeyError(key);
return NULL;
return -1;
}

assert(old_value != NULL);
PyInterpreterState *interp = _PyInterpreterState_GET();
uint64_t new_version = _PyDict_NotifyEvent(
interp, PyDict_EVENT_DELETED, mp, key, NULL);
delitem_common(mp, hash, ix, Py_NewRef(old_value), new_version);

ASSERT_CONSISTENT(mp);
return old_value;
*result = old_value;
return 1;
}

PyObject *
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)

int
PyDict_Pop(PyObject *op, PyObject *key, PyObject *default_value, PyObject **result)
{
Py_hash_t hash;
if (!PyDict_Check(op)) {
*result = NULL;
PyErr_BadInternalCall();
return -1;
}
PyDictObject *dict = (PyDictObject *)op;

if (((PyDictObject *)dict)->ma_used == 0) {
if (deflt) {
return Py_NewRef(deflt);
if (dict->ma_used == 0) {
if (default_value) {
*result = Py_NewRef(default_value);
return 0;
}
*result = NULL;
_PyErr_SetKeyError(key);
return NULL;
return -1;
}

Py_hash_t hash;
if (!PyUnicode_CheckExact(key) || (hash = unicode_get_hash(key)) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return NULL;
if (hash == -1) {
*result = NULL;
return -1;
}
}
return _PyDict_Pop_KnownHash(dict, key, hash, deflt);
return _PyDict_Pop_KnownHash(dict, key, hash, default_value, result);
}


PyObject *
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *default_value)
{
PyObject *result;
(void)PyDict_Pop(dict, key, default_value, &result);
return result;
}


/* Internal version of dict.from_keys(). It is subclass-friendly. */
PyObject *
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)
Expand Down Expand Up @@ -3463,7 +3493,9 @@ static PyObject *
dict_pop_impl(PyDictObject *self, PyObject *key, PyObject *default_value)
/*[clinic end generated code: output=3abb47b89f24c21c input=e221baa01044c44c]*/
{
return _PyDict_Pop((PyObject*)self, key, default_value);
PyObject *result;
PyDict_Pop((PyObject*)self, key, default_value, &result);
return result;
}

/*[clinic input]
Expand Down
3 changes: 2 additions & 1 deletion Objects/odictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,8 @@ _odict_popkey_hash(PyObject *od, PyObject *key, PyObject *failobj,
return NULL;
}
/* Now delete the value from the dict. */
value = _PyDict_Pop_KnownHash(od, key, hash, failobj);
(void)_PyDict_Pop_KnownHash((PyDictObject *)od, key, hash,
failobj, &value);
}
else if (value == NULL && !PyErr_Occurred()) {
/* Apply the fallback value, if necessary. */
Expand Down