From aed66d92e164eba844a9ad3c6c0289c8a232cf5b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 14 Nov 2026 19:06:28 +0100 Subject: [PATCH 1/2] Add PyDict_AsFrozenDictAndClear() function --- docs/api.rst | 8 +++++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 36 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 19 +++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index ec0ad0e..cf86c17 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -23,6 +23,14 @@ Latest version of the header file: `pythoncapi_compat.h `_. +Python 3.16 +----------- + +.. c:function:: PyObject* PyDict_AsFrozenDictAndClear(PyObject *dict) + + See `PyDict_AsFrozenDictAndClear() documentation `__. + + Python 3.15 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index ca4c07f..87558a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2026-11-14: Add ``PyDict_AsFrozenDictAndClear()`` function. * 2026-11-04: Add ``Py_SETREF()`` and ``Py_XSETREF()`` to the limited C API older than Python 3.16. * 2026-10-12: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 0c6c1c2..ab0ad09 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -3154,6 +3154,42 @@ PyEval_GetFrameBuiltins(void) #endif +// Python 3.16 adds PyDict_AsFrozenDictAndClear() +#if (_Py_PACK_VERSION(3, 15) <= PY_VERSION_HEX \ + && PY_VERSION_HEX < _Py_PACK_VERSION(3, 16)) +static inline PyObject * +PyDict_AsFrozenDictAndClear(PyObject *dict) +{ + if (dict == NULL || !PyDict_Check(dict)) { + PyErr_BadInternalCall(); + return NULL; + } + +#ifndef Py_LIMITED_API + PyObject *res = PyFrozenDict_New(dict); +#else + // PyFrozenDict_New() is not part of the limited C API. + // Instead, get the built-in "frozendict" type and call frozendict(dict). + PyObject *builtins = PyEval_GetFrameBuiltins(); + assert(builtins != NULL); + PyObject *frozendict_type = PyMapping_GetItemString(builtins, "frozendict"); + Py_DECREF(builtins); + if (frozendict_type == NULL) { + return NULL; + } + + PyObject *res = PyObject_CallOneArg(frozendict_type, dict); + Py_DECREF(frozendict_type); +#endif + if (res == NULL) { + return NULL; + } + PyDict_Clear(dict); + return res; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 977430f..30c7690 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1155,6 +1155,9 @@ test_dict_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) PyObject *invalid_dict = NULL; PyObject *get_value = NULL; int res; +#if _Py_PACK_VERSION(3, 15) <= PY_VERSION_HEX + PyObject *frozendict = NULL; +#endif // test PyDict_New() dict = PyDict_New(); @@ -1245,6 +1248,22 @@ test_dict_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) PyErr_Clear(); assert(get_value == NULL); +#if _Py_PACK_VERSION(3, 15) <= PY_VERSION_HEX + assert(PyDict_Size(dict) == 1); + frozendict = PyDict_AsFrozenDictAndClear(dict); + assert(frozendict != NULL); +#ifndef Py_LIMITED_API + assert(PyFrozenDict_CheckExact(frozendict)); +#else + assert(Py_TYPE(frozendict) != &PyDict_Type); +#endif + assert(PyDict_GetItemRef(frozendict, key, &get_value) == 1); + assert(get_value == value); + Py_DECREF(get_value); + Py_DECREF(frozendict); + assert(PyDict_Size(dict) == 0); +#endif + Py_DECREF(dict); Py_DECREF(key); Py_DECREF(missing_key); From fc73e70f3dc40707f736c779d462a94611a3ef4f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 14 Nov 2026 19:31:04 +0100 Subject: [PATCH 2/2] Fix Python 3.16 support --- pythoncapi_compat.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index ab0ad09..a7a0b36 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -3154,9 +3154,10 @@ PyEval_GetFrameBuiltins(void) #endif -// Python 3.16 adds PyDict_AsFrozenDictAndClear() +// Python 3.16.0a2 adds PyDict_AsFrozenDictAndClear() #if (_Py_PACK_VERSION(3, 15) <= PY_VERSION_HEX \ - && PY_VERSION_HEX < _Py_PACK_VERSION(3, 16)) + && (PY_VERSION_HEX < _Py_PACK_FULL_VERSION(3, 16, 0, 0xA, 2) \ + || defined(Py_LIMITED_API))) static inline PyObject * PyDict_AsFrozenDictAndClear(PyObject *dict) {