Skip to content

Commit

Permalink
pythongh-111262: Add PyDict_Pop() function (python#112028)
Browse files Browse the repository at this point in the history
_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>
  • Loading branch information
3 people authored and aisk committed Feb 11, 2024
1 parent b7d52b8 commit 024183c
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 73 deletions.
27 changes: 27 additions & 0 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,33 @@ Dictionary Objects
.. versionadded:: 3.4
.. c:function:: int PyDict_Pop(PyObject *p, PyObject *key, PyObject **result)
Remove *key* from dictionary *p* and optionally return the removed value.
Do not raise :exc:`KeyError` if the key missing.
- If the key is present, set *\*result* to a new reference to the removed
value if *result* is not ``NULL``, and return ``1``.
- If the key is missing, set *\*result* to ``NULL`` if *result* is not
``NULL``, and return ``0``.
- On error, raise an exception and return ``-1``.
This is similar to :meth:`dict.pop`, but without the default value and
not raising :exc:`KeyError` if the key missing.
.. versionadded:: 3.13
.. c:function:: int PyDict_PopString(PyObject *p, const char *key, PyObject **result)
Similar to :c:func:`PyDict_Pop`, but *key* is specified as a
:c:expr:`const char*` UTF-8 encoded bytes string, rather than a
:c:expr:`PyObject*`.
.. versionadded:: 3.13
.. c:function:: PyObject* PyDict_Items(PyObject *p)
Return a :c:type:`PyListObject` containing all the items from the dictionary.
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,12 @@ New Features
Python ``list.extend()`` and ``list.clear()`` methods.
(Contributed by Victor Stinner in :gh:`111138`.)

* Add :c:func:`PyDict_Pop` and :c:func:`PyDict_PopString` functions: remove a
key from a dictionary and optionally return the removed value. This is
similar to :meth:`dict.pop`, but without the default value and not raising
:exc:`KeyError` if the key missing.
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)


Porting to Python 3.13
----------------------
Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/dictobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) {

PyAPI_FUNC(int) PyDict_ContainsString(PyObject *mp, const char *key);

PyAPI_FUNC(int) PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result);
PyAPI_FUNC(int) PyDict_PopString(PyObject *dict, const char *key, PyObject **result);
PyAPI_FUNC(PyObject *) _PyDict_Pop(PyObject *dict, PyObject *key, PyObject *default_value);

/* Dictionary watchers */
Expand Down
6 changes: 5 additions & 1 deletion Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ 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 **result);

#define DKIX_EMPTY (-1)
#define DKIX_DUMMY (-2) /* Used internally */
Expand Down
87 changes: 87 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,93 @@ 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
dict_pop_null = _testcapi.dict_pop_null

# key present, get removed value
mydict = {"key": "value", "key2": "value2"}
self.assertEqual(dict_pop(mydict, "key"), (1, "value"))
self.assertEqual(mydict, {"key2": "value2"})
self.assertEqual(dict_pop(mydict, "key2"), (1, "value2"))
self.assertEqual(mydict, {})

# key present, ignore removed value
mydict = {"key": "value", "key2": "value2"}
self.assertEqual(dict_pop_null(mydict, "key"), 1)
self.assertEqual(mydict, {"key2": "value2"})
self.assertEqual(dict_pop_null(mydict, "key2"), 1)
self.assertEqual(mydict, {})

# key missing, expect removed value; empty dict has a fast path
self.assertEqual(dict_pop({}, "key"), (0, NULL))
self.assertEqual(dict_pop({"a": 1}, "key"), (0, NULL))

# key missing, ignored removed value; empty dict has a fast path
self.assertEqual(dict_pop_null({}, "key"), 0)
self.assertEqual(dict_pop_null({"a": 1}, "key"), 0)

# dict error
not_dict = UserDict({1: 2})
self.assertRaises(SystemError, dict_pop, not_dict, "key")
self.assertRaises(SystemError, dict_pop_null, not_dict, "key")

# key error; don't hash key if dict is empty
not_hashable_key = ["list"]
self.assertEqual(dict_pop({}, not_hashable_key), (0, NULL))
with self.assertRaises(TypeError):
dict_pop({'key': 1}, not_hashable_key)
dict_pop({}, NULL) # key is not checked if dict is empty

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

def test_dict_popstring(self):
# Test PyDict_PopString()
dict_popstring = _testcapi.dict_popstring
dict_popstring_null = _testcapi.dict_popstring_null

# key present, get removed value
mydict = {"key": "value", "key2": "value2"}
self.assertEqual(dict_popstring(mydict, "key"), (1, "value"))
self.assertEqual(mydict, {"key2": "value2"})
self.assertEqual(dict_popstring(mydict, "key2"), (1, "value2"))
self.assertEqual(mydict, {})

# key present, ignore removed value
mydict = {"key": "value", "key2": "value2"}
self.assertEqual(dict_popstring_null(mydict, "key"), 1)
self.assertEqual(mydict, {"key2": "value2"})
self.assertEqual(dict_popstring_null(mydict, "key2"), 1)
self.assertEqual(mydict, {})

# key missing; empty dict has a fast path
self.assertEqual(dict_popstring({}, "key"), (0, NULL))
self.assertEqual(dict_popstring_null({}, "key"), 0)
self.assertEqual(dict_popstring({"a": 1}, "key"), (0, NULL))
self.assertEqual(dict_popstring_null({"a": 1}, "key"), 0)

# non-ASCII key
non_ascii = '\U0001f40d'
dct = {'\U0001f40d': 123}
self.assertEqual(dict_popstring(dct, '\U0001f40d'.encode()), (1, 123))
dct = {'\U0001f40d': 123}
self.assertEqual(dict_popstring_null(dct, '\U0001f40d'.encode()), 1)

# dict error
not_dict = UserDict({1: 2})
self.assertRaises(SystemError, dict_popstring, not_dict, "key")
self.assertRaises(SystemError, dict_popstring_null, not_dict, "key")

# key error
self.assertRaises(UnicodeDecodeError, dict_popstring, {1: 2}, INVALID_UTF8)
self.assertRaises(UnicodeDecodeError, dict_popstring_null, {1: 2}, INVALID_UTF8)

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


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add :c:func:`PyDict_Pop` and :c:func:`PyDict_PopString` functions: remove a key
from a dictionary and optionally return the removed value. This is similar to
:meth:`dict.pop`, but without the default value and not raising :exc:`KeyError`
if the key missing. Patch by Stefan Behnel and Victor Stinner.
28 changes: 15 additions & 13 deletions Modules/_functoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1087,19 +1087,9 @@ 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) {
int res = _PyDict_Pop_KnownHash((PyDictObject*)self->cache, link->key,
link->hash, &popresult);
if (res < 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,10 +1100,22 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
Py_DECREF(result);
return NULL;
}
if (res == 0) {
/* 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. */
assert(popresult == NULL);
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__)
from running while we're still adjusting the links. */
assert(popresult != NULL);
oldkey = link->key;
oldresult = link->result;

Expand Down
87 changes: 86 additions & 1 deletion Modules/_testcapi/dict.c
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,88 @@ dict_mergefromseq2(PyObject *self, PyObject *args)
}


static PyObject *
dict_pop(PyObject *self, PyObject *args)
{
// Test PyDict_Pop(dict, key, &value)
PyObject *dict, *key;
if (!PyArg_ParseTuple(args, "OO", &dict, &key)) {
return NULL;
}
NULLABLE(dict);
NULLABLE(key);
PyObject *result = UNINITIALIZED_PTR;
int res = PyDict_Pop(dict, key, &result);
if (res < 0) {
assert(result == NULL);
return NULL;
}
if (res == 0) {
assert(result == NULL);
result = Py_NewRef(Py_None);
}
else {
assert(result != NULL);
}
return Py_BuildValue("iN", res, result);
}


static PyObject *
dict_pop_null(PyObject *self, PyObject *args)
{
// Test PyDict_Pop(dict, key, NULL)
PyObject *dict, *key;
if (!PyArg_ParseTuple(args, "OO", &dict, &key)) {
return NULL;
}
NULLABLE(dict);
NULLABLE(key);
RETURN_INT(PyDict_Pop(dict, key, NULL));
}


static PyObject *
dict_popstring(PyObject *self, PyObject *args)
{
PyObject *dict;
const char *key;
Py_ssize_t key_size;
if (!PyArg_ParseTuple(args, "Oz#", &dict, &key, &key_size)) {
return NULL;
}
NULLABLE(dict);
PyObject *result = UNINITIALIZED_PTR;
int res = PyDict_PopString(dict, key, &result);
if (res < 0) {
assert(result == NULL);
return NULL;
}
if (res == 0) {
assert(result == NULL);
result = Py_NewRef(Py_None);
}
else {
assert(result != NULL);
}
return Py_BuildValue("iN", res, result);
}


static PyObject *
dict_popstring_null(PyObject *self, PyObject *args)
{
PyObject *dict;
const char *key;
Py_ssize_t key_size;
if (!PyArg_ParseTuple(args, "Oz#", &dict, &key, &key_size)) {
return NULL;
}
NULLABLE(dict);
RETURN_INT(PyDict_PopString(dict, key, NULL));
}


static PyMethodDef test_methods[] = {
{"dict_check", dict_check, METH_O},
{"dict_checkexact", dict_checkexact, METH_O},
Expand Down Expand Up @@ -358,7 +440,10 @@ 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},
{"dict_pop_null", dict_pop_null, METH_VARARGS},
{"dict_popstring", dict_popstring, METH_VARARGS},
{"dict_popstring_null", dict_popstring_null, METH_VARARGS},
{NULL},
};

Expand Down
7 changes: 2 additions & 5 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -967,11 +967,8 @@ 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);
}
else {
if (PyDict_Pop(tstate->dict, self->key, NULL) < 0) {
// Silently ignore error
PyErr_Clear();
}
}
Expand Down
10 changes: 2 additions & 8 deletions Modules/socketmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -392,16 +392,10 @@ remove_unusable_flags(PyObject *m)
break;
}
else {
PyObject *flag_name = PyUnicode_FromString(win_runtime_flags[i].flag_name);
if (flag_name == NULL) {
if (PyDict_PopString(dict, win_runtime_flags[i].flag_name,
NULL) < 0) {
return -1;
}
PyObject *v = _PyDict_Pop(dict, flag_name, Py_None);
Py_DECREF(flag_name);
if (v == NULL) {
return -1;
}
Py_DECREF(v);
}
}
return 0;
Expand Down

0 comments on commit 024183c

Please sign in to comment.