Skip to content

Commit

Permalink
pythongh-106320: Add PyDict_Pop() function
Browse files Browse the repository at this point in the history
Change the API of the internal _PyDict_Pop_KnownHash() function
to return an int.

Co-Authored-By: Stefan Behnel <stefan_ml@behnel.de>
  • Loading branch information
vstinner and scoder committed Nov 10, 2023
1 parent baeb771 commit 5bd1e51
Show file tree
Hide file tree
Showing 14 changed files with 165 additions and 34 deletions.
17 changes: 17 additions & 0 deletions Doc/c-api/dict.rst
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 :term:`strong reference` to the
removed value, and return ``1``.
- If the key is missing and *default_value* is not NULL, set *\*result*
to a :term:`strong 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
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
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
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
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.

@@ -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
Expand Up @@ -2481,3 +2481,5 @@
[function._Py_SetRefcnt]
added = '3.13'
abi_only = true
[function.PyDict_Pop]
added = '3.13'
4 changes: 2 additions & 2 deletions Modules/_functoolsmodule.c
Expand Up @@ -1087,8 +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);
(void)_PyDict_Pop_KnownHash((PyDictObject*)self->cache, link->key,
link->hash, Py_None, &popresult);
if (popresult == Py_None) {
/* Getting here means that the user function call or another
thread has already removed the old key from the dictionary.
Expand Down
21 changes: 20 additions & 1 deletion Modules/_testcapi/dict.c
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
88 changes: 59 additions & 29 deletions Objects/dictobject.c
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
2 changes: 1 addition & 1 deletion Objects/odictobject.c
Expand Up @@ -1049,7 +1049,7 @@ _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
1 change: 1 addition & 0 deletions PC/python3dll.c

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

0 comments on commit 5bd1e51

Please sign in to comment.