From dab5f9834fff35c4c2d5d397feecd0278a7a6d75 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Oct 2023 02:46:57 +0200 Subject: [PATCH 01/87] Fix typo: _Py_IsFinalizing() not Py_IsFinalizing --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index dcd97ff..4a52c63 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -853,7 +853,7 @@ PyModule_Add(PyObject *mod, const char *name, PyObject *value) // gh-108014 added Py_IsFinalizing() to Python 3.13.0a1 // bpo-1856 added _Py_Finalizing to Python 3.2.1b1. -// Py_IsFinalizing() was added to PyPy 7.3.0. +// _Py_IsFinalizing() was added to PyPy 7.3.0. #if (0x030201B1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030D00A1) \ && (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x7030000) static inline int Py_IsFinalizing(void) From 99ab0d3767b2cfad3108e83cf04e40005ee73b6c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Oct 2023 12:00:37 +0200 Subject: [PATCH 02/87] Add PyUnicode_EqualToUTF8() function (#78) Add PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() functions. --- docs/api.rst | 8 ++++ docs/changelog.rst | 5 ++ pythoncapi_compat.h | 73 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 35 ++++++++++++++ 4 files changed, 121 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 9204d9a..3725b4e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -113,6 +113,14 @@ Python 3.13 Available on Python 3.5.2 and newer. +.. c:function:: int PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) + + See `PyUnicode_EqualToUTF8() documentation `__. + +.. c:function:: int PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t size) + + See `PyUnicode_EqualToUTF8AndSize() documentation `__. + Python 3.12 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 9fd3d06..4147a4a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +* 2023-10-04: Add functions: + + * ``PyUnicode_EqualToUTF8()`` + * ``PyUnicode_EqualToUTF8AndSize()`` + * 2023-10-03: Add functions: * ``PyObject_VisitManagedDict()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4a52c63..b423fbd 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -939,6 +939,79 @@ PyThreadState_GetUnchecked(void) } #endif +// gh-110289 added PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_len) +{ + Py_ssize_t len; + const void *utf8; + PyObject *exc_type, *exc_value, *exc_tb; + int res; + + // API cannot report errors so save/restore the exception + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + + // Python 3.3.0a1 added PyUnicode_AsUTF8AndSize() +#if PY_VERSION_HEX >= 0x030300A1 + if (PyUnicode_IS_ASCII(unicode)) { + utf8 = PyUnicode_DATA(unicode); + len = PyUnicode_GET_LENGTH(unicode); + } + else { + utf8 = PyUnicode_AsUTF8AndSize(unicode, &len); + if (utf8 == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + } + + if (len != str_len) { + res = 0; + goto done; + } + res = (memcmp(utf8, str, (size_t)len) == 0); +#else + PyObject *bytes = PyUnicode_AsUTF8String(unicode); + if (bytes == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + +#if PY_VERSION_HEX >= 0x03000000 + len = PyBytes_GET_SIZE(bytes); + utf8 = PyBytes_AS_STRING(bytes); +#else + len = PyString_GET_SIZE(bytes); + utf8 = PyString_AS_STRING(bytes); +#endif + if (len != str_len) { + Py_DECREF(bytes); + res = 0; + goto done; + } + + res = (memcmp(utf8, str, (size_t)len) == 0); + Py_DECREF(bytes); +#endif + +done: + PyErr_Restore(exc_type, exc_value, exc_tb); + return res; +} + +static inline int +PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) +{ + return PyUnicode_EqualToUTF8AndSize(unicode, str, (Py_ssize_t)strlen(str)); +} +#endif + #ifdef __cplusplus } diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 78ee1c8..8d8ef38 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1361,6 +1361,40 @@ test_managed_dict(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif // PY_VERSION_HEX >= 0x030B00A3 +static PyObject * +test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *abc = PyUnicode_FromString("abc"); + if (abc == NULL) { + return NULL; + } + + PyObject *abc0def = PyUnicode_FromStringAndSize("abc\0def", 7); + if (abc == NULL) { + return NULL; + } + + // PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() can be called + // with an exception raised and they must not clear the current exception. + PyErr_NoMemory(); + + assert(PyUnicode_EqualToUTF8AndSize(abc, "abc", 3) == 1); + assert(PyUnicode_EqualToUTF8AndSize(abc, "Python", 6) == 0); + assert(PyUnicode_EqualToUTF8AndSize(abc0def, "abc\0def", 7) == 1); + + assert(PyUnicode_EqualToUTF8(abc, "abc") == 1); + assert(PyUnicode_EqualToUTF8(abc, "Python") == 0); + assert(PyUnicode_EqualToUTF8(abc0def, "abc\0def") == 0); + + assert(PyErr_ExceptionMatches(PyExc_MemoryError)); + PyErr_Clear(); + + Py_DECREF(abc); + Py_DECREF(abc0def); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1390,6 +1424,7 @@ static struct PyMethodDef methods[] = { #ifdef TEST_MANAGED_DICT {"test_managed_dict", test_managed_dict, METH_NOARGS, _Py_NULL}, #endif + {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 85e4cd55dec60c94924830dad6541b0890fbdd57 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 16 Oct 2023 14:55:35 +0200 Subject: [PATCH 03/87] Test Python 3.13 (#79) * GHA: add Python 3.13. * runtests.py: add "python3.13". * GHA workflow: use Python 3.12 instead of 3.11 on Windows/macOS * ReadTheDocs: use Python 3.12 instead of 3.11. * Drop support for Python 3.11 beta versions (b1, b2, b3) in tests. --- .github/workflows/build.yml | 11 +++++++---- .readthedocs.yaml | 2 +- docs/api.rst | 2 +- runtests.py | 1 + tests/setup.py | 4 ---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab5a2bc..1c8005e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,10 @@ jobs: - "3.9" - "3.10" - "3.11" - # CPython 3.12 final is scheduled for October 2023: - # https://peps.python.org/pep-0693/ - "3.12" + # CPython 3.13 final is scheduled for October 2024: + # https://peps.python.org/pep-0719/ + - "3.13" # Python 2.7 was removed from GHA setup-python in June 2023: # https://github.com/actions/setup-python/issues/672 @@ -55,13 +56,15 @@ jobs: - os: windows-latest python: 3.6 - os: windows-latest - python: 3.11 + python: 3.12 # macOS: test old and new Python - os: macos-latest python: 3.6 - os: macos-latest - python: 3.11 + python: 3.12 + + # Ubuntu: test deadsnakes Python not supported by GHA python-versions - os: ubuntu-20.04 python: 3.5 - os: ubuntu-20.04 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index de85e78..735b67d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,7 +6,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" sphinx: configuration: docs/conf.py diff --git a/docs/api.rst b/docs/api.rst index 3725b4e..d4846db 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,7 +7,7 @@ functions for old Python versions. Supported Python versions: -* Python 3.5 - 3.11 +* Python 3.5 - 3.12 * PyPy 2.7 * PyPy 3.6 - 3.9 diff --git a/runtests.py b/runtests.py index 38507a1..28b2982 100755 --- a/runtests.py +++ b/runtests.py @@ -41,6 +41,7 @@ "python3.10", "python3.11", "python3.12", + "python3.13", "pypy", "pypy2", "pypy2.7", diff --git a/tests/setup.py b/tests/setup.py index 4deaa36..2e10c15 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -14,10 +14,6 @@ # C++ is only supported on Python 3.6 and newer TEST_CPP = (sys.version_info >= (3, 6)) -if 0x30b0000 <= sys.hexversion <= 0x30b00b3: - # Don't test C++ on Python 3.11b1 - 3.11b3: these versions have C++ - # compatibility issues. - TEST_CPP = False SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) From 1c1ab386d3c255cb7c4170c774ae50a20faf6e20 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 13 Nov 2023 20:32:09 +0100 Subject: [PATCH 04/87] Add PyList_Extend() (#80) --- docs/api.rst | 8 ++++++++ docs/changelog.rst | 5 +++++ pythoncapi_compat.h | 15 +++++++++++++++ tests/test_pythoncapi_compat_cext.c | 28 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index d4846db..a0b7ecf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -121,6 +121,14 @@ Python 3.13 See `PyUnicode_EqualToUTF8AndSize() documentation `__. +.. c:function:: int PyList_Extend(PyObject *list, PyObject *iterable) + + See `PyList_Extend() documentation `__. + +.. c:function:: int PyList_Clear(PyObject *list) + + See `PyList_Clear() documentation `__. + Python 3.12 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 4147a4a..25e14d2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +* 2023-11-13: Add functions: + + * ``PyList_Extend()`` + * ``PyList_Clear()`` + * 2023-10-04: Add functions: * ``PyUnicode_EqualToUTF8()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index b423fbd..6c67426 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1013,6 +1013,21 @@ PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) #endif +// gh-111138 added PyList_Extend() and PyList_Clear() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyList_Extend(PyObject *list, PyObject *iterable) +{ + return PyList_SetSlice(list, PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, iterable); +} + +static inline int +PyList_Clear(PyObject *list) +{ + return PyList_SetSlice(list, 0, PY_SSIZE_T_MAX, NULL); +} +#endif + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 8d8ef38..81cfe6b 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1395,6 +1395,33 @@ test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_list(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *list = PyList_New(0); + if (list == NULL) { + return NULL; + } + + PyObject *abc = PyUnicode_FromString("abc"); + if (abc == NULL) { + return NULL; + } + + // test PyList_Extend() + assert(PyList_Extend(list, abc) == 0); + Py_DECREF(abc); + assert(PyList_GET_SIZE(list) == 3); + + // test PyList_Clear() + assert(PyList_Clear(list) == 0); + assert(PyList_GET_SIZE(list) == 0); + + Py_DECREF(list); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1425,6 +1452,7 @@ static struct PyMethodDef methods[] = { {"test_managed_dict", test_managed_dict, METH_NOARGS, _Py_NULL}, #endif {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, + {"test_list", test_list, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 5bf2fb2f2d12217ce5a7c906703b249af6b4787d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Nov 2023 15:02:42 +0100 Subject: [PATCH 05/87] Add PyDict_Pop() function (#81) Add PyDict_Pop() and PyDict_PopString() functions. --- docs/api.rst | 8 +++ docs/changelog.rst | 5 ++ pythoncapi_compat.h | 61 +++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 77 +++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index a0b7ecf..c1ef118 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -129,6 +129,14 @@ Python 3.13 See `PyList_Clear() documentation `__. +.. c:function:: int PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result) + + See `PyDict_Pop() documentation `__. + +.. c:function:: int PyDict_PopString(PyObject *dict, const char *key, PyObject **result) + + See `PyDict_PopString() documentation `__. + Python 3.12 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 25e14d2..834b4f6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +* 2023-11-14: Add functions: + + * ``PyDict_Pop()`` + * ``PyDict_PopString()`` + * 2023-11-13: Add functions: * ``PyList_Extend()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 6c67426..4a3f733 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1028,6 +1028,67 @@ PyList_Clear(PyObject *list) } #endif +// gh-111262 added PyDict_Pop() and PyDict_PopString() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result) +{ + PyObject *value; + + if (!PyDict_Check(dict)) { + PyErr_BadInternalCall(); + if (result) { + *result = NULL; + } + return -1; + } + + // bpo-16991 added _PyDict_Pop() to Python 3.5.0b2. + // Python 3.6.0b3 changed _PyDict_Pop() first argument type to PyObject*. + // Python 3.13.0a1 removed _PyDict_Pop(). +#if defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030500b2 || PY_VERSION_HEX >= 0x030D0000 + value = PyObject_CallMethod(dict, "pop", "O", key); +#elif PY_VERSION_HEX < 0x030600b3 + value = _PyDict_Pop(_Py_CAST(PyDictObject*, dict), key, NULL); +#else + value = _PyDict_Pop(dict, key, NULL); +#endif + if (value == NULL) { + if (result) { + *result = NULL; + } + if (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; + } + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; +} + +static inline int +PyDict_PopString(PyObject *dict, const char *key, PyObject **result) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + if (result != NULL) { + *result = NULL; + } + return -1; + } + + int res = PyDict_Pop(dict, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 81cfe6b..a97f813 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1253,6 +1253,82 @@ test_dict_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_dict_pop(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + + PyObject *key = PyUnicode_FromString("key"); + assert(key != NULL); + PyObject *value = PyUnicode_FromString("abc"); + assert(value != NULL); + + // test PyDict_Pop(), get the removed value, key is present + assert(PyDict_SetItem(dict, key, value) == 0); + PyObject *removed = UNINITIALIZED_OBJ; + assert(PyDict_Pop(dict, key, &removed) == 1); + assert(removed == value); + Py_DECREF(removed); + + // test PyDict_Pop(), ignore the removed value, key is present + assert(PyDict_SetItem(dict, key, value) == 0); + assert(PyDict_Pop(dict, key, NULL) == 1); + + // test PyDict_Pop(), key is missing + removed = UNINITIALIZED_OBJ; + assert(PyDict_Pop(dict, key, &removed) == 0); + assert(removed == NULL); + assert(PyDict_Pop(dict, key, NULL) == 0); + + // test PyDict_PopString(), get the removed value, key is present + assert(PyDict_SetItem(dict, key, value) == 0); + removed = UNINITIALIZED_OBJ; + assert(PyDict_PopString(dict, "key", &removed) == 1); + assert(removed == value); + Py_DECREF(removed); + + // test PyDict_PopString(), ignore the removed value, key is present + assert(PyDict_SetItem(dict, key, value) == 0); + assert(PyDict_PopString(dict, "key", NULL) == 1); + + // test PyDict_PopString(), key is missing + removed = UNINITIALIZED_OBJ; + assert(PyDict_PopString(dict, "key", &removed) == 0); + assert(removed == NULL); + assert(PyDict_PopString(dict, "key", NULL) == 0); + + // dict error + removed = UNINITIALIZED_OBJ; + assert(PyDict_Pop(key, key, &removed) == -1); + assert(removed == NULL); + assert(PyErr_ExceptionMatches(PyExc_SystemError)); + PyErr_Clear(); + + assert(PyDict_Pop(key, key, NULL) == -1); + assert(PyErr_ExceptionMatches(PyExc_SystemError)); + PyErr_Clear(); + + removed = UNINITIALIZED_OBJ; + assert(PyDict_PopString(key, "key", &removed) == -1); + assert(removed == NULL); + assert(PyErr_ExceptionMatches(PyExc_SystemError)); + PyErr_Clear(); + + assert(PyDict_PopString(key, "key", NULL) == -1); + assert(PyErr_ExceptionMatches(PyExc_SystemError)); + PyErr_Clear(); + + // exit + Py_DECREF(dict); + Py_DECREF(key); + Py_DECREF(value); + Py_RETURN_NONE; +} + + static PyObject * test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -1447,6 +1523,7 @@ static struct PyMethodDef methods[] = { {"test_getattr", test_getattr, METH_NOARGS, _Py_NULL}, {"test_getitem", test_getitem, METH_NOARGS, _Py_NULL}, {"test_dict_api", test_dict_api, METH_NOARGS, _Py_NULL}, + {"test_dict_pop", test_dict_pop, METH_NOARGS, _Py_NULL}, {"test_long_api", test_long_api, METH_NOARGS, _Py_NULL}, #ifdef TEST_MANAGED_DICT {"test_managed_dict", test_managed_dict, METH_NOARGS, _Py_NULL}, From 481bee0b4a55564ef4578f0ad7c090ae94a7c64a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 18 Nov 2023 03:18:01 +0100 Subject: [PATCH 06/87] GHA: test Python 2.7 (#82) --- .github/workflows/build.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c8005e..b3964fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,3 +85,17 @@ jobs: run: python -VV - name: Run tests run: python runtests.py --current --verbose + + test_python27: + name: 'Test Python 2.7' + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install Python 2.7 + run: | + sudo apt-get update + sudo apt-get -yq install python2.7 python2.7-dev + - name: Display the Python version + run: python2.7 -VV + - name: Run tests + run: python2.7 runtests.py --current --verbose From 2ff444151ae7472e571f4514480ab9833fdfc867 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 1 Dec 2023 03:08:59 +0100 Subject: [PATCH 07/87] Update tests --- runtests.py | 5 +++-- tests/test_pythoncapi_compat_cext.c | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/runtests.py b/runtests.py index 28b2982..fdf1697 100755 --- a/runtests.py +++ b/runtests.py @@ -4,8 +4,9 @@ Usage:: - python3 test_matrix.py - python3 test_matrix.py -v # verbose mode + python3 runtests.py + python3 runtests.py --verbose + python3 runtests.py --current --verbose """ from __future__ import absolute_import from __future__ import print_function diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index a97f813..83969af 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1575,7 +1575,7 @@ static struct PyModuleDef module_def = { PyModuleDef_HEAD_INIT, MODULE_NAME_STR, // m_name _Py_NULL, // m_doc - 0, // m_doc + 0, // m_size methods, // m_methods #if PY_VERSION_HEX >= 0x03050000 module_slots, // m_slots From 4678af46f6015a3b38d84498f6d5e165c9df2479 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 15 Dec 2023 03:58:37 +0100 Subject: [PATCH 08/87] Add Py_HashPointer() (#83) --- docs/api.rst | 4 ++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 19 +++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 29 +++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index c1ef118..32db605 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -137,6 +137,10 @@ Python 3.13 See `PyDict_PopString() documentation `__. +.. c:function:: Py_hash_t Py_HashPointer(const void *ptr) + + See `Py_HashPointer() documentation `__. + Python 3.12 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 834b4f6..40fdece 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2023-12-15: Add function ``Py_HashPointer()``. * 2023-11-14: Add functions: * ``PyDict_Pop()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4a3f733..8b356b2 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1089,6 +1089,25 @@ PyDict_PopString(PyObject *dict, const char *key, PyObject **result) } #endif + +#if PY_VERSION_HEX < 0x030200A4 +// Python 3.2.0a4 added Py_hash_t type +typedef Py_ssize_t Py_hash_t; +#endif + + +// gh-111545 added Py_HashPointer() to Python 3.13.0a3 +#if PY_VERSION_HEX < 0x030D00A3 +static inline Py_hash_t Py_HashPointer(const void *ptr) +{ +#if PY_VERSION_HEX >= 0x030900A4 && !defined(PYPY_VERSION) + return _Py_HashPointer(ptr); +#else + return _Py_HashPointer(_Py_CAST(void*, ptr)); +#endif +} +#endif + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 83969af..b8d73a5 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1498,6 +1498,34 @@ test_list(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + void *ptr0 = NULL; + assert(Py_HashPointer(ptr0) == 0); + +#ifndef PYPY_VERSION +#if SIZEOF_VOID_P == 8 + void *ptr1 = (void*)(uintptr_t)0xABCDEF1234567890; + assert(Py_HashPointer(ptr1) == (uintptr_t)0x0ABCDEF123456789); +#else + void *ptr1 = (void*)(uintptr_t)0xDEADCAFE; + assert(Py_HashPointer(ptr1) == (uintptr_t)0xEDEADCAF); +#endif +#else + // PyPy +#if SIZEOF_VOID_P == 8 + void *ptr1 = (void*)(uintptr_t)0xABCDEF1234567890; +#else + void *ptr1 = (void*)(uintptr_t)0xDEADCAFE; +#endif + assert(Py_HashPointer(ptr1) == (Py_hash_t)ptr1); +#endif + + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1530,6 +1558,7 @@ static struct PyMethodDef methods[] = { #endif {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, {"test_list", test_list, METH_NOARGS, _Py_NULL}, + {"test_hash", test_hash, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From deb6f40255c05985dad09565566bc75c84f4addf Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 15 Dec 2023 04:45:48 +0100 Subject: [PATCH 09/87] API: document not supported functions --- docs/api.rst | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 32db605..45a9e18 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -141,6 +141,11 @@ Python 3.13 See `Py_HashPointer() documentation `__. +Not supported: + +* ``PySys_Audit()``. +* ``PyErr_FormatUnraisable()``. + Python 3.12 ----------- @@ -157,6 +162,20 @@ Python 3.12 Not available on PyPy. +Not supported: + +* ``PyDict_AddWatcher()``, ``PyDict_Watch()``. +* ``PyCode_AddWatcher()``, ``PyCode_ClearWatcher()``. +* ``PyErr_GetRaisedException()``, ``PyErr_SetRaisedException()``. +* ``_PyErr_ChainExceptions1()``. +* ``PyErr_DisplayException()``. +* ``_Py_IsImmortal()``. +* ``Py_NewInterpreterFromConfig()``. +* ``PyException_GetArgs()``, ``PyException_SetArgs()``. +* ``PyEval_SetProfileAllThreads()``, ``PyEval_SetTraceAllThreads()``. +* ``PyFunction_SetVectorcall()``. +* ``PyType_FromMetaclass()``: implementation too big to be backported. +* ``PyVectorcall_Call()``. Python 3.11 ----------- @@ -257,6 +276,15 @@ Python 3.11 Not available on PyPy +Not supported: + +* ``PyType_GetModuleByDef()``. +* ``PyType_GetName()``. +* ``PyType_GetQualName()``. +* ``Py_Version`` constant. +* ``PyErr_GetHandledException()``, ``PyErr_SetHandledException()``. +* ``PyFrame_GetGenerator()``. + Python 3.10 ----------- @@ -288,6 +316,17 @@ Python 3.10 See `PyModule_AddObjectRef() documentation `__. +Not supported: + +* ``PyCodec_Unregister()``. +* ``PyDateTime_DATE_GET_TZINFO()``, ``PyDateTime_TIME_GET_TZINFO()``. +* ``PyErr_SetInterruptEx()``. +* ``PyGC_Enable()``, ``PyGC_Disable()`` and ``PyGC_IsEnabled()``. +* ``PyIter_Send()``. +* ``PySet_CheckExact()``. +* ``Py_TPFLAGS_DISALLOW_INSTANTIATION`` constant. +* ``Py_TPFLAGS_IMMUTABLETYPE`` constant. + Python 3.9 ---------- @@ -330,6 +369,12 @@ PyObject See `PY_VECTORCALL_ARGUMENTS_OFFSET documentation `__. +Not supported: + +* ``PyVectorcall_CallMethod()``. +* ``PyType_FromModuleAndSpec()`` + + PyFrameObject ^^^^^^^^^^^^^ @@ -394,6 +439,35 @@ Module helper See `PyModule_AddType() documentation `__. +Python 3.8 +---------- + +Not supported: + +* ``PyCode_NewWithPosOnlyArgs()``. + +Python 3.7 +---------- + +Not supported: + +* ``PyImport_GetModule()``. +* ``PyInterpreterState_GetID()``. +* ``PySlice_Unpack()``, ``PySlice_AdjustIndices()``. +* ``PyTimeZone_FromOffset()``, ``PyTimeZone_FromOffsetAndName()``. +* ``Py_RETURN_RICHCOMPARE()``. +* ``Py_UNREACHABLE`` macro. + +Python 3.6 +---------- + +Not supported: + +* ``PyErr_ResourceWarning()``. +* ``PyErr_SetImportErrorSubclass()``. +* ``PyOS_FSPath()``. +* ``Py_FinalizeEx()``. + Python 3.5.2 ------------ @@ -401,6 +475,15 @@ Python 3.5.2 .. c:macro:: Py_XSETREF(op, op2) +Not supported: + +* ``PyCodec_NameReplaceErrors()``. +* ``PyErr_FormatV()``. +* ``PyExc_RecursionError``. +* ``PyModule_FromDefAndSpec()``, ``PyModule_FromDefAndSpec2()``, + and ``PyModule_ExecDef()``. +* ``PyNumber_MatrixMultiply()`` and ``PyNumber_InPlaceMatrixMultiply()``. + Python 3.4 ---------- @@ -408,6 +491,24 @@ Python 3.4 See `Py_UNUSED() documentation `__. +Python 3.2 +---------- + +Not supported: + +* ``Py_VA_COPY``. +* ``PySys_SetArgvEx()``. +* ``PyLong_AsLongLongAndOverflow()``. +* ``PyErr_NewExceptionWithDoc()``. + +Python 3.1 +---------- + +Not supported: + +* ``PyOS_string_to_double()``. +* ``PyCapsule`` API. + Borrow variant -------------- From 52486a9d964c3f9a91ca9c60f5eefabcc06e92e4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 20 Feb 2024 15:10:25 +0100 Subject: [PATCH 10/87] Add PyTime API (#84) --- docs/api.rst | 30 ++++++++++ docs/changelog.rst | 9 +++ pythoncapi_compat.h | 89 +++++++++++++++++++++++++++++ runtests.py | 3 + tests/test_pythoncapi_compat.py | 4 ++ tests/test_pythoncapi_compat_cext.c | 36 ++++++++++++ 6 files changed, 171 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 45a9e18..162b27b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -141,6 +141,36 @@ Python 3.13 See `Py_HashPointer() documentation `__. +.. c:type:: PyTime_t + + A timestamp or duration in nanoseconds, represented as a signed 64-bit + integer. + +.. c:var:: PyTime_t PyTime_MIN + + Minimum value of :c:type:`PyTime_t`. + +.. c:var:: PyTime_t PyTime_MAX + + Maximum value of :c:type:`PyTime_t`. + +.. c:function:: double PyTime_AsSecondsDouble(PyTime_t t) + + See `PyTime_AsSecondsDouble() documentation `__. + +.. c:function:: int PyTime_Monotonic(PyTime_t *result) + + See `PyTime_Monotonic() documentation `__. + +.. c:function:: int PyTime_Time(PyTime_t *result) + + See `PyTime_Time() documentation `__. + +.. c:function:: int PyTime_PerfCounter(PyTime_t *result) + + See `PyTime_PerfCounter() documentation `__. + + Not supported: * ``PySys_Audit()``. diff --git a/docs/changelog.rst b/docs/changelog.rst index 40fdece..76b3d82 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ Changelog ========= +* 2024-02-20: Add PyTime API: + + * ``PyTime_t`` type + * ``PyTime_MIN`` and ``PyTime_MAX`` constants + * ``PyTime_AsSecondsDouble()`` + * ``PyTime_Monotonic()`` + * ``PyTime_PerfCounter()`` + * ``PyTime_Time()`` + * 2023-12-15: Add function ``Py_HashPointer()``. * 2023-11-14: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 8b356b2..ebf4cbe 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1108,6 +1108,95 @@ static inline Py_hash_t Py_HashPointer(const void *ptr) } #endif + +// Python 3.13a4 added a PyTime API. +// Use the private API added to Python 3.5. +#if PY_VERSION_HEX < 0x030D00A4 && PY_VERSION_HEX >= 0x03050000 +typedef _PyTime_t PyTime_t; +#define PyTime_MIN _PyTime_MIN +#define PyTime_MAX _PyTime_MAX + +static inline double PyTime_AsSecondsDouble(PyTime_t t) +{ return _PyTime_AsSecondsDouble(t); } + +static inline int PyTime_Monotonic(PyTime_t *result) +{ return _PyTime_GetMonotonicClockWithInfo(result, NULL); } + +static inline int PyTime_Time(PyTime_t *result) +{ return _PyTime_GetSystemClockWithInfo(result, NULL); } + +static inline int PyTime_PerfCounter(PyTime_t *result) +{ +#if PY_VERSION_HEX >= 0x03070000 && !defined(PYPY_VERSION) + return _PyTime_GetPerfCounterWithInfo(result, NULL); +#elif PY_VERSION_HEX >= 0x03070000 + // Call time.perf_counter_ns() and convert Python int object to PyTime_t. + // Cache time.perf_counter_ns() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter_ns"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + long long value = PyLong_AsLongLong(res); + Py_DECREF(res); + + if (value == -1 && PyErr_Occurred()) { + return -1; + } + + Py_BUILD_ASSERT(sizeof(value) >= sizeof(PyTime_t)); + *result = (PyTime_t)value; + return 0; +#else + // Call time.perf_counter() and convert C double to PyTime_t. + // Cache time.perf_counter() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + double d = PyFloat_AsDouble(res); + Py_DECREF(res); + + if (d == -1.0 && PyErr_Occurred()) { + return -1; + } + + // Avoid floor() to avoid having to link to libm + *result = (PyTime_t)(d * 1e9); + return 0; +#endif +} + +#endif + + #ifdef __cplusplus } #endif diff --git a/runtests.py b/runtests.py index fdf1697..dfe0dd3 100755 --- a/runtests.py +++ b/runtests.py @@ -30,6 +30,7 @@ TEST_UPGRADE = os.path.join(TEST_DIR, "test_upgrade_pythoncapi.py") PYTHONS = ( + # CPython "python3-debug", "python3", "python2.7", @@ -43,6 +44,8 @@ "python3.11", "python3.12", "python3.13", + + # PyPy "pypy", "pypy2", "pypy2.7", diff --git a/tests/test_pythoncapi_compat.py b/tests/test_pythoncapi_compat.py index 948906c..17ade5a 100644 --- a/tests/test_pythoncapi_compat.py +++ b/tests/test_pythoncapi_compat.py @@ -188,6 +188,10 @@ def main(): global VERBOSE VERBOSE = ("-v" in sys.argv[1:] or "--verbose" in sys.argv[1:]) + if (3, 13) <= sys.version_info <= (3, 13, 0, 'alpha', 4): + print("SKIP Python 3.13 alpha 1..4: not supported!") + return + if faulthandler is not None: faulthandler.enable() diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index b8d73a5..94a2cfb 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1526,6 +1526,39 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +#if PY_VERSION_HEX >= 0x03050000 +#define TEST_PYTIME + +static PyObject * +test_time(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyTime_t t; +#define UNINITIALIZED_TIME ((PyTime_t)-483884113929936179) + + t = UNINITIALIZED_TIME; + assert(PyTime_Time(&t) == 0); + assert(t != UNINITIALIZED_TIME); + + t = UNINITIALIZED_TIME; + assert(PyTime_Monotonic(&t) == 0); + assert(t != UNINITIALIZED_TIME); + + // Test multiple times since an implementation uses a cache + for (int i=0; i < 5; i++) { + t = UNINITIALIZED_TIME; + assert(PyTime_PerfCounter(&t) == 0); + assert(t != UNINITIALIZED_TIME); + } + + assert(PyTime_AsSecondsDouble(1) == 1e-9); + assert(PyTime_AsSecondsDouble(1500 * 1000 * 1000) == 1.5); + assert(PyTime_AsSecondsDouble(-500 * 1000 * 1000) == -0.5); + + Py_RETURN_NONE; +} +#endif + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1559,6 +1592,9 @@ static struct PyMethodDef methods[] = { {"test_unicode", test_unicode, METH_NOARGS, _Py_NULL}, {"test_list", test_list, METH_NOARGS, _Py_NULL}, {"test_hash", test_hash, METH_NOARGS, _Py_NULL}, +#ifdef TEST_PYTIME + {"test_time", test_time, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 7539c7f82c0eda744ac455f0a15bf41432f2b02c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 9 Mar 2024 22:14:47 +0100 Subject: [PATCH 11/87] Add hash constants (#85) --- docs/changelog.rst | 7 +++++++ pythoncapi_compat.h | 12 ++++++++++++ tests/test_pythoncapi_compat_cext.c | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 76b3d82..2569314 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog ========= +* 2024-03-09: Add hash constants: + + * ``PyHASH_BITS`` + * ``PyHASH_IMAG`` + * ``PyHASH_INF`` + * ``PyHASH_MODULUS`` + * 2024-02-20: Add PyTime API: * ``PyTime_t`` type diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index ebf4cbe..43ba0dd 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1196,6 +1196,18 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #endif +// gh-111389 added hash constants to Python 3.13.0a5. These constants were +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9. +#if (!defined(PyHASH_BITS) \ + && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ + || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ + && PYPY_VERSION_NUM >= 0x07090000))) +# define PyHASH_BITS _PyHASH_BITS +# define PyHASH_MODULUS _PyHASH_MODULUS +# define PyHASH_INF _PyHASH_INF +# define PyHASH_IMAG _PyHASH_IMAG +#endif + #ifdef __cplusplus } diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 94a2cfb..4e9fb83 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1522,6 +1522,20 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(Py_HashPointer(ptr1) == (Py_hash_t)ptr1); #endif +#if ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ + || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ + && PYPY_VERSION_NUM >= 0x07090000)) + // Just check that constants are available + size_t bits = PyHASH_BITS; + assert(bits >= 8); + size_t mod = PyHASH_MODULUS; + assert(mod >= 7); + size_t inf = PyHASH_INF; + assert(inf != 0); + size_t imag = PyHASH_IMAG; + assert(imag != 0); +#endif + Py_RETURN_NONE; } From 18d42e1c2d7bd590215a6ed4da6846dd08afe983 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 17 Mar 2024 15:51:17 +0100 Subject: [PATCH 12/87] Update _Py_NULL for C23 --- pythoncapi_compat.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 43ba0dd..7164b46 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -30,14 +30,14 @@ extern "C" { # define _Py_CAST(type, expr) ((type)(expr)) #endif -// On C++11 and newer, _Py_NULL is defined as nullptr on C++11, -// otherwise it is defined as NULL. -#ifndef _Py_NULL -# if defined(__cplusplus) && __cplusplus >= 201103 -# define _Py_NULL nullptr -# else -# define _Py_NULL NULL -# endif +// Static inline functions should use _Py_NULL rather than using directly NULL +// to prevent C++ compiler warnings. On C23 and newer and on C++11 and newer, +// _Py_NULL is defined as nullptr. +#if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ + || (defined(__cplusplus) && __cplusplus >= 201103) +# define _Py_NULL nullptr +#else +# define _Py_NULL NULL #endif // Cast argument to PyObject* type. From 7000d0ed622843a5f6a62948ae51b14674f21479 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 17 Mar 2024 15:53:43 +0100 Subject: [PATCH 13/87] Update GitHub Actions --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3964fa..64c4856 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,10 +72,10 @@ jobs: steps: # https://github.com/actions/checkout - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python # https://github.com/actions/setup-python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -90,7 +90,7 @@ jobs: name: 'Test Python 2.7' runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Python 2.7 run: | sudo apt-get update From b16ff9aeefddac76e6965dba3dee65d3fcc39981 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 21 Mar 2024 17:20:06 +0100 Subject: [PATCH 14/87] Add Py_GetConstant() and Py_GetConstantBorrowed() (#87) --- docs/api.rst | 9 +++ docs/changelog.rst | 5 ++ pythoncapi_compat.h | 77 ++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 86 +++++++++++++++++++++++++++++ 4 files changed, 177 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 162b27b..598c1cb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -192,6 +192,15 @@ Python 3.12 Not available on PyPy. +.. c:function:: PyObject* Py_GetConstant(unsigned int constant_id) + + See `Py_GetConstant() documentation `__. + +.. c:function:: PyObject* Py_GetConstantBorrowed(unsigned int constant_id) + + See `Py_GetConstantBorrowed() documentation `__. + + Not supported: * ``PyDict_AddWatcher()``, ``PyDict_Watch()``. diff --git a/docs/changelog.rst b/docs/changelog.rst index 2569314..f869e10 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +* 2024-03-21: Add functions: + + * ``Py_GetConstant()`` + * ``Py_GetConstantBorrowed()`` + * 2024-03-09: Add hash constants: * ``PyHASH_BITS`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 7164b46..6cd3394 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1209,6 +1209,83 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #endif +// gh-111545 added Py_GetConstant() and Py_GetConstantBorrowed() +// to Python 3.13.0a6 +#if PY_VERSION_HEX < 0x030D00A6 + +#define Py_CONSTANT_NONE 0 +#define Py_CONSTANT_FALSE 1 +#define Py_CONSTANT_TRUE 2 +#define Py_CONSTANT_ELLIPSIS 3 +#define Py_CONSTANT_NOT_IMPLEMENTED 4 +#define Py_CONSTANT_ZERO 5 +#define Py_CONSTANT_ONE 6 +#define Py_CONSTANT_EMPTY_STR 7 +#define Py_CONSTANT_EMPTY_BYTES 8 +#define Py_CONSTANT_EMPTY_TUPLE 9 + +static inline PyObject* Py_GetConstant(unsigned int constant_id) +{ + static PyObject* constants[Py_CONSTANT_EMPTY_TUPLE + 1] = {NULL}; + + if (constants[Py_CONSTANT_NONE] == NULL) { + constants[Py_CONSTANT_NONE] = Py_None; + constants[Py_CONSTANT_FALSE] = Py_False; + constants[Py_CONSTANT_TRUE] = Py_True; + constants[Py_CONSTANT_ELLIPSIS] = Py_Ellipsis; + constants[Py_CONSTANT_NOT_IMPLEMENTED] = Py_NotImplemented; + + constants[Py_CONSTANT_ZERO] = PyLong_FromLong(0); + if (constants[Py_CONSTANT_ZERO] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_ONE] = PyLong_FromLong(1); + if (constants[Py_CONSTANT_ONE] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_STR] = PyUnicode_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_STR] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_BYTES] = PyBytes_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_BYTES] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_TUPLE] = PyTuple_New(0); + if (constants[Py_CONSTANT_EMPTY_TUPLE] == NULL) { + goto fatal_error; + } + // goto dance to avoid compiler warnings about Py_FatalError() + goto init_done; + +fatal_error: + // This case should never happen + Py_FatalError("Py_GetConstant() failed to get constants"); + } + +init_done: + if (constant_id <= Py_CONSTANT_EMPTY_TUPLE) { + return Py_NewRef(constants[constant_id]); + } + else { + PyErr_BadInternalCall(); + return NULL; + } +} + +static inline PyObject* Py_GetConstantBorrowed(unsigned int constant_id) +{ + PyObject *obj = Py_GetConstant(constant_id); + Py_XDECREF(obj); + return obj; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 4e9fb83..d906f94 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1573,6 +1573,91 @@ test_time(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif +static void +check_get_constant(PyObject* (*get_constant)(unsigned int), int borrowed) +{ +#define CLEAR(var) if (!borrowed) { Py_DECREF(var); } + + PyObject *obj, *expected; + + // Py_CONSTANT_NONE + obj = get_constant(Py_CONSTANT_NONE); + assert(obj == Py_None); + CLEAR(obj); + + // Py_CONSTANT_FALSE + obj = get_constant(Py_CONSTANT_FALSE); + assert(obj = Py_False); + CLEAR(obj); + + // Py_CONSTANT_TRUE + obj = get_constant(Py_CONSTANT_TRUE); + assert(obj == Py_True); + CLEAR(obj); + + // Py_CONSTANT_ELLIPSIS + obj = get_constant(Py_CONSTANT_ELLIPSIS); + assert(obj == Py_Ellipsis); + CLEAR(obj); + + // Py_CONSTANT_NOT_IMPLEMENTED + obj = get_constant(Py_CONSTANT_NOT_IMPLEMENTED); + assert(obj == Py_NotImplemented); + CLEAR(obj); + + // Py_CONSTANT_ZERO + obj = get_constant(Py_CONSTANT_ZERO); + expected = PyLong_FromLong(0); + assert(expected != NULL); + assert(Py_TYPE(obj) == &PyLong_Type); + assert(PyObject_RichCompareBool(obj, expected, Py_EQ) == 1); + CLEAR(obj); + Py_DECREF(expected); + + // Py_CONSTANT_ONE + obj = get_constant(Py_CONSTANT_ONE); + expected = PyLong_FromLong(1); + assert(expected != NULL); + assert(Py_TYPE(obj) == &PyLong_Type); + assert(PyObject_RichCompareBool(obj, expected, Py_EQ) == 1); + CLEAR(obj); + Py_DECREF(expected); + + // Py_CONSTANT_EMPTY_STR + obj = get_constant(Py_CONSTANT_EMPTY_STR); + assert(Py_TYPE(obj) == &PyUnicode_Type); +#if PY_VERSION_HEX >= 0x03030000 + assert(PyUnicode_GetLength(obj) == 0); +#else + assert(PyUnicode_GetSize(obj) == 0); +#endif + CLEAR(obj); + + // Py_CONSTANT_EMPTY_BYTES + obj = get_constant(Py_CONSTANT_EMPTY_BYTES); + assert(Py_TYPE(obj) == &PyBytes_Type); + assert(PyBytes_Size(obj) == 0); + CLEAR(obj); + + // Py_CONSTANT_EMPTY_TUPLE + obj = get_constant(Py_CONSTANT_EMPTY_TUPLE); + assert(Py_TYPE(obj) == &PyTuple_Type); + assert(PyTuple_Size(obj) == 0); + CLEAR(obj); + +#undef CLEAR +} + + +static PyObject * +test_get_constant(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + check_get_constant(Py_GetConstant, 0); + check_get_constant(Py_GetConstantBorrowed, 1); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1609,6 +1694,7 @@ static struct PyMethodDef methods[] = { #ifdef TEST_PYTIME {"test_time", test_time, METH_NOARGS, _Py_NULL}, #endif + {"test_get_constant", test_get_constant, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From d16872a44be1c363be0b6aecacb134ae759d89ef Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 25 Mar 2024 17:40:36 +0100 Subject: [PATCH 15/87] Fix test_unicode() --- tests/test_pythoncapi_compat_cext.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index d906f94..4c2c31c 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1446,7 +1446,8 @@ test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } PyObject *abc0def = PyUnicode_FromStringAndSize("abc\0def", 7); - if (abc == NULL) { + if (abc0def == NULL) { + Py_DECREF(abc); return NULL; } From f66799130acd8843802185553dadf0e300c5fe05 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 29 Mar 2024 11:21:30 +0100 Subject: [PATCH 16/87] gh-88: Add PyList_GetItemRef() --- docs/api.rst | 4 ++++ pythoncapi_compat.h | 12 ++++++++++++ tests/test_pythoncapi_compat_cext.c | 23 ++++++++++++++++------- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 598c1cb..2d8083f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -170,6 +170,10 @@ Python 3.13 See `PyTime_PerfCounter() documentation `__. +.. c:function:: PyObject* PyList_GetItemRef(PyObject *op, Py_ssize_t index) + + See `PyList_GetItemRef() documentation `__. + Not supported: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 6cd3394..5c3f5b1 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1286,6 +1286,18 @@ static inline PyObject* Py_GetConstantBorrowed(unsigned int constant_id) #endif +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline PyObject * +PyList_GetItemRef(PyObject *op, Py_ssize_t index) +{ + PyObject *item = PyList_GetItem(op, index); + Py_XINCREF(item); + return item; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 4c2c31c..c0bd831 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1480,15 +1480,24 @@ test_list(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) return NULL; } - PyObject *abc = PyUnicode_FromString("abc"); - if (abc == NULL) { - return NULL; + // test PyList_Extend() + { + PyObject *abc = PyUnicode_FromString("abc"); + if (abc == NULL) { + Py_DECREF(list); + return NULL; + } + + assert(PyList_Extend(list, abc) == 0); + Py_DECREF(abc); + assert(PyList_GET_SIZE(list) == 3); } - // test PyList_Extend() - assert(PyList_Extend(list, abc) == 0); - Py_DECREF(abc); - assert(PyList_GET_SIZE(list) == 3); + // test PyList_GetItemRef() + PyObject *item = PyList_GetItemRef(list, 1); + assert(item != NULL); + assert(item == PyList_GetItem(list, 1)); + Py_DECREF(item); // test PyList_Clear() assert(PyList_Clear(list) == 0); From bbf462cb923e36534f537445ce5147979264bbc9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 2 Apr 2024 11:14:55 +0200 Subject: [PATCH 17/87] Add PyDict_SetDefaultRef() --- pythoncapi_compat.h | 41 +++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 57 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 5c3f5b1..4dba13a 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1298,6 +1298,47 @@ PyList_GetItemRef(PyObject *op, Py_ssize_t index) #endif +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static int +PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, + PyObject **result) +{ + PyObject *value; + if (PyDict_GetItemRef(d, key, &value) < 0) { + // get error + if (result) { + *result = NULL; + } + return -1; + } + if (value != NULL) { + // present + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; + } + + // missing: set the item + if (PyDict_SetItem(d, key, default_value) < 0) { + // set error + if (result) { + *result = NULL; + } + return -1; + } + if (result) { + *result = Py_NewRef(default_value); + } + return 0; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c0bd831..bf26fa6 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1329,6 +1329,62 @@ test_dict_pop(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_dict_setdefault(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + PyObject *key = PyUnicode_FromString("key"); + assert(key != NULL); + PyObject *value = PyUnicode_FromString("abc"); + assert(value != NULL); + PyObject *invalid_key = PyList_New(0); // not hashable key + assert(invalid_key != NULL); + + // insert item + PyObject *result = UNINITIALIZED_OBJ; + assert(PyDict_SetDefaultRef(dict, key, value, &result) == 0); + assert(result == value); + Py_DECREF(result); + + // item already present + result = UNINITIALIZED_OBJ; + assert(PyDict_SetDefaultRef(dict, key, value, &result) == 1); + assert(result == value); + Py_DECREF(result); + + // error: invalid key + assert(!PyErr_Occurred()); + result = UNINITIALIZED_OBJ; + assert(PyDict_SetDefaultRef(dict, invalid_key, value, &result) == -1); + assert(result == NULL); + assert(PyErr_Occurred()); + PyErr_Clear(); + + // insert item with NULL result + assert(PyDict_Pop(dict, key, NULL) == 1); + assert(PyDict_SetDefaultRef(dict, key, value, NULL) == 0); + + // item already present with NULL result + assert(PyDict_SetDefaultRef(dict, key, value, NULL) == 1); + + // error: invalid key with NULL result + assert(!PyErr_Occurred()); + assert(PyDict_SetDefaultRef(dict, invalid_key, value, NULL) == -1); + assert(PyErr_Occurred()); + PyErr_Clear(); + + // exit + Py_DECREF(dict); + Py_DECREF(key); + Py_DECREF(value); + Py_DECREF(invalid_key); + Py_RETURN_NONE; +} + + static PyObject * test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -1694,6 +1750,7 @@ static struct PyMethodDef methods[] = { {"test_getitem", test_getitem, METH_NOARGS, _Py_NULL}, {"test_dict_api", test_dict_api, METH_NOARGS, _Py_NULL}, {"test_dict_pop", test_dict_pop, METH_NOARGS, _Py_NULL}, + {"test_dict_setdefault", test_dict_setdefault, METH_NOARGS, _Py_NULL}, {"test_long_api", test_long_api, METH_NOARGS, _Py_NULL}, #ifdef TEST_MANAGED_DICT {"test_managed_dict", test_managed_dict, METH_NOARGS, _Py_NULL}, From bbfc7a6fc62a97e7bfa56a427c6f9742861ed2df Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 2 Apr 2024 11:29:04 +0200 Subject: [PATCH 18/87] update doc --- docs/api.rst | 4 ++++ docs/changelog.rst | 1 + 2 files changed, 5 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 2d8083f..a6554e1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -174,6 +174,10 @@ Python 3.13 See `PyList_GetItemRef() documentation `__. +.. c:function:: int PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, PyObject **result) + + See `PyDict_SetDefaultRef() documentation `__. + Not supported: diff --git a/docs/changelog.rst b/docs/changelog.rst index f869e10..a530807 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2024-04-02: Add ``PyDict_SetDefaultRef()`` function. * 2024-03-21: Add functions: * ``Py_GetConstant()`` From b857ae7873d379601ad8970ebab383e039d5eb39 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 3 Apr 2024 20:36:20 +0200 Subject: [PATCH 19/87] update changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a530807..ff2010a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,7 @@ Changelog ========= * 2024-04-02: Add ``PyDict_SetDefaultRef()`` function. +* 2024-03-29: Add ``PyList_GetItemRef()`` function. * 2024-03-21: Add functions: * ``Py_GetConstant()`` From 68aad45c0d61e4a7fdaf6c9b6c6b41d2ac8bcdd0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 4 Apr 2024 23:30:21 +0200 Subject: [PATCH 20/87] gh-89: Fix Py_GetConstant() for Python 3.13.0a5+ --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4dba13a..7dd3ca2 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1211,7 +1211,7 @@ static inline int PyTime_PerfCounter(PyTime_t *result) // gh-111545 added Py_GetConstant() and Py_GetConstantBorrowed() // to Python 3.13.0a6 -#if PY_VERSION_HEX < 0x030D00A6 +#if PY_VERSION_HEX < 0x030D00A6 && !defined(Py_CONSTANT_NONE) #define Py_CONSTANT_NONE 0 #define Py_CONSTANT_FALSE 1 From 9bfd8de33c763d026fd2568ea302bd76b1046fba Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 4 Apr 2024 23:48:30 +0200 Subject: [PATCH 21/87] Update supported Python versions --- docs/api.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a6554e1..ef671e5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,18 +7,17 @@ functions for old Python versions. Supported Python versions: -* Python 3.5 - 3.12 -* PyPy 2.7 -* PyPy 3.6 - 3.9 +* Python 3.5 - 3.13 +* PyPy 2.7 and PyPy 3.6 - 3.10 Python 2.7 and Python 3.4 are no longer officially supported since GitHub Actions doesn't support them anymore: only best effort support is provided. C++03 and C++11 are supported on Python 3.6 and newer. -A C99 subset is required, like ``static inline`` functions: see `PEP 7 -`_. ISO C90 is partially supported -for Python 2.7. +A C11 subset (without optional features) is required, like ``static inline`` +functions: see `PEP 7 `_. ISO C90 +is partially supported for Python 2.7. Some functions related to frame objects and ``PyThreadState`` are not available on PyPy. From 01341acbbef0ca85cf2fa31b63307ddf4d9a87fb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 5 Apr 2024 02:26:59 +0200 Subject: [PATCH 22/87] Fix PyDict_SetDefaultRef() definition (#92) --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 7dd3ca2..16935df 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1300,7 +1300,7 @@ PyList_GetItemRef(PyObject *op, Py_ssize_t index) // gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 #if PY_VERSION_HEX < 0x030D00A4 -static int +static inline int PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, PyObject **result) { From ccea8848dca341f433a258c670ac5b71bb0d6f4d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 8 Apr 2024 22:11:31 +0200 Subject: [PATCH 23/87] users: +numpy (#93) --- docs/users.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/users.rst b/docs/users.rst index b817996..7271605 100644 --- a/docs/users.rst +++ b/docs/users.rst @@ -26,6 +26,8 @@ Examples of projects using pythoncapi_compat.h * `mypy `_ (mypyc, `commit `__) +* numpy: `pythoncapi-compat Git submodule + `_ * `pybluez `_ (`commit `__) * `python-snappy `_ From 2353a3d7fd0695c2ebe5fa56edec8b874156a6a2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 10 Apr 2024 11:53:15 +0200 Subject: [PATCH 24/87] Documentation (#94) --- docs/api.rst | 10 ++++++++-- docs/users.rst | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ef671e5..411c602 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -180,9 +180,15 @@ Python 3.13 Not supported: -* ``PySys_Audit()``. * ``PyErr_FormatUnraisable()``. - +* ``PyLong_AsNativeBytes()`` +* ``PyLong_FromNativeBytes()`` +* ``PyLong_FromUnsignedNativeBytes()`` +* ``PyObject_GenericHash()``. +* ``PySys_Audit()``. +* ``PySys_AuditTuple()``. +* ``PyType_GetFullyQualifiedName()`` +* ``PyType_GetModuleName()`` Python 3.12 ----------- diff --git a/docs/users.rst b/docs/users.rst index 7271605..b90466f 100644 --- a/docs/users.rst +++ b/docs/users.rst @@ -26,7 +26,8 @@ Examples of projects using pythoncapi_compat.h * `mypy `_ (mypyc, `commit `__) -* numpy: `pythoncapi-compat Git submodule +* `numpy `_: + `pythoncapi-compat Git submodule `_ * `pybluez `_ (`commit `__) From ffba5381192e410d6348f06284c613628f62ea53 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 23 May 2024 00:53:03 +0200 Subject: [PATCH 25/87] Drop Python 3.5 support: cannot be tested anymore (#97) --- .github/workflows/build.yml | 14 ++++++++------ docs/api.rst | 2 +- docs/changelog.rst | 1 + runtests.py | 2 -- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64c4856..8b1e0bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,9 +29,8 @@ jobs: - "3.12" # CPython 3.13 final is scheduled for October 2024: # https://peps.python.org/pep-0719/ - - "3.13" - # Python 2.7 was removed from GHA setup-python in June 2023: - # https://github.com/actions/setup-python/issues/672 + # TODO: Reenable 3.13 once fixed. + #- "3.13" # PyPy versions: # - https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md @@ -64,9 +63,8 @@ jobs: - os: macos-latest python: 3.12 - # Ubuntu: test deadsnakes Python not supported by GHA python-versions - - os: ubuntu-20.04 - python: 3.5 + # Ubuntu: test deadsnakes Python versions which are not supported by + # GHA python-versions. - os: ubuntu-20.04 python: 3.6 @@ -87,6 +85,10 @@ jobs: run: python runtests.py --current --verbose test_python27: + # Get Python 2.7 from Ubuntu 22.04. + # + # Python 2.7 was removed from GHA setup-python in June 2023: + # https://github.com/actions/setup-python/issues/672 name: 'Test Python 2.7' runs-on: ubuntu-22.04 steps: diff --git a/docs/api.rst b/docs/api.rst index 411c602..26fcd31 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,7 +7,7 @@ functions for old Python versions. Supported Python versions: -* Python 3.5 - 3.13 +* Python 3.6 - 3.14 * PyPy 2.7 and PyPy 3.6 - 3.10 Python 2.7 and Python 3.4 are no longer officially supported since GitHub diff --git a/docs/changelog.rst b/docs/changelog.rst index ff2010a..336df90 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2024-04-23: Drop Python 3.5 support. It cannot be tested anymore (pip fails). * 2024-04-02: Add ``PyDict_SetDefaultRef()`` function. * 2024-03-29: Add ``PyList_GetItemRef()`` function. * 2024-03-21: Add functions: diff --git a/runtests.py b/runtests.py index dfe0dd3..5064550 100755 --- a/runtests.py +++ b/runtests.py @@ -34,8 +34,6 @@ "python3-debug", "python3", "python2.7", - "python3.4", - "python3.5", "python3.6", "python3.7", "python3.8", From 9d14fad6c773b424f533d196c7e07441215e04d8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 23 May 2024 00:56:56 +0200 Subject: [PATCH 26/87] Fix test_frame() on Python 3.13 beta 1 (#98) --- .github/workflows/build.yml | 3 +-- tests/test_pythoncapi_compat_cext.c | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b1e0bc..2d863fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,8 +29,7 @@ jobs: - "3.12" # CPython 3.13 final is scheduled for October 2024: # https://peps.python.org/pep-0719/ - # TODO: Reenable 3.13 once fixed. - #- "3.13" + - "3.13" # PyPy versions: # - https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index bf26fa6..4fc043c 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -255,7 +255,10 @@ test_frame(PyObject *Py_UNUSED(module), PyObject* Py_UNUSED(ignored)) // test PyFrame_GetLocals() PyObject *locals = PyFrame_GetLocals(frame); assert(locals != _Py_NULL); + // Python 3.13 creates a local proxy +#if PY_VERSION_HEX < 0x030D0000 assert(PyDict_Check(locals)); +#endif // test PyFrame_GetGlobals() PyObject *globals = PyFrame_GetGlobals(frame); From 4c2e17d00fd953b42fb4f60204466c153d0d4605 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 14:21:38 +0200 Subject: [PATCH 27/87] GHA: don't test old Python on macOS (#100) --- .github/workflows/build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d863fe..6f43aa5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,9 +56,7 @@ jobs: - os: windows-latest python: 3.12 - # macOS: test old and new Python - - os: macos-latest - python: 3.6 + # macOS: test only new Python - os: macos-latest python: 3.12 From 18d1df75c72a4ac38fc0a934c13ab6de081a19d2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 3 Jun 2024 14:23:30 +0200 Subject: [PATCH 28/87] Add PyLong_GetSign() function (#99) --- docs/api.rst | 7 +++++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 15 +++++++++++++++ tests/test_pythoncapi_compat_cext.c | 6 ++++++ 4 files changed, 29 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 26fcd31..a75053e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -26,6 +26,13 @@ Latest version of the header file: `pythoncapi_compat.h `_. +Python 3.14 +----------- + +.. c:function:: int PyLong_GetSign(PyObject *obj, int *sign) + + See `PyLong_GetSign() documentation `__. + Python 3.13 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 336df90..4d0fb9d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2024-06-03: Add ``PyLong_GetSign()``. * 2024-04-23: Drop Python 3.5 support. It cannot be tested anymore (pip fails). * 2024-04-02: Add ``PyDict_SetDefaultRef()`` function. * 2024-03-29: Add ``PyList_GetItemRef()`` function. diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 16935df..1b59f93 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1339,6 +1339,21 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, #endif +// gh-116560 added PyLong_GetSign() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A1 +static inline int PyLong_GetSign(PyObject *obj, int *sign) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + + *sign = _PyLong_Sign(obj); + return 0; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 4fc043c..aa7b206 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -53,6 +53,7 @@ // Marker to check that pointer value was set static const char uninitialized[] = "uninitialized"; #define UNINITIALIZED_OBJ ((PyObject *)uninitialized) +#define UNINITIALIZED_INT 0x83ff979 static PyObject* @@ -1413,6 +1414,11 @@ test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) PyErr_Clear(); Py_DECREF(obj2); + // test PyLong_GetSign() + int sign = UNINITIALIZED_INT; + assert(PyLong_GetSign(obj, &sign) == 0); + assert(sign == 1); + Py_RETURN_NONE; } From ea1f7f6eac63ff401937515638252402ff33dccb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 8 Jul 2024 06:34:09 -0600 Subject: [PATCH 29/87] PyLong_GetSign will be added in 3.140a0 (#102) --- pythoncapi_compat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 1b59f93..51e8c0d 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1339,8 +1339,8 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, #endif -// gh-116560 added PyLong_GetSign() to Python 3.14a4 -#if PY_VERSION_HEX < 0x030E00A1 +// gh-116560 added PyLong_GetSign() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 static inline int PyLong_GetSign(PyObject *obj, int *sign) { if (!PyLong_Check(obj)) { From 4094c64bb0c6ae0ceb91b8f0cbf3ad28421f2503 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 18 Jul 2024 22:59:13 +0200 Subject: [PATCH 30/87] Add PyUnicodeWriter API (#95) --- docs/changelog.rst | 13 +++ pythoncapi_compat.h | 153 ++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 146 ++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d0fb9d..1e7ba2a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,19 @@ Changelog ========= +* 2024-07-18: Add functions: + + * ``PyUnicodeWriter_Create()`` + * ``PyUnicodeWriter_Discard()`` + * ``PyUnicodeWriter_Finish()`` + * ``PyUnicodeWriter_WriteChar()`` + * ``PyUnicodeWriter_WriteUTF8()`` + * ``PyUnicodeWriter_WriteStr()`` + * ``PyUnicodeWriter_WriteRepr()`` + * ``PyUnicodeWriter_WriteSubstring()`` + * ``PyUnicodeWriter_WriteWideChar()`` + * ``PyUnicodeWriter_Format()`` + * 2024-06-03: Add ``PyLong_GetSign()``. * 2024-04-23: Drop Python 3.5 support. It cannot be tested anymore (pip fails). * 2024-04-02: Add ``PyDict_SetDefaultRef()`` function. diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 51e8c0d..d45828f 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1338,6 +1338,159 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #endif +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 // gh-116560 added PyLong_GetSign() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index aa7b206..f813548 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1733,6 +1733,147 @@ test_get_constant(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +#define TEST_UNICODEWRITER 1 + +static PyObject * +test_unicodewriter(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) +{ + PyUnicodeWriter *writer = PyUnicodeWriter_Create(0); + if (writer == NULL) { + return NULL; + } + int ret; + + // test PyUnicodeWriter_WriteStr() + PyObject *str = PyUnicode_FromString("var"); + if (str == NULL) { + goto error; + } + ret = PyUnicodeWriter_WriteStr(writer, str); + Py_CLEAR(str); + if (ret < 0) { + goto error; + } + + // test PyUnicodeWriter_WriteChar() + if (PyUnicodeWriter_WriteChar(writer, '=') < 0) { + goto error; + } + + // test PyUnicodeWriter_WriteSubstring() + str = PyUnicode_FromString("[long]"); + if (str == NULL) { + goto error; + } + ret = PyUnicodeWriter_WriteSubstring(writer, str, 1, 5); + Py_CLEAR(str); + if (ret < 0) { + goto error; + } + + // test PyUnicodeWriter_WriteUTF8() + if (PyUnicodeWriter_WriteUTF8(writer, " valu\xC3\xA9", -1) < 0) { + goto error; + } + if (PyUnicodeWriter_WriteChar(writer, ' ') < 0) { + goto error; + } + + // test PyUnicodeWriter_WriteRepr() + str = PyUnicode_FromString("repr"); + if (str == NULL) { + goto error; + } + if (PyUnicodeWriter_WriteRepr(writer, str) < 0) { + goto error; + } + Py_CLEAR(str); + + { + PyObject *result = PyUnicodeWriter_Finish(writer); + if (result == NULL) { + return NULL; + } + assert(PyUnicode_EqualToUTF8(result, "var=long valu\xC3\xA9 'repr'")); + Py_DECREF(result); + } + + Py_RETURN_NONE; + +error: + PyUnicodeWriter_Discard(writer); + return NULL; +} + + +static PyObject * +test_unicodewriter_widechar(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) +{ + PyUnicodeWriter *writer = PyUnicodeWriter_Create(0); + if (writer == NULL) { + return NULL; + } + + // test PyUnicodeWriter_WriteWideChar() + int ret = PyUnicodeWriter_WriteWideChar(writer, L"euro=\u20AC", -1); + if (ret < 0) { + goto error; + } + + { + PyObject *result = PyUnicodeWriter_Finish(writer); + if (result == NULL) { + return NULL; + } + assert(PyUnicode_EqualToUTF8(result, "euro=\xe2\x82\xac")); + Py_DECREF(result); + } + + Py_RETURN_NONE; + +error: + PyUnicodeWriter_Discard(writer); + return NULL; +} + + +static PyObject * +test_unicodewriter_format(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) +{ + PyUnicodeWriter *writer = PyUnicodeWriter_Create(0); + if (writer == NULL) { + return NULL; + } + + // test PyUnicodeWriter_Format() + if (PyUnicodeWriter_Format(writer, "%s %i", "Hello", 123) < 0) { + goto error; + } + + // test PyUnicodeWriter_WriteChar() + if (PyUnicodeWriter_WriteChar(writer, '.') < 0) { + goto error; + } + + { + PyObject *result = PyUnicodeWriter_Finish(writer); + if (result == NULL) { + return NULL; + } + assert(PyUnicode_EqualToUTF8(result, "Hello 123.")); + Py_DECREF(result); + } + + Py_RETURN_NONE; + +error: + PyUnicodeWriter_Discard(writer); + return NULL; +} +#endif + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1771,6 +1912,11 @@ static struct PyMethodDef methods[] = { {"test_time", test_time, METH_NOARGS, _Py_NULL}, #endif {"test_get_constant", test_get_constant, METH_NOARGS, _Py_NULL}, +#ifdef TEST_UNICODEWRITER + {"test_unicodewriter", test_unicodewriter, METH_NOARGS, _Py_NULL}, + {"test_unicodewriter_widechar", test_unicodewriter_widechar, METH_NOARGS, _Py_NULL}, + {"test_unicodewriter_format", test_unicodewriter_format, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 39e2663e6acc0b68d5dd75bdaad0af33152552ae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 6 Aug 2024 15:29:31 +0200 Subject: [PATCH 31/87] Add static inline to PyUnicodeWriter_WriteStr (#104) Add static inline to PyUnicodeWriter_WriteStr() PyUnicodeWriter_WriteRepr(). --- pythoncapi_compat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index d45828f..4ff5c74 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1392,7 +1392,7 @@ PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); } -int +static inline int PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) { PyObject *str = PyObject_Str(obj); @@ -1405,7 +1405,7 @@ PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) return res; } -int +static inline int PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) { PyObject *str = PyObject_Repr(obj); From 2d18aecd7b2f549d38a13e27b682ea4966f37bd8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 Aug 2024 03:10:10 -0600 Subject: [PATCH 32/87] Add critical section API (#106) --- pythoncapi_compat.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4ff5c74..c0feaa2 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1338,6 +1338,13 @@ PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, } #endif +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + #if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) typedef struct PyUnicodeWriter PyUnicodeWriter; From d20d7f815bb5504c806d4eb1fd43192f074fe268 Mon Sep 17 00:00:00 2001 From: Peter Hawkins Date: Mon, 23 Sep 2024 12:09:13 -0400 Subject: [PATCH 33/87] Fix incorrect use of assignment in place of an equality test. (#108) --- tests/test_pythoncapi_compat_cext.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index f813548..5cf0f45 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1662,7 +1662,7 @@ check_get_constant(PyObject* (*get_constant)(unsigned int), int borrowed) // Py_CONSTANT_FALSE obj = get_constant(Py_CONSTANT_FALSE); - assert(obj = Py_False); + assert(obj == Py_False); CLEAR(obj); // Py_CONSTANT_TRUE From bb0934e4f6cf5d2c5cae85ebc4784e906551a777 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 8 Oct 2024 16:36:28 +0200 Subject: [PATCH 34/87] Document PyUnicodeWriter API (#109) --- docs/api.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index a75053e..9063cc5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -33,6 +33,51 @@ Python 3.14 See `PyLong_GetSign() documentation `__. +.. c:function:: PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) + + See `PyUnicodeWriter_Create() documentation `__. + +.. c:function:: PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) + + See `PyUnicodeWriter_Finish() documentation `__. + +.. c:function:: void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) + + See `PyUnicodeWriter_Discard() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) + + See `PyUnicodeWriter_WriteChar() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, const char *str, Py_ssize_t size) + + See `PyUnicodeWriter_WriteUTF8() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, const wchar_t *str, Py_ssize_t size) + + See `PyUnicodeWriter_WriteWideChar() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) + + See `PyUnicodeWriter_WriteStr() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) + + See `PyUnicodeWriter_WriteRepr() documentation `__. + +.. c:function:: int PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, Py_ssize_t start, Py_ssize_t end) + + See `PyUnicodeWriter_WriteSubstring() documentation `__. + +.. c:function:: int PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) + + See `PyUnicodeWriter_Format() documentation `__. + +Not supported: + +* ``PyUnicodeWriter_DecodeUTF8Stateful()`` + + Python 3.13 ----------- From abc0f29fb9b245efa3d12ba1e6b35c104f1784f1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 9 Oct 2024 10:47:33 +0200 Subject: [PATCH 35/87] Add PyUnicode_Equal() function (#110) --- docs/api.rst | 4 ++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 32 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 7 +++++++ 4 files changed, 44 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 9063cc5..72d78e9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -33,6 +33,10 @@ Python 3.14 See `PyLong_GetSign() documentation `__. +.. c:function:: int PyUnicode_Equal(PyObject *str1, PyObject *str2) + + See `PyUnicode_Equal() documentation `__. + .. c:function:: PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) See `PyUnicodeWriter_Create() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e7ba2a..5416a3d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2024-10-09: Add ``PyUnicode_Equal()`` function. * 2024-07-18: Add functions: * ``PyUnicodeWriter_Create()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index c0feaa2..014d4cc 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1514,6 +1514,38 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) #endif +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, + "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, + "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + extern int _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 5cf0f45..a2b07ed 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1531,6 +1531,13 @@ test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyErr_ExceptionMatches(PyExc_MemoryError)); PyErr_Clear(); + // Test PyUnicode_Equal() + assert(PyUnicode_Equal(abc, abc) == 1); + assert(PyUnicode_Equal(abc, abc0def) == 0); + assert(PyUnicode_Equal(abc, Py_True) == -1); + assert(PyErr_ExceptionMatches(PyExc_TypeError)); + PyErr_Clear(); + Py_DECREF(abc); Py_DECREF(abc0def); Py_RETURN_NONE; From fba497767a2773426b52f75b979cf1de76041e7e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 9 Oct 2024 11:14:25 +0200 Subject: [PATCH 36/87] Add PyBytes_Join() function (#111) --- docs/api.rst | 4 ++++ docs/changelog.rst | 6 +++++- pythoncapi_compat.h | 9 +++++++++ tests/test_pythoncapi_compat_cext.c | 31 +++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 72d78e9..d817bc3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -33,6 +33,10 @@ Python 3.14 See `PyLong_GetSign() documentation `__. +.. c:function:: PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) + + See `PyBytes_Join() documentation `__. + .. c:function:: int PyUnicode_Equal(PyObject *str1, PyObject *str2) See `PyUnicode_Equal() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 5416a3d..18121ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,11 @@ Changelog ========= -* 2024-10-09: Add ``PyUnicode_Equal()`` function. +* 2024-10-09: Add functions: + + * ``PyBytes_Join()`` + * ``PyUnicode_Equal()`` + * 2024-07-18: Add functions: * ``PyUnicodeWriter_Create()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 014d4cc..02df115 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1546,6 +1546,15 @@ static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) #endif +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index a2b07ed..e5f7d2f 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1881,6 +1881,36 @@ test_unicodewriter_format(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) #endif +static PyObject * +test_bytes(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *abc = PyBytes_FromString("a b c"); + if (abc == NULL) { + return NULL; + } + PyObject *list = PyObject_CallMethod(abc, "split", NULL); + Py_DECREF(abc); + if (list == NULL) { + return NULL; + } + PyObject *sep = PyBytes_FromString("-"); + if (sep == NULL) { + Py_DECREF(list); + return NULL; + } + + PyObject *join = PyBytes_Join(sep, list); + assert(join != NULL); + assert(PyBytes_Check(join)); + assert(memcmp(PyBytes_AS_STRING(join), "a-b-c", 5) == 0); + Py_DECREF(join); + + Py_DECREF(list); + Py_DECREF(sep); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1924,6 +1954,7 @@ static struct PyMethodDef methods[] = { {"test_unicodewriter_widechar", test_unicodewriter_widechar, METH_NOARGS, _Py_NULL}, {"test_unicodewriter_format", test_unicodewriter_format, METH_NOARGS, _Py_NULL}, #endif + {"test_bytes", test_bytes, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 3f1c06d12110a9f7ab9f83224282f0cb63fca429 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 9 Oct 2024 11:40:37 +0200 Subject: [PATCH 37/87] Add Py_HashBuffer() function (#112) --- docs/api.rst | 33 ++++++++++++++++++++++++++++- docs/changelog.rst | 1 + pythoncapi_compat.h | 21 ++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 15 +++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index d817bc3..d79eb87 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,7 +10,7 @@ Supported Python versions: * Python 3.6 - 3.14 * PyPy 2.7 and PyPy 3.6 - 3.10 -Python 2.7 and Python 3.4 are no longer officially supported since GitHub +Python 2.7 and Python 3.5 are no longer officially supported since GitHub Actions doesn't support them anymore: only best effort support is provided. C++03 and C++11 are supported on Python 3.6 and newer. @@ -37,6 +37,10 @@ Python 3.14 See `PyBytes_Join() documentation `__. +.. c:function:: Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) + + See `Py_HashBuffer() documentation `__. + .. c:function:: int PyUnicode_Equal(PyObject *str1, PyObject *str2) See `PyUnicode_Equal() documentation `__. @@ -83,7 +87,34 @@ Python 3.14 Not supported: +* ``PyConfig_Get()`` +* ``PyConfig_GetInt()`` +* ``PyConfig_Names()`` +* ``PyConfig_Set()`` +* ``PyInitConfig_AddModule()`` +* ``PyInitConfig_Create()`` +* ``PyInitConfig_Free()`` +* ``PyInitConfig_FreeStrList()`` +* ``PyInitConfig_GetError()`` +* ``PyInitConfig_GetExitCode()`` +* ``PyInitConfig_GetInt()`` +* ``PyInitConfig_GetStr()`` +* ``PyInitConfig_GetStrList()`` +* ``PyInitConfig_HasOption()`` +* ``PyInitConfig_SetInt()`` +* ``PyInitConfig_SetStr()`` +* ``PyInitConfig_SetStrList()`` +* ``PyLong_AsInt32()`` +* ``PyLong_AsInt64()`` +* ``PyLong_AsUInt32()`` +* ``PyLong_AsUInt64()`` +* ``PyLong_FromInt32()`` +* ``PyLong_FromInt64()`` +* ``PyLong_FromUInt32()`` +* ``PyLong_FromUInt64()`` +* ``PyType_GetBaseByToken()`` * ``PyUnicodeWriter_DecodeUTF8Stateful()`` +* ``Py_InitializeFromInitConfig()`` Python 3.13 diff --git a/docs/changelog.rst b/docs/changelog.rst index 18121ab..157a08b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog * ``PyBytes_Join()`` * ``PyUnicode_Equal()`` + * ``Py_HashBuffer()`` * 2024-07-18: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 02df115..dfd65a2 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1555,6 +1555,27 @@ static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) #endif +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + extern Py_hash_t _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index e5f7d2f..2d8db5c 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1618,6 +1618,20 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(imag != 0); #endif + // Test Py_HashBuffer() + { + PyObject *abc = PyBytes_FromString("abc"); + if (abc == NULL) { + return NULL; + } + Py_hash_t hash = Py_HashBuffer(PyBytes_AS_STRING(abc), + PyBytes_GET_SIZE(abc)); + Py_hash_t hash2 = PyObject_Hash(abc); + assert(hash == hash2); + + Py_DECREF(abc); + } + Py_RETURN_NONE; } @@ -1884,6 +1898,7 @@ test_unicodewriter_format(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) static PyObject * test_bytes(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { + // Test PyBytes_Join() PyObject *abc = PyBytes_FromString("a b c"); if (abc == NULL) { return NULL; From 38e2d327a31bc2a599190528ba83341d4e048cf0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 9 Oct 2024 12:00:16 +0200 Subject: [PATCH 38/87] Add PyIter_NextItem() function (#113) --- docs/api.rst | 4 +++ docs/changelog.rst | 1 + pythoncapi_compat.h | 37 ++++++++++++++++++++--- tests/test_pythoncapi_compat_cext.c | 46 +++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d79eb87..ceb232f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -33,6 +33,10 @@ Python 3.14 See `PyLong_GetSign() documentation `__. +.. c:function:: PyObject* PyIter_NextItem(PyObject *sep, PyObject *iterable) + + See `PyIter_NextItem() documentation `__. + .. c:function:: PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) See `PyBytes_Join() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 157a08b..856e5a9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog * 2024-10-09: Add functions: * ``PyBytes_Join()`` + * ``PyIter_NextItem()`` * ``PyUnicode_Equal()`` * ``Py_HashBuffer()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index dfd65a2..db1e8d2 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1519,14 +1519,12 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) { if (!PyUnicode_Check(str1)) { - PyErr_Format(PyExc_TypeError, - "first argument must be str, not %s", + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", Py_TYPE(str1)->tp_name); return -1; } if (!PyUnicode_Check(str2)) { - PyErr_Format(PyExc_TypeError, - "second argument must be str, not %s", + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", Py_TYPE(str2)->tp_name); return -1; } @@ -1576,6 +1574,37 @@ static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) #endif +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 2d8db5c..6ef1873 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1437,12 +1437,14 @@ static int heapmanaged_traverse(PyObject *self, visitproc visit, void *arg) { Py_VISIT(Py_TYPE(self)); + // Test PyObject_VisitManagedDict() return PyObject_VisitManagedDict(self, visit, arg); } static int heapmanaged_clear(PyObject *self) { + // Test PyObject_ClearManagedDict() PyObject_ClearManagedDict(self); return 0; } @@ -1475,6 +1477,7 @@ static PyType_Spec HeapCTypeWithManagedDict_spec = { static PyObject * test_managed_dict(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { + // Test PyObject_VisitManagedDict() and PyObject_ClearManagedDict() PyObject *type = PyType_FromSpec(&HeapCTypeWithManagedDict_spec); if (type == NULL) { return NULL; @@ -1926,6 +1929,48 @@ test_bytes(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_iter(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + // Test PyIter_NextItem() + PyObject *tuple = Py_BuildValue("(i)", 123); + if (tuple == NULL) { + return NULL; + } + PyObject *iter = PyObject_GetIter(tuple); + Py_DECREF(tuple); + if (iter == NULL) { + return NULL; + } + + // first item + PyObject *item = UNINITIALIZED_OBJ; + assert(PyIter_NextItem(iter, &item) == 1); + { + PyObject *expected = PyLong_FromLong(123); + assert(PyObject_RichCompareBool(item, expected, Py_EQ) == 1); + assert(expected != NULL); + Py_DECREF(expected); + } + + // StopIteration + item = UNINITIALIZED_OBJ; + assert(PyIter_NextItem(iter, &item) == 0); + assert(item == NULL); + assert(!PyErr_Occurred()); + + // non-iterable object + item = UNINITIALIZED_OBJ; + assert(PyIter_NextItem(Py_None, &item) == -1); + assert(item == NULL); + assert(PyErr_ExceptionMatches(PyExc_TypeError)); + PyErr_Clear(); + + Py_DECREF(iter); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -1970,6 +2015,7 @@ static struct PyMethodDef methods[] = { {"test_unicodewriter_format", test_unicodewriter_format, METH_NOARGS, _Py_NULL}, #endif {"test_bytes", test_bytes, METH_NOARGS, _Py_NULL}, + {"test_iter", test_iter, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 669c882624bcde615710747103dd912ccedc9ae5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 9 Oct 2024 12:41:02 +0200 Subject: [PATCH 39/87] Add PyLong_FromUInt64() and PyLong_AsUInt64() (#114) --- docs/api.rst | 41 +++++++++++--- docs/changelog.rst | 8 +++ pythoncapi_compat.h | 85 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 42 ++++++++++++++ 4 files changed, 168 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ceb232f..b94df42 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -89,6 +89,39 @@ Python 3.14 See `PyUnicodeWriter_Format() documentation `__. +.. c:function:: int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) + + See `PyLong_AsInt32() documentation `__. + +.. c:function:: int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) + + See `PyLong_AsInt64() documentation `__. + +.. c:function:: int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) + + See `PyLong_AsUInt32() documentation `__. + +.. c:function:: int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) + + See `PyLong_AsUInt64() documentation `__. + +.. c:function:: PyObject* PyLong_FromInt32(int32_t value) + + See `PyLong_FromInt32() documentation `__. + +.. c:function:: PyObject* PyLong_FromInt64(int64_t value) + + See `PyLong_FromInt64() documentation `__. + +.. c:function:: PyObject* PyLong_FromUInt32(uint32_t value) + + See `PyLong_FromUInt32() documentation `__. + +.. c:function:: PyObject* PyLong_FromUInt64(uint64_t value) + + See `PyLong_FromUInt64() documentation `__. + + Not supported: * ``PyConfig_Get()`` @@ -108,14 +141,6 @@ Not supported: * ``PyInitConfig_SetInt()`` * ``PyInitConfig_SetStr()`` * ``PyInitConfig_SetStrList()`` -* ``PyLong_AsInt32()`` -* ``PyLong_AsInt64()`` -* ``PyLong_AsUInt32()`` -* ``PyLong_AsUInt64()`` -* ``PyLong_FromInt32()`` -* ``PyLong_FromInt64()`` -* ``PyLong_FromUInt32()`` -* ``PyLong_FromUInt64()`` * ``PyType_GetBaseByToken()`` * ``PyUnicodeWriter_DecodeUTF8Stateful()`` * ``Py_InitializeFromInitConfig()`` diff --git a/docs/changelog.rst b/docs/changelog.rst index 856e5a9..a2d188c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,14 @@ Changelog * ``PyBytes_Join()`` * ``PyIter_NextItem()`` + * ``PyLong_AsInt32()`` + * ``PyLong_AsInt64()`` + * ``PyLong_AsUInt32()`` + * ``PyLong_AsUInt64()`` + * ``PyLong_FromInt32()`` + * ``PyLong_FromInt64()`` + * ``PyLong_FromUInt32()`` + * ``PyLong_FromUInt64()`` * ``PyUnicode_Equal()`` * ``Py_HashBuffer()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index db1e8d2..34a84c9 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -45,6 +45,13 @@ extern "C" { # define _PyObject_CAST(op) _Py_CAST(PyObject*, op) #endif +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + // bpo-42262 added Py_NewRef() to Python 3.10.0a3 #if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) @@ -1605,6 +1612,84 @@ static inline int PyIter_NextItem(PyObject *iter, PyObject **item) #endif +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 6ef1873..ecbed45 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1971,6 +1971,47 @@ test_iter(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_long_stdint(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject *obj; + + // Test PyLong_FromInt32() and PyLong_AsInt32() + obj = PyLong_FromInt32(INT32_C(-0x12345678)); + assert(obj != NULL); + int32_t i32; + assert(PyLong_AsInt32(obj, &i32) == 0); + assert(i32 == INT32_C(-0x12345678)); + Py_DECREF(obj); + + // Test PyLong_FromUInt32() and PyLong_AsUInt32() + obj = PyLong_FromUInt32(UINT32_C(0xDEADBEEF)); + assert(obj != NULL); + uint32_t u32; + assert(PyLong_AsUInt32(obj, &u32) == 0); + assert(u32 == UINT32_C(0xDEADBEEF)); + Py_DECREF(obj); + + // Test PyLong_FromInt64() and PyLong_AsInt64() + obj = PyLong_FromInt64(INT64_C(-0x12345678DEADBEEF)); + assert(obj != NULL); + int64_t i64; + assert(PyLong_AsInt64(obj, &i64) == 0); + assert(i64 == INT64_C(-0x12345678DEADBEEF)); + Py_DECREF(obj); + + // Test PyLong_FromUInt64() and PyLong_AsUInt64() + obj = PyLong_FromUInt64(UINT64_C(0xDEADBEEF12345678)); + assert(obj != NULL); + uint64_t u64; + assert(PyLong_AsUInt64(obj, &u64) == 0); + assert(u64 == UINT64_C(0xDEADBEEF12345678)); + Py_DECREF(obj); + + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2016,6 +2057,7 @@ static struct PyMethodDef methods[] = { #endif {"test_bytes", test_bytes, METH_NOARGS, _Py_NULL}, {"test_iter", test_iter, METH_NOARGS, _Py_NULL}, + {"test_long_stdint", test_long_stdint, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 74a7723fc16308f4cc3005b8b1056e47dd6ce8f0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 14 Oct 2024 18:59:32 +0200 Subject: [PATCH 40/87] Fix GitHub Actions for latest Ubuntu (#116) --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f43aa5..b749c60 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,6 @@ jobs: python: # Python versions (CPython): # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - - "3.7" - "3.8" - "3.9" - "3.10" @@ -64,6 +63,8 @@ jobs: # GHA python-versions. - os: ubuntu-20.04 python: 3.6 + - os: ubuntu-22.04 + python: 3.7 steps: # https://github.com/actions/checkout From 6d8c17a2d547eca24b7b3bb17865588385efde4b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:40:27 +0200 Subject: [PATCH 41/87] Fix 'redefinition; different linkage' errors with cp313-win (#115) --- pythoncapi_compat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 34a84c9..acaadf3 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1537,7 +1537,7 @@ static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) } #if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) - extern int _PyUnicode_Equal(PyObject *str1, PyObject *str2); + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); return _PyUnicode_Equal(str1, str2); #elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) @@ -1564,7 +1564,7 @@ static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) { #if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) - extern Py_hash_t _Py_HashBytes(const void *src, Py_ssize_t len); + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); return _Py_HashBytes(ptr, len); #else From ec07618fff0b13c78eb4d5bb4f058b46659a0214 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 15 Oct 2024 14:08:12 +0200 Subject: [PATCH 42/87] Treat warnings as errors on Windows (MSVC) (#117) * Replace "CPP" with "CXX". * Test also C++14 on Windows. * GitHub Actions: upgrade Windows latest Python to 3.13. --- .github/workflows/build.yml | 2 +- tests/setup.py | 77 +++++++++++++++-------------- tests/test_pythoncapi_compat.py | 17 +++++-- tests/test_pythoncapi_compat_cext.c | 8 +-- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b749c60..bdd3672 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: - os: windows-latest python: 3.6 - os: windows-latest - python: 3.12 + python: 3.13 # macOS: test only new Python - os: macos-latest diff --git a/tests/setup.py b/tests/setup.py index 2e10c15..825241e 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -13,38 +13,42 @@ # C++ is only supported on Python 3.6 and newer -TEST_CPP = (sys.version_info >= (3, 6)) +TEST_CXX = (sys.version_info >= (3, 6)) SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) # Windows uses MSVC compiler MSVC = (os.name == "nt") -# C compiler flags for GCC and clang COMMON_FLAGS = [ - # Treat warnings as error - '-Werror', - # Enable all warnings - '-Wall', '-Wextra', - # Extra warnings - '-Wconversion', - # /usr/lib64/pypy3.7/include/pyport.h:68:20: error: redefinition of typedef - # 'Py_hash_t' is a C11 feature - "-Wno-typedef-redefinition", + '-I' + SRC_DIR, ] -CFLAGS = COMMON_FLAGS + [ - # Use C99 for pythoncapi_compat.c which initializes PyModuleDef with a - # mixture of designated and non-designated initializers - '-std=c99', -] -CPPFLAGS = list(COMMON_FLAGS) -# FIXME: _Py_CAST() emits C++ compilers on Python 3.12. -# See: https://github.com/python/cpython/issues/94731 -if 0: - CPPFLAGS.extend(( - '-Wold-style-cast', - '-Wzero-as-null-pointer-constant', +if not MSVC: + # C compiler flags for GCC and clang + COMMON_FLAGS.extend(( + # Treat warnings as error + '-Werror', + # Enable all warnings + '-Wall', '-Wextra', + # Extra warnings + '-Wconversion', + # /usr/lib64/pypy3.7/include/pyport.h:68:20: error: redefinition of typedef + # 'Py_hash_t' is a C11 feature + "-Wno-typedef-redefinition", + )) + CFLAGS = COMMON_FLAGS + [ + # Use C99 for pythoncapi_compat.c which initializes PyModuleDef with a + # mixture of designated and non-designated initializers + '-std=c99', + ] +else: + # C compiler flags for MSVC + COMMON_FLAGS.extend(( + # Treat all compiler warnings as compiler errors + '/WX', )) + CFLAGS = list(COMMON_FLAGS) +CXXFLAGS = list(COMMON_FLAGS) def main(): @@ -66,34 +70,31 @@ def main(): # CC env var overrides sysconfig CC variable in setuptools os.environ['CC'] = cmd - cflags = ['-I' + SRC_DIR] - cppflags = list(cflags) - if not MSVC: - cflags.extend(CFLAGS) - cppflags.extend(CPPFLAGS) - # C extension c_ext = Extension( 'test_pythoncapi_compat_cext', sources=['test_pythoncapi_compat_cext.c'], - extra_compile_args=cflags) + extra_compile_args=CFLAGS) extensions = [c_ext] - if TEST_CPP: + if TEST_CXX: # C++ extension # MSVC has /std flag but doesn't support /std:c++11 if not MSVC: versions = [ - ('test_pythoncapi_compat_cpp03ext', '-std=c++03'), - ('test_pythoncapi_compat_cpp11ext', '-std=c++11'), + ('test_pythoncapi_compat_cpp03ext', ['-std=c++03']), + ('test_pythoncapi_compat_cpp11ext', ['-std=c++11']), ] else: - versions = [('test_pythoncapi_compat_cppext', None)] - for name, flag in versions: - flags = list(cppflags) - if flag is not None: - flags.append(flag) + versions = [ + ('test_pythoncapi_compat_cppext', None), + ('test_pythoncapi_compat_cpp14ext', ['/std:c++14', '/Zc:__cplusplus']), + ] + for name, std_flags in versions: + flags = list(CXXFLAGS) + if std_flags is not None: + flags.extend(std_flags) cpp_ext = Extension( name, sources=['test_pythoncapi_compat_cppext.cpp'], diff --git a/tests/test_pythoncapi_compat.py b/tests/test_pythoncapi_compat.py index 17ade5a..8480415 100644 --- a/tests/test_pythoncapi_compat.py +++ b/tests/test_pythoncapi_compat.py @@ -24,12 +24,23 @@ from utils import run_command, command_stdout +# Windows uses MSVC compiler +MSVC = (os.name == "nt") + TESTS = [ ("test_pythoncapi_compat_cext", "C"), - ("test_pythoncapi_compat_cppext", "C++"), - ("test_pythoncapi_compat_cpp03ext", "C++03"), - ("test_pythoncapi_compat_cpp11ext", "C++11"), ] +if not MSVC: + TESTS.extend(( + ("test_pythoncapi_compat_cpp03ext", "C++03"), + ("test_pythoncapi_compat_cpp11ext", "C++11"), + )) +else: + TESTS.extend(( + ("test_pythoncapi_compat_cppext", "C++"), + ("test_pythoncapi_compat_cpp14ext", "C++14"), + )) + VERBOSE = False diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index ecbed45..419a5dc 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -15,12 +15,14 @@ # define PYTHON3 1 #endif -#if defined(_MSC_VER) && defined(__cplusplus) -# define MODULE_NAME test_pythoncapi_compat_cppext +#if defined(__cplusplus) && __cplusplus >= 201402 +# define MODULE_NAME test_pythoncapi_compat_cpp14ext #elif defined(__cplusplus) && __cplusplus >= 201103 # define MODULE_NAME test_pythoncapi_compat_cpp11ext -#elif defined(__cplusplus) +#elif defined(__cplusplus) && !defined(_MSC_VER) # define MODULE_NAME test_pythoncapi_compat_cpp03ext +#elif defined(__cplusplus) +# define MODULE_NAME test_pythoncapi_compat_cppext #else # define MODULE_NAME test_pythoncapi_compat_cext #endif From 0041177c4f348c8952b4c8980b2c90856e61c7c7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:50:17 +0200 Subject: [PATCH 43/87] Update link to latest version (#118) --- docs/api.rst | 2 +- pythoncapi_compat.h | 2 +- upgrade_pythoncapi.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index b94df42..68429cc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -23,7 +23,7 @@ Some functions related to frame objects and ``PyThreadState`` are not available on PyPy. Latest version of the header file: -`pythoncapi_compat.h `_. +`pythoncapi_compat.h `_. Python 3.14 diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index acaadf3..ba1850e 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -7,7 +7,7 @@ // https://github.com/python/pythoncapi_compat // // Latest version: -// https://raw.githubusercontent.com/python/pythoncapi_compat/master/pythoncapi_compat.h +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h // // SPDX-License-Identifier: 0BSD diff --git a/upgrade_pythoncapi.py b/upgrade_pythoncapi.py index ea6e722..6640cec 100755 --- a/upgrade_pythoncapi.py +++ b/upgrade_pythoncapi.py @@ -10,7 +10,7 @@ PYTHONCAPI_COMPAT_URL = ('https://raw.githubusercontent.com/python/' - 'pythoncapi_compat/master/pythoncapi_compat.h') + 'pythoncapi-compat/main/pythoncapi_compat.h') PYTHONCAPI_COMPAT_H = 'pythoncapi_compat.h' INCLUDE_PYTHONCAPI_COMPAT = f'#include "{PYTHONCAPI_COMPAT_H}"' INCLUDE_PYTHONCAPI_COMPAT2 = f'#include <{PYTHONCAPI_COMPAT_H}>' From 77abeec5330ee0c97ab128066207e1b6f232bb56 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 12 Nov 2024 18:56:51 +0300 Subject: [PATCH 44/87] Add PyLong_IsPositive/Negative/Zero() functions (#119) Co-authored-by: Victor Stinner --- docs/api.rst | 12 ++++++++++++ docs/changelog.rst | 6 ++++++ pythoncapi_compat.h | 30 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 5 +++++ 4 files changed, 53 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 68429cc..7c743f8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -29,6 +29,18 @@ Latest version of the header file: Python 3.14 ----------- +.. c:function:: int PyLong_IsPositive(PyObject *obj) + + See `PyLong_IsPositive() documentation `__. + +.. c:function:: int PyLong_IsNegative(PyObject *obj) + + See `PyLong_IsNegative() documentation `__. + +.. c:function:: int PyLong_IsZero(PyObject *obj) + + See `PyLong_IsZero() documentation `__. + .. c:function:: int PyLong_GetSign(PyObject *obj, int *sign) See `PyLong_GetSign() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index a2d188c..3a616f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +* 2024-11-12: Add functions: + + * ``PyLong_IsPositive()`` + * ``PyLong_IsNegative()`` + * ``PyLong_IsZero()`` + * 2024-10-09: Add functions: * ``PyBytes_Join()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index ba1850e..c51dd6b 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1520,6 +1520,36 @@ static inline int PyLong_GetSign(PyObject *obj, int *sign) } #endif +// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2 +#if PY_VERSION_HEX < 0x030E00A2 +static inline int PyLong_IsPositive(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 1; +} + +static inline int PyLong_IsNegative(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == -1; +} + +static inline int PyLong_IsZero(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 0; +} +#endif + // gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 419a5dc..b8df8a6 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1421,6 +1421,11 @@ test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyLong_GetSign(obj, &sign) == 0); assert(sign == 1); + // test PyLong_IsPositive(), PyLong_IsNegative() and PyLong_IsZero() + assert(PyLong_IsPositive(obj) == 1); + assert(PyLong_IsNegative(obj) == 0); + assert(PyLong_IsZero(obj) == 0); + Py_RETURN_NONE; } From 03e441d5e0bf005b481cb61821d58e400cc1c293 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 15 Nov 2024 11:49:39 +0300 Subject: [PATCH 45/87] Relax PYPY_VERSION_NUM requirements for hash macros (#122) Those added in https://github.com/pypy/pypy/commit/5661dff36f1120e458374954ca2353b87b951089 which is available in PyPy 7.3.8+. --- pythoncapi_compat.h | 4 ++-- tests/test_pythoncapi_compat_cext.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index c51dd6b..7c76858 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1204,11 +1204,11 @@ static inline int PyTime_PerfCounter(PyTime_t *result) #endif // gh-111389 added hash constants to Python 3.13.0a5. These constants were -// added first as private macros to Python 3.4.0b1 and PyPy 7.3.9. +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8. #if (!defined(PyHASH_BITS) \ && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ - && PYPY_VERSION_NUM >= 0x07090000))) + && PYPY_VERSION_NUM >= 0x07030800))) # define PyHASH_BITS _PyHASH_BITS # define PyHASH_MODULUS _PyHASH_MODULUS # define PyHASH_INF _PyHASH_INF diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index b8df8a6..c413689 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1616,7 +1616,7 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #if ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ - && PYPY_VERSION_NUM >= 0x07090000)) + && PYPY_VERSION_NUM >= 0x07030800)) // Just check that constants are available size_t bits = PyHASH_BITS; assert(bits >= 8); From 0f1d42a10a3f594ad48894912396df31b2c2d55d Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Tue, 19 Nov 2024 10:09:30 +0200 Subject: [PATCH 46/87] prepare for pypy3.11 release (#123) --- pythoncapi_compat.h | 6 +++--- runtests.py | 1 - tests/test_pythoncapi_compat_cext.c | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 7c76858..2218b1b 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -287,7 +287,7 @@ PyFrame_GetVarString(PyFrameObject *frame, const char *name) // bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 -#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) static inline PyInterpreterState * PyThreadState_GetInterpreter(PyThreadState *tstate) { @@ -918,7 +918,7 @@ static inline int PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return -1; } Py_VISIT(*dict); @@ -929,7 +929,7 @@ static inline void PyObject_ClearManagedDict(PyObject *obj) { PyObject **dict = _PyObject_GetDictPtr(obj); - if (*dict == NULL) { + if (dict == NULL || *dict == NULL) { return; } Py_CLEAR(*dict); diff --git a/runtests.py b/runtests.py index 5064550..2e92dba 100755 --- a/runtests.py +++ b/runtests.py @@ -13,7 +13,6 @@ import argparse import os.path import shutil -import subprocess import sys try: from shutil import which diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c413689..4f369f7 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -758,7 +758,7 @@ test_import(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) static void gc_collect(void) { -#if defined(PYPY_VERSION) +#if defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000 PyObject *mod = PyImport_ImportModule("gc"); assert(mod != _Py_NULL); @@ -1432,8 +1432,8 @@ test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) // --- HeapCTypeWithManagedDict -------------------------------------------- -// Py_TPFLAGS_MANAGED_DICT was added to Python 3.11.0a3 -#if PY_VERSION_HEX >= 0x030B00A3 +// Py_TPFLAGS_MANAGED_DICT was added to Python 3.11.0a3 but is not implemented on PyPy +#if PY_VERSION_HEX >= 0x030B00A3 && ! defined(PYPY_VERSION) # define TEST_MANAGED_DICT typedef struct { From 900c130f9cdc6ec3ae19a76410c76d39bdc3b958 Mon Sep 17 00:00:00 2001 From: Dan Yeaw Date: Fri, 29 Nov 2024 04:36:26 -0500 Subject: [PATCH 47/87] Add PyGObject as a user (#124) --- docs/users.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/users.rst b/docs/users.rst index b90466f..abf4307 100644 --- a/docs/users.rst +++ b/docs/users.rst @@ -45,7 +45,10 @@ Examples of projects using pythoncapi_compat.h `__) * `PyTorch `_ (`pythoncapi_compat.h copy `__) - +* `PyGObject `_ + (`commit `__, + `pythoncapi-compat Meson subproject + `__) Projects not using pythoncapi_compat.h ====================================== From 61709bfa512f66842fbc70bac5fb3279d0bdba7b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 13 Dec 2024 17:47:15 +0300 Subject: [PATCH 48/87] Add PyLong Import/Export API (#121) PyPy is not supported, as well as Python 2. In the later case it's possible, but hardly worth code complications: most real-word potential consumers (e.g. Sage, gmpy2 or python-flint) support only Python 3. Co-authored-by: Victor Stinner --- docs/api.rst | 36 ++++++ docs/changelog.rst | 12 ++ pythoncapi_compat.h | 179 ++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 44 +++++++ 4 files changed, 271 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 7c743f8..07b303a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -29,6 +29,42 @@ Latest version of the header file: Python 3.14 ----------- +.. c:struct:: PyLongLayout + + See `PyLongLayout documentation `__. + +.. c:function:: const PyLongLayout* PyLong_GetNativeLayout(void) + + See `PyLong_GetNativeLayout() documentation `__. + +.. c:struct:: PyLongExport + + See `PyLongExport documentation `__. + +.. c:function:: int PyLong_Export(PyObject *obj, PyLongExport *export_long) + + See `PyLong_Export() documentation `__. + +.. c:function:: void PyLong_FreeExport(PyLongExport *export_long) + + See `PyLong_FreeExport() documentation `__. + +.. c:struct:: PyLongWriter + + See `PyLongWriter documentation `__. + +.. c:function:: PyLongWriter* PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) + + See `PyLongWriter_Create() documentation `__. + +.. c:function:: PyObject* PyLongWriter_Finish(PyLongWriter *writer) + + See `PyLongWriter_Finish() documentation `__. + +.. c:function:: void PyLongWriter_Discard(PyLongWriter *writer) + + See `PyLongWriter_Discard() documentation `__. + .. c:function:: int PyLong_IsPositive(PyObject *obj) See `PyLong_IsPositive() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a616f7..ec263a3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,18 @@ Changelog ========= +* 2024-12-13: Add functions and structs: + + * ``PyLongLayout`` + * ``PyLong_GetNativeLayout()`` + * ``PyLongExport`` + * ``PyLong_Export()`` + * ``PyLong_FreeExport()`` + * ``PyLongWriter`` + * ``PyLongWriter_Create()`` + * ``PyLongWriter_Finish()`` + * ``PyLongWriter_Discard()`` + * 2024-11-12: Add functions: * ``PyLong_IsPositive()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 2218b1b..5e22e7d 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1720,6 +1720,185 @@ static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) #endif +// gh-102471 added import and export API for integers to 3.14.0a2. +#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +// Helpers to access PyLongObject internals. +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if PY_VERSION_HEX >= 0x030C0000 + op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3); +#elif PY_VERSION_HEX >= 0x030900A4 + Py_SET_SIZE(op, sign * size); +#else + Py_SIZE(op) = sign * size; +#endif +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (Py_ssize_t)(op->long_value.lv_tag >> 3); +#else + return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op); +#endif +} + +static inline digit* +_PyLong_GetDigits(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (digit*)(op->long_value.ob_digit); +#else + return (digit*)(op->ob_digit); +#endif +} + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + Py_uintptr_t _reserved; +} PyLongExport; + +typedef struct PyLongWriter PyLongWriter; + +static inline const PyLongLayout* +PyLong_GetNativeLayout(void) +{ + static const PyLongLayout PyLong_LAYOUT = { + PyLong_SHIFT, + sizeof(digit), + -1, // least significant first + PY_LITTLE_ENDIAN ? -1 : 1, + }; + + return &PyLong_LAYOUT; +} + +static inline int +PyLong_Export(PyObject *obj, PyLongExport *export_long) +{ + if (!PyLong_Check(obj)) { + memset(export_long, 0, sizeof(*export_long)); + PyErr_Format(PyExc_TypeError, "expected int, got %s", + Py_TYPE(obj)->tp_name); + return -1; + } + + // Fast-path: try to convert to a int64_t + PyLongObject *self = (PyLongObject*)obj; + int overflow; +#if SIZEOF_LONG == 8 + long value = PyLong_AsLongAndOverflow(obj, &overflow); +#else + // Windows has 32-bit long, so use 64-bit long long instead + long long value = PyLong_AsLongLongAndOverflow(obj, &overflow); +#endif + Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t)); + // the function cannot fail since obj is a PyLongObject + assert(!(value == -1 && PyErr_Occurred())); + + if (!overflow) { + export_long->value = value; + export_long->negative = 0; + export_long->ndigits = 0; + export_long->digits = 0; + export_long->_reserved = 0; + } + else { + export_long->value = 0; + export_long->negative = _PyLong_Sign(obj) < 0; + export_long->ndigits = _PyLong_DigitCount(self); + if (export_long->ndigits == 0) { + export_long->ndigits = 1; + } + export_long->digits = _PyLong_GetDigits(self); + export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj); + } + return 0; +} + +static inline void +PyLong_FreeExport(PyLongExport *export_long) +{ + PyObject *obj = (PyObject*)export_long->_reserved; + + if (obj) { + export_long->_reserved = 0; + Py_DECREF(obj); + } +} + +static inline PyLongWriter* +PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) +{ + if (ndigits <= 0) { + PyErr_SetString(PyExc_ValueError, "ndigits must be positive"); + return NULL; + } + assert(digits != NULL); + + PyLongObject *obj = _PyLong_New(ndigits); + if (obj == NULL) { + return NULL; + } + _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits); + + *digits = _PyLong_GetDigits(obj); + return (PyLongWriter*)obj; +} + +static inline void +PyLongWriter_Discard(PyLongWriter *writer) +{ + PyLongObject *obj = (PyLongObject *)writer; + + assert(Py_REFCNT(obj) == 1); + Py_DECREF(obj); +} + +static inline PyObject* +PyLongWriter_Finish(PyLongWriter *writer) +{ + PyObject *obj = (PyObject *)writer; + PyLongObject *self = (PyLongObject*)obj; + Py_ssize_t j = _PyLong_DigitCount(self); + Py_ssize_t i = j; + int sign = _PyLong_Sign(obj); + + assert(Py_REFCNT(obj) == 1); + + // Normalize and get singleton if possible + while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) { + --i; + } + if (i != j) { + if (i == 0) { + sign = 0; + } + _PyLong_SetSignAndDigitCount(self, sign, i); + } + if (i <= 1) { + long val = sign * (long)(_PyLong_GetDigits(self)[0]); + Py_DECREF(obj); + return PyLong_FromLong(val); + } + + return obj; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 4f369f7..28663d2 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1426,6 +1426,50 @@ test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyLong_IsNegative(obj) == 0); assert(PyLong_IsZero(obj) == 0); +#if defined(PYTHON3) && !defined(PYPY_VERSION) + // test import/export API + digit *digits; + PyLongWriter *writer; + static PyLongExport long_export; + + writer = PyLongWriter_Create(1, 1, (void**)&digits); + PyLongWriter_Discard(writer); + + writer = PyLongWriter_Create(1, 1, (void**)&digits); + digits[0] = 123; + obj = PyLongWriter_Finish(writer); + + check_int(obj, -123); + PyLong_Export(obj, &long_export); + assert(long_export.value == -123); + assert(long_export.digits == NULL); + PyLong_FreeExport(&long_export); + Py_DECREF(obj); + + writer = PyLongWriter_Create(0, 5, (void**)&digits); + digits[0] = 1; + digits[1] = 0; + digits[2] = 0; + digits[3] = 0; + digits[4] = 1; + obj = PyLongWriter_Finish(writer); + + PyLong_Export(obj, &long_export); + assert(long_export.value == 0); + digits = (digit*)long_export.digits; + assert(digits[0] == 1); + assert(digits[1] == 0); + assert(digits[2] == 0); + assert(digits[3] == 0); + assert(digits[4] == 1); + PyLong_FreeExport(&long_export); + Py_DECREF(obj); + + const PyLongLayout *layout = PyLong_GetNativeLayout(); + assert(layout->digits_order == -1); + assert(layout->digit_size == sizeof(digit)); +#endif // defined(PYTHON3) && !defined(PYPY_VERSION) + Py_RETURN_NONE; } From 7eb512b67cf3b4449c72bdfba04af24cb1503514 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 16 Dec 2024 14:41:38 +0100 Subject: [PATCH 49/87] Add structmember.h constants (#126) --- docs/changelog.rst | 26 +++++++++++++++++++++ pythoncapi_compat.h | 34 ++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 35 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index ec263a3..3500bd5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,32 @@ Changelog ========= +* 2024-12-16: Add ``structmember.h`` constants: + + * ``Py_T_BOOL`` + * ``Py_T_BYTE`` + * ``Py_T_CHAR`` + * ``Py_T_DOUBLE`` + * ``Py_T_FLOAT`` + * ``Py_T_INT`` + * ``Py_T_LONGLONG`` + * ``Py_T_LONG`` + * ``Py_T_OBJECT_EX`` + * ``Py_T_PYSSIZET`` + * ``Py_T_SHORT`` + * ``Py_T_STRING_INPLACE`` + * ``Py_T_STRING`` + * ``Py_T_UBYTE`` + * ``Py_T_UINT`` + * ``Py_T_ULONGLONG`` + * ``Py_T_ULONG`` + * ``Py_T_USHORT`` + * ``_Py_T_NONE`` + * ``_Py_T_OBJECT`` + * ``Py_AUDIT_READ`` + * ``Py_READONLY`` + * ``_Py_WRITE_RESTRICTED`` + * 2024-12-13: Add functions and structs: * ``PyLongLayout`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 5e22e7d..cee282d 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -24,6 +24,9 @@ extern "C" { #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) # include "frameobject.h" // PyFrameObject, PyFrame_GetBack() #endif +#if PY_VERSION_HEX < 0x030C00A3 +# include // T_SHORT, READONLY +#endif #ifndef _Py_CAST @@ -1899,6 +1902,37 @@ PyLongWriter_Finish(PyLongWriter *writer) #endif +#if PY_VERSION_HEX < 0x030C00A3 +# define Py_T_SHORT T_SHORT +# define Py_T_INT T_INT +# define Py_T_LONG T_LONG +# define Py_T_FLOAT T_FLOAT +# define Py_T_DOUBLE T_DOUBLE +# define Py_T_STRING T_STRING +# define _Py_T_OBJECT T_OBJECT +# define Py_T_CHAR T_CHAR +# define Py_T_BYTE T_BYTE +# define Py_T_UBYTE T_UBYTE +# define Py_T_USHORT T_USHORT +# define Py_T_UINT T_UINT +# define Py_T_ULONG T_ULONG +# define Py_T_STRING_INPLACE T_STRING_INPLACE +# define Py_T_BOOL T_BOOL +# define Py_T_OBJECT_EX T_OBJECT_EX +# define Py_T_LONGLONG T_LONGLONG +# define Py_T_ULONGLONG T_ULONGLONG +# define Py_T_PYSSIZET T_PYSSIZET + +# if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +# define _Py_T_NONE T_NONE +# endif + +# define Py_READONLY READONLY +# define Py_AUDIT_READ READ_RESTRICTED +# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 28663d2..aff19e0 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2063,6 +2063,40 @@ test_long_stdint(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_structmember(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + assert(Py_T_SHORT >= 0); + assert(Py_T_INT >= 0); + assert(Py_T_LONG >= 0); + assert(Py_T_FLOAT >= 0); + assert(Py_T_DOUBLE >= 0); + assert(Py_T_STRING >= 0); + assert(_Py_T_OBJECT >= 0); + assert(Py_T_CHAR >= 0); + assert(Py_T_BYTE >= 0); + assert(Py_T_UBYTE >= 0); + assert(Py_T_USHORT >= 0); + assert(Py_T_UINT >= 0); + assert(Py_T_ULONG >= 0); + assert(Py_T_STRING_INPLACE >= 0); + assert(Py_T_BOOL >= 0); + assert(Py_T_OBJECT_EX >= 0); + assert(Py_T_LONGLONG >= 0); + assert(Py_T_ULONGLONG >= 0); + assert(Py_T_PYSSIZET >= 0); +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + assert(_Py_T_NONE >= 0); +#endif + + assert(Py_READONLY >= 0); + assert(Py_AUDIT_READ >= 0); + assert(_Py_WRITE_RESTRICTED >= 0); + + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2109,6 +2143,7 @@ static struct PyMethodDef methods[] = { {"test_bytes", test_bytes, METH_NOARGS, _Py_NULL}, {"test_iter", test_iter, METH_NOARGS, _Py_NULL}, {"test_long_stdint", test_long_stdint, METH_NOARGS, _Py_NULL}, + {"test_structmember", test_structmember, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 79404e9b5bab9d40d71f18562e20df03fc69e7cb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 6 Jan 2025 14:39:36 +0100 Subject: [PATCH 50/87] Add Py_fopen() and Py_fclose() (#127) --- docs/api.rst | 8 ++++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 41 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 16 +++++++++++ 4 files changed, 66 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 07b303a..a85a61d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -169,6 +169,14 @@ Python 3.14 See `PyLong_FromUInt64() documentation `__. +.. c:function:: FILE* Py_fopen(PyObject *path, const char *mode) + + See `Py_fopen() documentation `__. + +.. c:function:: int Py_fclose(FILE *file) + + See `Py_fclose() documentation `__. + Not supported: diff --git a/docs/changelog.rst b/docs/changelog.rst index 3500bd5..04a63b2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2025-01-06: Add ``Py_fopen()`` and ``Py_fclose()`` functions. * 2024-12-16: Add ``structmember.h`` constants: * ``Py_T_BOOL`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index cee282d..0ea8e80 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1933,6 +1933,47 @@ PyLongWriter_Finish(PyLongWriter *writer) #endif +// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A4 +static inline FILE* Py_fopen(PyObject *path, const char *mode) +{ +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + extern FILE* _Py_fopen_obj(PyObject *path, const char *mode); + return _Py_fopen_obj(path, mode); +#else + FILE *f; + PyObject *bytes; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } +#else + if (!PyString_Check(path)) { + PyErr_SetString(PyExc_TypeError, "except str"); + return NULL; + } + bytes = Py_NewRef(path); +#endif + const char *path_bytes = PyBytes_AS_STRING(bytes); + + f = fopen(path_bytes, mode); + Py_DECREF(bytes); + + if (f == NULL) { + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path); + return NULL; + } + return f; +#endif +} + +int Py_fclose(FILE *file) +{ + return fclose(file); +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index aff19e0..f1557bc 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2097,6 +2097,21 @@ test_structmember(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject * +test_file(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + const char *filename = __FILE__; + PyObject *path = create_string(filename); + + FILE *fp = Py_fopen(path, "rb"); + Py_DECREF(path); + assert(fp != NULL); + Py_fclose(fp); + + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2144,6 +2159,7 @@ static struct PyMethodDef methods[] = { {"test_iter", test_iter, METH_NOARGS, _Py_NULL}, {"test_long_stdint", test_long_stdint, METH_NOARGS, _Py_NULL}, {"test_structmember", test_structmember, METH_NOARGS, _Py_NULL}, + {"test_file", test_file, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From b1b2071331f0d88d7661ca9c0ad19b81282071bc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jan 2025 22:42:27 +0100 Subject: [PATCH 51/87] Optimize PyWeakref_GetRef() (#129) --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 0ea8e80..c533b82 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -583,7 +583,7 @@ static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) return 0; } *pobj = Py_NewRef(obj); - return (*pobj != NULL); + return 1; } #endif From 8a52253179b91d0345b1d68ffeb518fce577b8f3 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 19 Jan 2025 22:46:25 +0100 Subject: [PATCH 52/87] Add PyConfig_Get() (#128) --- docs/api.rst | 10 +- docs/changelog.rst | 1 + pythoncapi_compat.h | 224 ++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 63 ++++++++ 4 files changed, 296 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a85a61d..f2dd262 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -177,11 +177,17 @@ Python 3.14 See `Py_fclose() documentation `__. +.. c:function:: PyObject* PyConfig_Get(const char *name) + + See `PyConfig_Get() documentation `__. + +.. c:function:: int PyConfig_GetInt(const char *name, int *value) + + See `PyConfig_GetInt() documentation `__. + Not supported: -* ``PyConfig_Get()`` -* ``PyConfig_GetInt()`` * ``PyConfig_Names()`` * ``PyConfig_Set()`` * ``PyInitConfig_AddModule()`` diff --git a/docs/changelog.rst b/docs/changelog.rst index 04a63b2..74e046b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2025-01-19: Add ``PyConfig_Get()`` functions. * 2025-01-06: Add ``Py_fopen()`` and ``Py_fclose()`` functions. * 2024-12-16: Add ``structmember.h`` constants: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index c533b82..8c1822d 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -19,6 +19,7 @@ extern "C" { #endif #include +#include // offsetof() // Python 3.11.0b4 added PyFrame_Back() to Python.h #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) @@ -1974,6 +1975,229 @@ int Py_fclose(FILE *file) #endif +#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +static inline PyObject* +PyConfig_Get(const char *name) +{ + typedef enum { + _PyConfig_MEMBER_INT, + _PyConfig_MEMBER_UINT, + _PyConfig_MEMBER_ULONG, + _PyConfig_MEMBER_BOOL, + _PyConfig_MEMBER_WSTR, + _PyConfig_MEMBER_WSTR_OPT, + _PyConfig_MEMBER_WSTR_LIST, + } PyConfigMemberType; + + typedef struct { + const char *name; + size_t offset; + PyConfigMemberType type; + const char *sys_attr; + } PyConfigSpec; + +#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \ + {#MEMBER, offsetof(PyConfig, MEMBER), \ + _PyConfig_MEMBER_##TYPE, sys_attr} + + static const PyConfigSpec config_spec[] = { + PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"), + PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"), + PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"), + PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"), + PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), + PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), + PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), + PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), + PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"), +#endif + PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"), + PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"), + PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL), +#endif +#ifdef Py_GIL_DISABLED + PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL), +#ifdef MS_WINDOWS + PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"), +#endif + PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX && defined(__APPLE__) + PYTHONCAPI_COMPAT_SPEC(use_system_logger, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), +#endif + }; + +#undef PYTHONCAPI_COMPAT_SPEC + + const PyConfigSpec *spec; + int found = 0; + for (size_t i=0; i < Py_ARRAY_LENGTH(config_spec); i++) { + spec = &config_spec[i]; + if (strcmp(spec->name, name) == 0) { + found = 1; + break; + } + } + if (found) { + if (spec->sys_attr != NULL) { + PyObject *value = PySys_GetObject(spec->sys_attr); + if (value == NULL) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr); + return NULL; + } + return Py_NewRef(value); + } + + extern const PyConfig* _Py_GetConfig(void); + const PyConfig *config = _Py_GetConfig(); + void *member = (char *)config + spec->offset; + switch (spec->type) { + case _PyConfig_MEMBER_INT: + case _PyConfig_MEMBER_UINT: + { + int value = *(int *)member; + return PyLong_FromLong(value); + } + case _PyConfig_MEMBER_BOOL: + { + int value = *(int *)member; + return PyBool_FromLong(value != 0); + } + case _PyConfig_MEMBER_ULONG: + { + unsigned long value = *(unsigned long *)member; + return PyLong_FromUnsignedLong(value); + } + case _PyConfig_MEMBER_WSTR: + case _PyConfig_MEMBER_WSTR_OPT: + { + wchar_t *wstr = *(wchar_t **)member; + if (wstr != NULL) { + return PyUnicode_FromWideChar(wstr, -1); + } + else { + return Py_NewRef(Py_None); + } + } + case _PyConfig_MEMBER_WSTR_LIST: + { + const PyWideStringList *list = (const PyWideStringList *)member; + PyObject *tuple = PyTuple_New(list->length); + if (tuple == NULL) { + return NULL; + } + + for (Py_ssize_t i = 0; i < list->length; i++) { + PyObject *item = PyUnicode_FromWideChar(list->items[i], -1); + if (item == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, item); + } + return tuple; + } + default: + Py_UNREACHABLE(); + } + } + + PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name); + return NULL; +} + +static inline int +PyConfig_GetInt(const char *name, int *value) +{ + PyObject *obj = PyConfig_Get(name); + if (obj == NULL) { + return -1; + } + + if (!PyLong_Check(obj)) { + Py_DECREF(obj); + PyErr_Format(PyExc_TypeError, "config option %s is not an int", name); + return -1; + } + + int as_int = PyLong_AsInt(obj); + Py_DECREF(obj); + if (as_int == -1 && PyErr_Occurred()) { + PyErr_Format(PyExc_OverflowError, + "config option %s value does not fit into a C int", name); + return -1; + } + + *value = as_int; + return 0; +} +#endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index f1557bc..bae0ee3 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2112,6 +2112,66 @@ test_file(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +#if 0x03090000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +static PyObject * +test_config(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + // Test PyConfig_Get() + PyObject *sys = PyImport_ImportModule("sys"); + if (sys == _Py_NULL) { + return _Py_NULL; + } + + PyObject *obj = PyConfig_Get("argv"); + PyObject *sys_attr = PyObject_GetAttrString(sys, "argv"); + assert(obj == sys_attr); + Py_DECREF(obj); + Py_DECREF(sys_attr); + + obj = PyConfig_Get("module_search_paths"); + sys_attr = PyObject_GetAttrString(sys, "path"); + assert(obj == sys_attr); + Py_DECREF(obj); + Py_DECREF(sys_attr); + + obj = PyConfig_Get("xoptions"); + sys_attr = PyObject_GetAttrString(sys, "_xoptions"); + assert(obj == sys_attr); + Py_DECREF(obj); + Py_DECREF(sys_attr); + + obj = PyConfig_Get("use_environment"); + assert(PyBool_Check(obj)); + Py_DECREF(obj); + + obj = PyConfig_Get("verbose"); + assert(PyLong_Check(obj)); + Py_DECREF(obj); + + assert(PyConfig_Get("nonexistent") == NULL); + assert(PyErr_ExceptionMatches(PyExc_ValueError)); + PyErr_Clear(); + + // Test PyConfig_GetInt() + int value = -3; + + assert(PyConfig_GetInt("verbose", &value) == 0); + assert(value >= 0); + + assert(PyConfig_GetInt("argv", &value) == -1); + assert(PyErr_ExceptionMatches(PyExc_TypeError)); + PyErr_Clear(); + + assert(PyConfig_GetInt("nonexistent", &value) == -1); + assert(PyErr_ExceptionMatches(PyExc_ValueError)); + PyErr_Clear(); + + Py_DECREF(sys); + Py_RETURN_NONE; +} +#endif + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2160,6 +2220,9 @@ static struct PyMethodDef methods[] = { {"test_long_stdint", test_long_stdint, METH_NOARGS, _Py_NULL}, {"test_structmember", test_structmember, METH_NOARGS, _Py_NULL}, {"test_file", test_file, METH_NOARGS, _Py_NULL}, +#if 0x03090000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + {"test_config", test_config, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 03043e3bb23e81094f17cecd1eb58a0a893f05a1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 26 Jan 2025 12:45:01 +0100 Subject: [PATCH 53/87] GHA: Test more Python versions on macOS/Windows (#132) Remove PyConfig.use_system_logger: it will only be available on Python 3.13.2 which is not released yet. --- .github/workflows/build.yml | 37 ++++++++++++++++++++++++++++++------- pythoncapi_compat.h | 3 --- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdd3672..dd726af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,22 +49,45 @@ jobs: - "pypy3.7-v7.3.2" include: - # Windows: test old and new Python + # Windows - os: windows-latest - python: 3.6 + python: "3.6" - os: windows-latest - python: 3.13 + python: "3.7" + - os: windows-latest + python: "3.8" + - os: windows-latest + python: "3.9" + - os: windows-latest + python: "3.10" + - os: windows-latest + python: "3.11" + - os: windows-latest + python: "3.12" + - os: windows-latest + python: "3.13" - # macOS: test only new Python + # macOS + # Python 3.8 is the oldest version available on macOS/arm64. + - os: macos-latest + python: "3.8" + - os: macos-latest + python: "3.9" + - os: macos-latest + python: "3.10" + - os: macos-latest + python: "3.11" + - os: macos-latest + python: "3.12" - os: macos-latest - python: 3.12 + python: "3.13" # Ubuntu: test deadsnakes Python versions which are not supported by # GHA python-versions. - os: ubuntu-20.04 - python: 3.6 + python: "3.6" - os: ubuntu-22.04 - python: 3.7 + python: "3.7" steps: # https://github.com/actions/checkout diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 8c1822d..52f36cc 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2082,9 +2082,6 @@ PyConfig_Get(const char *name) PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), #endif PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), -#if 0x030D0000 <= PY_VERSION_HEX && defined(__APPLE__) - PYTHONCAPI_COMPAT_SPEC(use_system_logger, BOOL, _Py_NULL), -#endif PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), #if 0x030A0000 <= PY_VERSION_HEX PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), From d24c2016f1d418a954369adac029fc30058ddbe4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 26 Jan 2025 12:50:10 +0100 Subject: [PATCH 54/87] Fix multiple definitions for Py_fclose (#130) --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 52f36cc..8cf63f5 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1968,7 +1968,7 @@ static inline FILE* Py_fopen(PyObject *path, const char *mode) #endif } -int Py_fclose(FILE *file) +static inline int Py_fclose(FILE *file) { return fclose(file); } From d341dac942a419a6d5ef17b06eced51e0c9921fd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 26 Jan 2025 12:54:05 +0100 Subject: [PATCH 55/87] GHA: Test Python 3.14 (#133) --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd726af..3164e9e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,9 +26,10 @@ jobs: - "3.10" - "3.11" - "3.12" - # CPython 3.13 final is scheduled for October 2024: - # https://peps.python.org/pep-0719/ - "3.13" + # CPython 3.14 final is scheduled for October 2025: + # https://peps.python.org/pep-0719/ + - "3.14" # PyPy versions: # - https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md From 4c5370162377493e25da47e32f84a3df5b397804 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 26 Jan 2025 13:08:13 +0100 Subject: [PATCH 56/87] Avoid Py_ARRAY_LENGTH() (#134) Replace Py_ARRAY_LENGTH(array) with sizeof(array)/sizeof(array[0]). Py_ARRAY_LENGTH() fails with C++ on Python 3.9 on macOS. --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 8cf63f5..4d28846 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2092,7 +2092,7 @@ PyConfig_Get(const char *name) const PyConfigSpec *spec; int found = 0; - for (size_t i=0; i < Py_ARRAY_LENGTH(config_spec); i++) { + for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) { spec = &config_spec[i]; if (strcmp(spec->name, name) == 0) { found = 1; From 81eefa76fa1483f68cf489940715d42fadeb484e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 26 Jan 2025 13:16:23 +0100 Subject: [PATCH 57/87] test_config: get the last PyConfig member (#135) --- tests/test_pythoncapi_compat_cext.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index bae0ee3..9d324a4 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2148,6 +2148,15 @@ test_config(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyLong_Check(obj)); Py_DECREF(obj); + // Get the last member +#if 0x030A0000 <= PY_VERSION_HEX + obj = PyConfig_Get("warn_default_encoding"); +#else + obj = PyConfig_Get("user_site_directory"); +#endif + assert(PyLong_Check(obj)); + Py_DECREF(obj); + assert(PyConfig_Get("nonexistent") == NULL); assert(PyErr_ExceptionMatches(PyExc_ValueError)); PyErr_Clear(); From c84545f0e1e21757d4901f75c47333d25a3fcff0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 28 Jan 2025 14:18:38 +0100 Subject: [PATCH 58/87] Closes #136: Replace extern with PyAPI_FUNC() (#137) --- pythoncapi_compat.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4d28846..e534c1c 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1939,7 +1939,8 @@ PyLongWriter_Finish(PyLongWriter *writer) static inline FILE* Py_fopen(PyObject *path, const char *mode) { #if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) - extern FILE* _Py_fopen_obj(PyObject *path, const char *mode); + PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); + return _Py_fopen_obj(path, mode); #else FILE *f; @@ -2109,7 +2110,8 @@ PyConfig_Get(const char *name) return Py_NewRef(value); } - extern const PyConfig* _Py_GetConfig(void); + PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + const PyConfig *config = _Py_GetConfig(); void *member = (char *)config + spec->offset; switch (spec->type) { From 632d1aa0c4be6c67498d6b97630ddd7d7eb0f90a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 18 Feb 2025 11:05:49 +0100 Subject: [PATCH 59/87] Don't redefine _Py_NULL macro if already defined (#138) Original mypy fix by Michael R. Crusoe. --- pythoncapi_compat.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index e534c1c..4b179e4 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -37,11 +37,13 @@ extern "C" { // Static inline functions should use _Py_NULL rather than using directly NULL // to prevent C++ compiler warnings. On C23 and newer and on C++11 and newer, // _Py_NULL is defined as nullptr. -#if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ - || (defined(__cplusplus) && __cplusplus >= 201103) -# define _Py_NULL nullptr -#else -# define _Py_NULL NULL +#ifndef _Py_NULL +# if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ + || (defined(__cplusplus) && __cplusplus >= 201103) +# define _Py_NULL nullptr +# else +# define _Py_NULL NULL +# endif #endif // Cast argument to PyObject* type. From 3082742a3b0fdd795281b06525534f9e56f9e407 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 31 Mar 2025 20:20:44 +0200 Subject: [PATCH 60/87] Test PyPy 3.11 (#140) --- .github/workflows/build.yml | 1 + runtests.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3164e9e..ae2bf42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,7 @@ jobs: - "pypy3.8" - "pypy3.9" - "pypy3.10" + - "pypy3.11" # Old PyPy versions # See https://foss.heptapod.net/pypy/pypy/-/issues/3991 diff --git a/runtests.py b/runtests.py index 2e92dba..c858516 100755 --- a/runtests.py +++ b/runtests.py @@ -52,6 +52,7 @@ "pypy3.8", "pypy3.9", "pypy3.10", + "pypy3.11", ) From 0a8b2c56331a31d7f7096faaa1c1c26467b51c15 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 14 Apr 2025 18:52:50 +0200 Subject: [PATCH 61/87] upgrade_pythoncapi: try to preserve newlines when patching (#141) In case we use \n on Windows or \r\n on Linux we don't want patching those files change every line due to newlines being adjusted to the platform defaults. Instead pass 'newline=""' to all open() calls to preserve newlines. And when adding new lines use the first type of newline found in the file. The test changes make the patching code reuseable and adds a second test with multiple different newlines in the input. --- tests/test_upgrade_pythoncapi.py | 77 +++++++++++++++++++------------- upgrade_pythoncapi.py | 12 +++-- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/tests/test_upgrade_pythoncapi.py b/tests/test_upgrade_pythoncapi.py index bf6f648..48bad12 100644 --- a/tests/test_upgrade_pythoncapi.py +++ b/tests/test_upgrade_pythoncapi.py @@ -43,33 +43,12 @@ def reformat(source): class Tests(unittest.TestCase): maxDiff = 80 * 30 - def _test_patch_file(self, tmp_dir): - # test Patcher.patcher() - source = """ - PyTypeObject* - test_type(PyObject *obj, PyTypeObject *type) - { - Py_TYPE(obj) = type; - return Py_TYPE(obj); - } - """ - expected = """ - #include "pythoncapi_compat.h" - - PyTypeObject* - test_type(PyObject *obj, PyTypeObject *type) - { - Py_SET_TYPE(obj, type); - return Py_TYPE(obj); - } - """ - source = reformat(source) - expected = reformat(expected) - + def _patch_file(self, source, tmp_dir=None): + # test Patcher.patcher() filename = tempfile.mktemp(suffix='.c', dir=tmp_dir) old_filename = filename + ".old" try: - with open(filename, "w", encoding="utf-8") as fp: + with open(filename, "w", encoding="utf-8", newline="") as fp: fp.write(source) old_stderr = sys.stderr @@ -93,10 +72,10 @@ def _test_patch_file(self, tmp_dir): sys.stderr = old_stderr sys.argv = old_argv - with open(filename, encoding="utf-8") as fp: + with open(filename, encoding="utf-8", newline="") as fp: new_contents = fp.read() - with open(old_filename, encoding="utf-8") as fp: + with open(old_filename, encoding="utf-8", newline="") as fp: old_contents = fp.read() finally: try: @@ -108,15 +87,53 @@ def _test_patch_file(self, tmp_dir): except FileNotFoundError: pass - self.assertEqual(new_contents, expected) self.assertEqual(old_contents, source) + return new_contents def test_patch_file(self): - self._test_patch_file(None) + source = """ + PyTypeObject* + test_type(PyObject *obj, PyTypeObject *type) + { + Py_TYPE(obj) = type; + return Py_TYPE(obj); + } + """ + expected = """ + #include "pythoncapi_compat.h" + + PyTypeObject* + test_type(PyObject *obj, PyTypeObject *type) + { + Py_SET_TYPE(obj, type); + return Py_TYPE(obj); + } + """ + source = reformat(source) + expected = reformat(expected) + + new_contents = self._patch_file(source) + self.assertEqual(new_contents, expected) - def test_patch_directory(self): with tempfile.TemporaryDirectory() as tmp_dir: - self._test_patch_file(tmp_dir) + new_contents = self._patch_file(source, tmp_dir) + self.assertEqual(new_contents, expected) + + def test_patch_file_preserve_newlines(self): + source = """ + Py_ssize_t get_size(PyVarObject *obj)\r\n\ + \n\ + { return obj->ob_size; }\r\ + """ + expected = """ + Py_ssize_t get_size(PyVarObject *obj)\r\n\ + \n\ + { return Py_SIZE(obj); }\r\ + """ + source = reformat(source) + expected = reformat(expected) + new_contents = self._patch_file(source) + self.assertEqual(new_contents, expected) def check_replace(self, source, expected, **kwargs): source = reformat(source) diff --git a/upgrade_pythoncapi.py b/upgrade_pythoncapi.py index 6640cec..68b5c53 100755 --- a/upgrade_pythoncapi.py +++ b/upgrade_pythoncapi.py @@ -554,13 +554,17 @@ def _get_operations(self, parser): return operations def add_line(self, content, line): - line = line + '\n' + # Use the first matching newline + match = re.search(r'(?:\r\n|\n|\r)', content) + newline = match.group(0) if match else '\n' + + line = line + newline # FIXME: tolerate trailing spaces if line not in content: # FIXME: add macro after the first header comment # FIXME: add macro after includes # FIXME: add macro after: #define PY_SSIZE_T_CLEAN - return line + '\n' + content + return line + newline + content else: return content @@ -601,7 +605,7 @@ def patch_file(self, filename): encoding = "utf-8" errors = "surrogateescape" - with open(filename, encoding=encoding, errors=errors) as fp: + with open(filename, encoding=encoding, errors=errors, newline="") as fp: old_contents = fp.read() new_contents, operations = self._patch(old_contents) @@ -620,7 +624,7 @@ def patch_file(self, filename): # If old_filename already exists, replace it os.replace(filename, old_filename) - with open(filename, "w", encoding=encoding, errors=errors) as fp: + with open(filename, "w", encoding=encoding, errors=errors, newline="") as fp: fp.write(new_contents) self.applied_operations |= set(operations) From ecf3cd40f004fe0f63cca6f80ac4eae330c4605b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Jun 2025 16:55:52 +0200 Subject: [PATCH 62/87] GHA: Get rid of Ubuntu 20.04 (#144) GHA no longer supports Ubuntu 20.04. Remove also Python 3.8 on macOS. --- .github/workflows/build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ae2bf42..72d91db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,9 +70,7 @@ jobs: python: "3.13" # macOS - # Python 3.8 is the oldest version available on macOS/arm64. - - os: macos-latest - python: "3.8" + # Python 3.9 is the oldest version available on macOS/arm64. - os: macos-latest python: "3.9" - os: macos-latest @@ -86,8 +84,6 @@ jobs: # Ubuntu: test deadsnakes Python versions which are not supported by # GHA python-versions. - - os: ubuntu-20.04 - python: "3.6" - os: ubuntu-22.04 python: "3.7" From fde4d3457d7c1e6f7d09afeae5afd2218fbb2cae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Jun 2025 16:59:23 +0200 Subject: [PATCH 63/87] Add PySys_GetAttr() function (#143) --- docs/api.rst | 19 ++++++++ docs/changelog.rst | 7 +++ pythoncapi_compat.h | 65 ++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 72 +++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index f2dd262..609edbd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -26,6 +26,25 @@ Latest version of the header file: `pythoncapi_compat.h `_. +Python 3.15 +----------- + +.. c:function:: PyObject* PySys_GetAttr(const char *name) + + See `PySys_GetAttr() documentation `__. + +.. c:function:: PyObject* PySys_GetAttrString(const char *name) + + See `PySys_GetAttrString() documentation `__. + +.. c:function:: PyObject* PySys_GetOptionalAttr(const char *name) + + See `PySys_GetOptionalAttr() documentation `__. + +.. c:function:: PyObject* PySys_GetOptionalAttrString(const char *name) + + See `PySys_GetOptionalAttrString() documentation `__. + Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 74e046b..bb981c4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ Changelog ========= +* 2025-06-03: Add functions: + + * ``PySys_GetAttr()`` + * ``PySys_GetAttrString()`` + * ``PySys_GetOptionalAttr()`` + * ``PySys_GetOptionalAttrString()`` + * 2025-01-19: Add ``PyConfig_Get()`` functions. * 2025-01-06: Add ``Py_fopen()`` and ``Py_fclose()`` functions. * 2024-12-16: Add ``structmember.h`` constants: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 4b179e4..eb84307 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2199,6 +2199,71 @@ PyConfig_GetInt(const char *name, int *value) #endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030F0000 +static inline PyObject* +PySys_GetAttrString(const char *name) +{ +#if PY_VERSION_HEX >= 0x03000000 + PyObject *value = Py_XNewRef(PySys_GetObject(name)); +#else + PyObject *value = Py_XNewRef(PySys_GetObject((char*)name)); +#endif + if (value != NULL) { + return value; + } + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", name); + } + return NULL; +} + +static inline PyObject* +PySys_GetAttr(PyObject *name) +{ +#if PY_VERSION_HEX >= 0x03000000 + const char *name_str = PyUnicode_AsUTF8(name); +#else + const char *name_str = PyString_AsString(name); +#endif + if (name_str == NULL) { + return NULL; + } + + return PySys_GetAttrString(name_str); +} + +static inline int +PySys_GetOptionalAttrString(const char *name, PyObject **value) +{ +#if PY_VERSION_HEX >= 0x03000000 + *value = Py_XNewRef(PySys_GetObject(name)); +#else + *value = Py_XNewRef(PySys_GetObject((char*)name)); +#endif + if (*value != NULL) { + return 1; + } + return 0; +} + +static inline int +PySys_GetOptionalAttr(PyObject *name, PyObject **value) +{ +#if PY_VERSION_HEX >= 0x03000000 + const char *name_str = PyUnicode_AsUTF8(name); +#else + const char *name_str = PyString_AsString(name); +#endif + if (name_str == NULL) { + *value = NULL; + return -1; + } + + return PySys_GetOptionalAttrString(name_str, value); +} +#endif // PY_VERSION_HEX < 0x030F00A1 + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 9d324a4..82cf387 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2181,6 +2181,77 @@ test_config(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) #endif +static PyObject * +test_sys(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + const char *stdout_str = "stdout"; + PyObject *stdout_obj = create_string(stdout_str); +#if PYTHON3 + PyObject *sys_stdout = PySys_GetObject(stdout_str); // borrowed ref +#else + PyObject *sys_stdout = PySys_GetObject((char*)stdout_str); // borrowed ref +#endif + const char *nonexistent_str = "nonexistent"; + PyObject *nonexistent_obj = create_string(nonexistent_str); + PyObject *error_obj = PyLong_FromLong(1); + PyObject *value; + + // get sys.stdout + value = PySys_GetAttr(stdout_obj); + assert(value == sys_stdout); + Py_DECREF(value); + + value = PySys_GetAttrString(stdout_str); + assert(value == sys_stdout); + Py_DECREF(value); + + value = UNINITIALIZED_OBJ; + assert(PySys_GetOptionalAttr(stdout_obj, &value) == 1); + assert(value == sys_stdout); + Py_DECREF(value); + + value = UNINITIALIZED_OBJ; + assert(PySys_GetOptionalAttrString(stdout_str, &value) == 1); + assert(value == sys_stdout); + Py_DECREF(value); + + // non existent attribute + value = PySys_GetAttr(nonexistent_obj); + assert(value == NULL); + assert(PyErr_ExceptionMatches(PyExc_RuntimeError)); + PyErr_Clear(); + + value = PySys_GetAttrString(nonexistent_str); + assert(value == NULL); + assert(PyErr_ExceptionMatches(PyExc_RuntimeError)); + PyErr_Clear(); + + value = UNINITIALIZED_OBJ; + assert(PySys_GetOptionalAttr(nonexistent_obj, &value) == 0); + assert(value == NULL); + + value = UNINITIALIZED_OBJ; + assert(PySys_GetOptionalAttrString(nonexistent_str, &value) == 0); + assert(value == NULL); + + // invalid attribute type + value = PySys_GetAttr(error_obj); + assert(value == NULL); + assert(PyErr_ExceptionMatches(PyExc_TypeError)); + PyErr_Clear(); + + value = UNINITIALIZED_OBJ; + assert(PySys_GetOptionalAttr(error_obj, &value) == -1); + assert(value == NULL); + assert(PyErr_ExceptionMatches(PyExc_TypeError)); + PyErr_Clear(); + + Py_DECREF(stdout_obj); + Py_DECREF(nonexistent_obj); + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2232,6 +2303,7 @@ static struct PyMethodDef methods[] = { #if 0x03090000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) {"test_config", test_config, METH_NOARGS, _Py_NULL}, #endif + {"test_sys", test_sys, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From ffae0ffa2a3f906fd471e2d672731205f7e5febd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 9 Jun 2025 14:50:50 +0200 Subject: [PATCH 64/87] Add PyUnicodeWriter_WriteASCII() (#145) --- docs/api.rst | 4 ++++ docs/changelog.rst | 1 + pythoncapi_compat.h | 12 ++++++++++++ tests/test_pythoncapi_compat_cext.c | 7 ++++++- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 609edbd..6efae15 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -136,6 +136,10 @@ Python 3.14 See `PyUnicodeWriter_WriteUTF8() documentation `__. +.. c:function:: int PyUnicodeWriter_WriteASCII(PyUnicodeWriter *writer, const char *str, Py_ssize_t size) + + See `PyUnicodeWriter_WriteASCII() documentation `__. + .. c:function:: int PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, const wchar_t *str, Py_ssize_t size) See `PyUnicodeWriter_WriteWideChar() documentation `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index bb981c4..b8e3a86 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2025-06-09: Add ``PyUnicodeWriter_WriteASCII()`` function. * 2025-06-03: Add functions: * ``PySys_GetAttr()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index eb84307..20646af 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1456,6 +1456,18 @@ PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, return res; } +static inline int +PyUnicodeWriter_WriteASCII(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, + str, size); +} + static inline int PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, const wchar_t *str, Py_ssize_t size) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 82cf387..6e76316 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1847,6 +1847,11 @@ test_unicodewriter(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) goto error; } + // test PyUnicodeWriter_WriteASCII() + if (PyUnicodeWriter_WriteASCII(writer, " non-ASCII", -1) < 0) { + goto error; + } + // test PyUnicodeWriter_WriteUTF8() if (PyUnicodeWriter_WriteUTF8(writer, " valu\xC3\xA9", -1) < 0) { goto error; @@ -1870,7 +1875,7 @@ test_unicodewriter(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) if (result == NULL) { return NULL; } - assert(PyUnicode_EqualToUTF8(result, "var=long valu\xC3\xA9 'repr'")); + assert(PyUnicode_EqualToUTF8(result, "var=long non-ASCII valu\xC3\xA9 'repr'")); Py_DECREF(result); } From b541b98df1e3e5aabb5def27422a75c876f5a88a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 24 Jun 2025 11:40:12 +0200 Subject: [PATCH 65/87] Avoid %T format in error message (#146) The %T format was added to Python 3.13 (PEP 737). --- pythoncapi_compat.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 20646af..3320f68 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1491,7 +1491,8 @@ PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, Py_ssize_t start, Py_ssize_t end) { if (!PyUnicode_Check(str)) { - PyErr_Format(PyExc_TypeError, "expect str, not %T", str); + PyErr_Format(PyExc_TypeError, "expect str, not %s", + Py_TYPE(str)->tp_name); return -1; } if (start < 0 || start > end) { From 879f85df92ce23019216464866dffe39d56f4207 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 9 Aug 2025 19:04:06 +1000 Subject: [PATCH 66/87] Fixed typo (#148) --- upgrade_pythoncapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/upgrade_pythoncapi.py b/upgrade_pythoncapi.py index 68b5c53..4348e70 100755 --- a/upgrade_pythoncapi.py +++ b/upgrade_pythoncapi.py @@ -507,7 +507,7 @@ def __init__(self, args=None): self.operations = None self.applied_operations = set() - # Set temporariliy by patch() + # Set temporarily by patch() self._has_pythoncapi_compat = None self._applied_operations = None From 5e317108f872c904eb726cb8d560dcadbdf88a72 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 9 Aug 2025 19:05:14 +1000 Subject: [PATCH 67/87] Updated readthedocs to latest version of Python (#147) Use latest Ubuntu. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 735b67d..21e6b81 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,9 +4,9 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3.12" + python: "3" sphinx: configuration: docs/conf.py From 718e12ed7b2d13fd4651803c0284ce3c68eb9727 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 1 Sep 2025 08:15:41 -0600 Subject: [PATCH 68/87] Add CI builds for free-threaded Python versions (#150) Add missing error-checking in PyLong export tests. --- .github/workflows/build.yml | 6 ++++++ tests/test_pythoncapi_compat_cext.c | 29 ++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72d91db..6ffc74b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,9 +27,11 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.13t" # CPython 3.14 final is scheduled for October 2025: # https://peps.python.org/pep-0719/ - "3.14" + - "3.14t" # PyPy versions: # - https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md @@ -68,6 +70,8 @@ jobs: python: "3.12" - os: windows-latest python: "3.13" + - os: windows-latest + python: "3.13t" # macOS # Python 3.9 is the oldest version available on macOS/arm64. @@ -81,6 +85,8 @@ jobs: python: "3.12" - os: macos-latest python: "3.13" + - os: macos-latest + python: "3.13t" # Ubuntu: test deadsnakes Python versions which are not supported by # GHA python-versions. diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 6e76316..c719d42 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1432,29 +1432,48 @@ test_long_api(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) PyLongWriter *writer; static PyLongExport long_export; - writer = PyLongWriter_Create(1, 1, (void**)&digits); + writer = PyLongWriter_Create(1, 1, (void **)&digits); + if (writer == NULL) { + return NULL; + } PyLongWriter_Discard(writer); - writer = PyLongWriter_Create(1, 1, (void**)&digits); + writer = PyLongWriter_Create(1, 1, (void **)&digits); + if (writer == NULL) { + return NULL; + } digits[0] = 123; obj = PyLongWriter_Finish(writer); + if (obj == NULL) { + return NULL; + } check_int(obj, -123); - PyLong_Export(obj, &long_export); + if (PyLong_Export(obj, &long_export) < 0) { + return NULL; + } assert(long_export.value == -123); assert(long_export.digits == NULL); PyLong_FreeExport(&long_export); Py_DECREF(obj); - writer = PyLongWriter_Create(0, 5, (void**)&digits); + writer = PyLongWriter_Create(0, 5, (void **)&digits); + if (writer == NULL) { + return NULL; + } digits[0] = 1; digits[1] = 0; digits[2] = 0; digits[3] = 0; digits[4] = 1; obj = PyLongWriter_Finish(writer); + if (obj == NULL) { + return NULL; + } - PyLong_Export(obj, &long_export); + if (PyLong_Export(obj, &long_export) < 0) { + return NULL; + } assert(long_export.value == 0); digits = (digit*)long_export.digits; assert(digits[0] == 1); From 90c06a4cae557bdbfa4f231a781d2b5c1a8f6d1c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 1 Sep 2025 10:50:09 -0600 Subject: [PATCH 69/87] Add PyUnstable_Object_IsUniquelyReferenced (#149) --- docs/api.rst | 3 +++ docs/changelog.rst | 1 + pythoncapi_compat.h | 18 ++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 6efae15..ccd4d08 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -208,6 +208,9 @@ Python 3.14 See `PyConfig_GetInt() documentation `__. +.. c:function:: int PyUnstable_Object_IsUniquelyReferenced(PyObject *op) + + See `PyUnstable_Object_IsUniquelyReferenced() documentation `__. Not supported: diff --git a/docs/changelog.rst b/docs/changelog.rst index b8e3a86..fa9a35d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,7 @@ Changelog ========= +* 2025-09-01: Add ``PyUnstable_Object_IsUniquelyReferenced()`` function. * 2025-06-09: Add ``PyUnicodeWriter_WriteASCII()`` function. * 2025-06-03: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 3320f68..d14e90f 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2211,6 +2211,24 @@ PyConfig_GetInt(const char *name, int *value) } #endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) +// gh-133144 added PyUnstable_Object_IsUniquelyReferenced() to Python 3.14.0b1. +// Adapted from _PyObject_IsUniquelyReferenced() implementation. +#if PY_VERSION_HEX < 0x030E00B0 +static inline int PyUnstable_Object_IsUniquelyReferenced(PyObject *obj) +{ +#if !defined(Py_GIL_DISABLED) + return Py_REFCNT(obj) == 1; +#else + // NOTE: the entire ob_ref_shared field must be zero, including flags, to + // ensure that other threads cannot concurrently create new references to + // this object. + return (_Py_IsOwnedByCurrentThread(obj) && + _Py_atomic_load_uint32_relaxed(&obj->ob_ref_local) == 1 && + _Py_atomic_load_ssize_relaxed(&obj->ob_ref_shared) == 0); +#endif +} +#endif + #if PY_VERSION_HEX < 0x030F0000 static inline PyObject* diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index c719d42..31386d5 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1972,6 +1972,25 @@ test_unicodewriter_format(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) } #endif +static PyObject * +test_uniquely_referenced(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) +{ + PyObject *obj = Py_BuildValue("(s, s)", "hello", "world"); + if (obj == NULL) { + return NULL; + } + + assert(PyUnstable_Object_IsUniquelyReferenced(obj)); + + Py_INCREF(obj); + + assert(!PyUnstable_Object_IsUniquelyReferenced(obj)); + + Py_DECREF(obj); + Py_DECREF(obj); + + Py_RETURN_NONE; +} static PyObject * test_bytes(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) @@ -2328,6 +2347,7 @@ static struct PyMethodDef methods[] = { {"test_config", test_config, METH_NOARGS, _Py_NULL}, #endif {"test_sys", test_sys, METH_NOARGS, _Py_NULL}, + {"test_uniquely_referenced", test_uniquely_referenced, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From ab72af8b1a9adfccb3578eea8e9b6d5c6449f409 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 18 Sep 2025 17:45:42 +0100 Subject: [PATCH 70/87] PEP 782: Add PyBytesWriter C API (#139) --- docs/api.rst | 16 ++ docs/changelog.rst | 15 ++ pythoncapi_compat.h | 254 ++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 98 +++++++++++ 4 files changed, 383 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index ccd4d08..ac4f654 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -45,6 +45,22 @@ Python 3.15 See `PySys_GetOptionalAttrString() documentation `__. +.. c:function:: PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size) +.. c:function:: void* PyBytesWriter_GetData(PyBytesWriter *writer) +.. c:function:: Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer) +.. c:function:: PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: PyObject* PyBytesWriter_Finish(PyBytesWriter *writer) +.. c:function:: PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) +.. c:function:: void PyBytesWriter_Discard(PyBytesWriter *writer) +.. c:function:: int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf) +.. c:function:: int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size) +.. c:function:: int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) + + See `PyBytesWriter documentation `__. + + Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index fa9a35d..60a818c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,21 @@ Changelog ========= +* 2025-11-18: Add PEP 782 functions: + + * ``PyBytesWriter_Create()`` + * ``PyBytesWriter_Discard()`` + * ``PyBytesWriter_Finish()`` + * ``PyBytesWriter_FinishWithPointer()`` + * ``PyBytesWriter_FinishWithSize()`` + * ``PyBytesWriter_Format()`` + * ``PyBytesWriter_GetData()`` + * ``PyBytesWriter_GetSize()`` + * ``PyBytesWriter_Grow()`` + * ``PyBytesWriter_GrowAndUpdatePointer()`` + * ``PyBytesWriter_Resize()`` + * ``PyBytesWriter_WriteBytes()`` + * 2025-09-01: Add ``PyUnstable_Object_IsUniquelyReferenced()`` function. * 2025-06-09: Add ``PyUnicodeWriter_WriteASCII()`` function. * 2025-06-03: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index d14e90f..ecc445e 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2295,6 +2295,260 @@ PySys_GetOptionalAttr(PyObject *name, PyObject **value) #endif // PY_VERSION_HEX < 0x030F00A1 +#if PY_VERSION_HEX < 0x030F00A1 +typedef struct PyBytesWriter { + char small_buffer[256]; + PyObject *obj; + Py_ssize_t size; +} PyBytesWriter; + +static inline Py_ssize_t +_PyBytesWriter_GetAllocated(PyBytesWriter *writer) +{ + if (writer->obj == NULL) { + return sizeof(writer->small_buffer); + } + else { + return PyBytes_GET_SIZE(writer->obj); + } +} + + +static inline int +_PyBytesWriter_Resize_impl(PyBytesWriter *writer, Py_ssize_t size, + int resize) +{ + int overallocate = resize; + assert(size >= 0); + + if (size <= _PyBytesWriter_GetAllocated(writer)) { + return 0; + } + + if (overallocate) { +#ifdef MS_WINDOWS + /* On Windows, overallocate by 50% is the best factor */ + if (size <= (PY_SSIZE_T_MAX - size / 2)) { + size += size / 2; + } +#else + /* On Linux, overallocate by 25% is the best factor */ + if (size <= (PY_SSIZE_T_MAX - size / 4)) { + size += size / 4; + } +#endif + } + + if (writer->obj != NULL) { + if (_PyBytes_Resize(&writer->obj, size)) { + return -1; + } + assert(writer->obj != NULL); + } + else { + writer->obj = PyBytes_FromStringAndSize(NULL, size); + if (writer->obj == NULL) { + return -1; + } + + if (resize) { + assert((size_t)size > sizeof(writer->small_buffer)); + memcpy(PyBytes_AS_STRING(writer->obj), + writer->small_buffer, + sizeof(writer->small_buffer)); + } + } + return 0; +} + +static inline void* +PyBytesWriter_GetData(PyBytesWriter *writer) +{ + if (writer->obj == NULL) { + return writer->small_buffer; + } + else { + return PyBytes_AS_STRING(writer->obj); + } +} + +static inline Py_ssize_t +PyBytesWriter_GetSize(PyBytesWriter *writer) +{ + return writer->size; +} + +static inline void +PyBytesWriter_Discard(PyBytesWriter *writer) +{ + if (writer == NULL) { + return; + } + + Py_XDECREF(writer->obj); + PyMem_Free(writer); +} + +static inline PyBytesWriter* +PyBytesWriter_Create(Py_ssize_t size) +{ + if (size < 0) { + PyErr_SetString(PyExc_ValueError, "size must be >= 0"); + return NULL; + } + + PyBytesWriter *writer = (PyBytesWriter*)PyMem_Malloc(sizeof(PyBytesWriter)); + if (writer == NULL) { + PyErr_NoMemory(); + return NULL; + } + + writer->obj = NULL; + writer->size = 0; + + if (size >= 1) { + if (_PyBytesWriter_Resize_impl(writer, size, 0) < 0) { + PyBytesWriter_Discard(writer); + return NULL; + } + writer->size = size; + } + return writer; +} + +static inline PyObject* +PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) +{ + PyObject *result; + if (size == 0) { + result = PyBytes_FromStringAndSize("", 0); + } + else if (writer->obj != NULL) { + if (size != PyBytes_GET_SIZE(writer->obj)) { + if (_PyBytes_Resize(&writer->obj, size)) { + goto error; + } + } + result = writer->obj; + writer->obj = NULL; + } + else { + result = PyBytes_FromStringAndSize(writer->small_buffer, size); + } + PyBytesWriter_Discard(writer); + return result; + +error: + PyBytesWriter_Discard(writer); + return NULL; +} + +static inline PyObject* +PyBytesWriter_Finish(PyBytesWriter *writer) +{ + return PyBytesWriter_FinishWithSize(writer, writer->size); +} + +static inline PyObject* +PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) +{ + Py_ssize_t size = (char*)buf - (char*)PyBytesWriter_GetData(writer); + if (size < 0 || size > _PyBytesWriter_GetAllocated(writer)) { + PyBytesWriter_Discard(writer); + PyErr_SetString(PyExc_ValueError, "invalid end pointer"); + return NULL; + } + + return PyBytesWriter_FinishWithSize(writer, size); +} + +static inline int +PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) +{ + if (size < 0) { + PyErr_SetString(PyExc_ValueError, "size must be >= 0"); + return -1; + } + if (_PyBytesWriter_Resize_impl(writer, size, 1) < 0) { + return -1; + } + writer->size = size; + return 0; +} + +static inline int +PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size) +{ + if (size < 0 && writer->size + size < 0) { + PyErr_SetString(PyExc_ValueError, "invalid size"); + return -1; + } + if (size > PY_SSIZE_T_MAX - writer->size) { + PyErr_NoMemory(); + return -1; + } + size = writer->size + size; + + if (_PyBytesWriter_Resize_impl(writer, size, 1) < 0) { + return -1; + } + writer->size = size; + return 0; +} + +static inline void* +PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, + Py_ssize_t size, void *buf) +{ + Py_ssize_t pos = (char*)buf - (char*)PyBytesWriter_GetData(writer); + if (PyBytesWriter_Grow(writer, size) < 0) { + return NULL; + } + return (char*)PyBytesWriter_GetData(writer) + pos; +} + +static inline int +PyBytesWriter_WriteBytes(PyBytesWriter *writer, + const void *bytes, Py_ssize_t size) +{ + if (size < 0) { + size_t len = strlen((const char*)bytes); + if (len > (size_t)PY_SSIZE_T_MAX) { + PyErr_NoMemory(); + return -1; + } + size = (Py_ssize_t)len; + } + + Py_ssize_t pos = writer->size; + if (PyBytesWriter_Grow(writer, size) < 0) { + return -1; + } + char *buf = (char*)PyBytesWriter_GetData(writer); + memcpy(buf + pos, bytes, (size_t)size); + return 0; +} + +static inline int +PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyBytes_FromFormatV(format, vargs); + va_end(vargs); + + if (str == NULL) { + return -1; + } + int res = PyBytesWriter_WriteBytes(writer, + PyBytes_AS_STRING(str), + PyBytes_GET_SIZE(str)); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030F00A1 + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 31386d5..aa53ae0 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2295,6 +2295,103 @@ test_sys(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static int +test_byteswriter_highlevel(void) +{ + PyObject *obj; + PyBytesWriter *writer = PyBytesWriter_Create(0); + if (writer == NULL) { + goto error; + } + if (PyBytesWriter_WriteBytes(writer, "Hello", -1) < 0) { + goto error; + } + if (PyBytesWriter_Format(writer, " %s!", "World") < 0) { + goto error; + } + + obj = PyBytesWriter_Finish(writer); + if (obj == NULL) { + return -1; + } + assert(PyBytes_Check(obj)); + assert(strcmp(PyBytes_AS_STRING(obj), "Hello World!") == 0); + Py_DECREF(obj); + return 0; + +error: + PyBytesWriter_Discard(writer); + return -1; +} + +static int +test_byteswriter_abc(void) +{ + PyBytesWriter *writer = PyBytesWriter_Create(3); + if (writer == NULL) { + return -1; + } + + char *str = (char*)PyBytesWriter_GetData(writer); + memcpy(str, "abc", 3); + + PyObject *obj = PyBytesWriter_Finish(writer); + if (obj == NULL) { + return -1; + } + assert(PyBytes_Check(obj)); + assert(strcmp(PyBytes_AS_STRING(obj), "abc") == 0); + Py_DECREF(obj); + return 0; +} + +static int +test_byteswriter_grow(void) +{ + PyBytesWriter *writer = PyBytesWriter_Create(10); + if (writer == NULL) { + return -1; + } + + char *buf = (char*)PyBytesWriter_GetData(writer); + memcpy(buf, "Hello ", strlen("Hello ")); + buf += strlen("Hello "); + + buf = (char*)PyBytesWriter_GrowAndUpdatePointer(writer, 10, buf); + if (buf == NULL) { + PyBytesWriter_Discard(writer); + return -1; + } + + memcpy(buf, "World", strlen("World")); + buf += strlen("World"); + + PyObject *obj = PyBytesWriter_FinishWithPointer(writer, buf); + if (obj == NULL) { + return -1; + } + assert(PyBytes_Check(obj)); + assert(strcmp(PyBytes_AS_STRING(obj), "Hello World") == 0); + Py_DECREF(obj); + return 0; +} + +static PyObject * +test_byteswriter(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + if (test_byteswriter_highlevel() < 0) { + return NULL; + } + if (test_byteswriter_abc() < 0) { + return NULL; + } + if (test_byteswriter_grow() < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2348,6 +2445,7 @@ static struct PyMethodDef methods[] = { #endif {"test_sys", test_sys, METH_NOARGS, _Py_NULL}, {"test_uniquely_referenced", test_uniquely_referenced, METH_NOARGS, _Py_NULL}, + {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From c3c63ee663874b17edd931b97076c92d5941aab5 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Thu, 2 Oct 2025 20:37:25 +0200 Subject: [PATCH 71/87] Add meson.build file (#151) This makes it easier to integrate pythoncapi-compat in Meson based projects. --- meson.build | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 meson.build diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..7c5f9fc --- /dev/null +++ b/meson.build @@ -0,0 +1,6 @@ +project( + 'pythoncapi-compat', + 'c' +) + +incdir = include_directories('.') From 97f1582fc44c3912934157a726fb6f44efd7cec9 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 3 Oct 2025 11:25:04 +0200 Subject: [PATCH 72/87] Fix compiler warnings for PyBytesWriter_Format (#152) --- pythoncapi_compat.h | 4 ++++ tests/setup.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index ecc445e..791d511 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2529,6 +2529,10 @@ PyBytesWriter_WriteBytes(PyBytesWriter *writer, return 0; } +static inline int +PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) + Py_GCC_ATTRIBUTE((format(printf, 2, 0))); + static inline int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) { diff --git a/tests/setup.py b/tests/setup.py index 825241e..33a0e66 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -34,7 +34,11 @@ '-Wconversion', # /usr/lib64/pypy3.7/include/pyport.h:68:20: error: redefinition of typedef # 'Py_hash_t' is a C11 feature - "-Wno-typedef-redefinition", + '-Wno-typedef-redefinition', + # Formatting checks + '-Wformat', + '-Wformat-nonliteral', + '-Wformat-security', )) CFLAGS = COMMON_FLAGS + [ # Use C99 for pythoncapi_compat.c which initializes PyModuleDef with a From 22811c3f0e69908894d2bd724f572b32667f2141 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 3 Oct 2025 22:28:58 +0200 Subject: [PATCH 73/87] Fix varargs indicator for PyBytesWriter_Format (#153) --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 791d511..6a7037e 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2531,7 +2531,7 @@ PyBytesWriter_WriteBytes(PyBytesWriter *writer, static inline int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) - Py_GCC_ATTRIBUTE((format(printf, 2, 0))); + Py_GCC_ATTRIBUTE((format(printf, 2, 3))); static inline int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) From 89e023ee32195e89ab557c8da473dcf940921d60 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 15:41:22 +0200 Subject: [PATCH 74/87] Add PyTuple_FromArray() function (#154) --- docs/api.rst | 30 ++++++++++-------- docs/changelog.rst | 3 +- pythoncapi_compat.h | 17 ++++++++++ tests/test_pythoncapi_compat_cext.c | 49 +++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 14 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index ac4f654..de80e3d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -29,6 +29,21 @@ Latest version of the header file: Python 3.15 ----------- +.. c:function:: PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size) +.. c:function:: void* PyBytesWriter_GetData(PyBytesWriter *writer) +.. c:function:: Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer) +.. c:function:: PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: PyObject* PyBytesWriter_Finish(PyBytesWriter *writer) +.. c:function:: PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) +.. c:function:: void PyBytesWriter_Discard(PyBytesWriter *writer) +.. c:function:: int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size) +.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf) +.. c:function:: int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size) +.. c:function:: int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) + + See `PyBytesWriter documentation `__. + .. c:function:: PyObject* PySys_GetAttr(const char *name) See `PySys_GetAttr() documentation `__. @@ -45,20 +60,9 @@ Python 3.15 See `PySys_GetOptionalAttrString() documentation `__. -.. c:function:: PyBytesWriter* PyBytesWriter_Create(Py_ssize_t size) -.. c:function:: void* PyBytesWriter_GetData(PyBytesWriter *writer) -.. c:function:: Py_ssize_t PyBytesWriter_GetSize(PyBytesWriter *writer) -.. c:function:: PyObject* PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) -.. c:function:: PyObject* PyBytesWriter_Finish(PyBytesWriter *writer) -.. c:function:: PyObject* PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf) -.. c:function:: void PyBytesWriter_Discard(PyBytesWriter *writer) -.. c:function:: int PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size) -.. c:function:: int PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size) -.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf) -.. c:function:: int PyBytesWriter_WriteBytes(PyBytesWriter *writer, const void *bytes, Py_ssize_t size) -.. c:function:: int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) +.. c:function:: PyObject* PyTuple_FromArray(PyObject *const *array, Py_ssize_t size) - See `PyBytesWriter documentation `__. + See `PyTuple_FromArray() documentation `__. Python 3.14 diff --git a/docs/changelog.rst b/docs/changelog.rst index 60a818c..2d84389 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,8 @@ Changelog ========= -* 2025-11-18: Add PEP 782 functions: +* 2025-10-14: Add ``PyTuple_FromArray()`` function. +* 2025-09-18: Add PEP 782 functions: * ``PyBytesWriter_Create()`` * ``PyBytesWriter_Discard()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 6a7037e..55b2dbd 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2553,6 +2553,23 @@ PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) #endif // PY_VERSION_HEX < 0x030F00A1 +#if PY_VERSION_HEX < 0x030F00A1 +static inline PyObject* +PyTuple_FromArray(PyObject *const *array, Py_ssize_t size) +{ + PyObject *tuple = PyTuple_New(size); + if (tuple == NULL) { + return NULL; + } + for (Py_ssize_t i=0; i < size; i++) { + PyObject *item = array[i]; + PyTuple_SET_ITEM(tuple, i, Py_NewRef(item)); + } + return tuple; +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index aa53ae0..25c7499 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2392,6 +2392,54 @@ test_byteswriter(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } +static PyObject* +test_tuple_fromarray(void) +{ + PyObject* array[] = { + PyLong_FromLong(1), + PyLong_FromLong(2), + PyLong_FromLong(3) + }; + PyObject *tuple = PyTuple_FromArray(array, 3); + if (tuple == NULL) { + goto error; + } + + assert(PyTuple_GET_SIZE(tuple) == 3); + assert(PyTuple_GET_ITEM(tuple, 0) == array[0]); + assert(PyTuple_GET_ITEM(tuple, 1) == array[1]); + assert(PyTuple_GET_ITEM(tuple, 2) == array[2]); + + Py_DECREF(tuple); + Py_DECREF(array[0]); + Py_DECREF(array[1]); + Py_DECREF(array[2]); + + // Test PyTuple_FromArray(NULL, 0) + tuple = PyTuple_FromArray(NULL, 0); + if (tuple == NULL) { + return NULL; + } + assert(PyTuple_GET_SIZE(tuple) == 0); + Py_DECREF(tuple); + + Py_RETURN_NONE; + +error: + Py_DECREF(array[0]); + Py_DECREF(array[1]); + Py_DECREF(array[2]); + return NULL; +} + + +static PyObject* +test_tuple(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + return test_tuple_fromarray(); +} + + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, {"test_py_is", test_py_is, METH_NOARGS, _Py_NULL}, @@ -2446,6 +2494,7 @@ static struct PyMethodDef methods[] = { {"test_sys", test_sys, METH_NOARGS, _Py_NULL}, {"test_uniquely_referenced", test_uniquely_referenced, METH_NOARGS, _Py_NULL}, {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, + {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From a49e0fbef4ead30c7f803989d994e9b22dded4a1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 16:05:13 +0200 Subject: [PATCH 75/87] Add PyUnstable_Unicode_GET_CACHED_HASH() function (#155) --- docs/api.rst | 6 ++++++ docs/changelog.rst | 6 +++++- pythoncapi_compat.h | 13 +++++++++++++ tests/test_pythoncapi_compat_cext.c | 7 +++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index de80e3d..7b64c64 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -64,6 +64,12 @@ Python 3.15 See `PyTuple_FromArray() documentation `__. +.. c:function:: Py_hash_t PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) + + See `PyUnstable_Unicode_GET_CACHED_HASH() documentation `__. + + Not available on PyPy. + Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 2d84389..007a665 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,11 @@ Changelog ========= -* 2025-10-14: Add ``PyTuple_FromArray()`` function. +* 2025-10-14: Add functions: + + * ``PyTuple_FromArray()`` + * ``PyUnstable_Unicode_GET_CACHED_HASH()`` + * 2025-09-18: Add PEP 782 functions: * ``PyBytesWriter_Create()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 55b2dbd..85c6cd5 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2570,6 +2570,19 @@ PyTuple_FromArray(PyObject *const *array, Py_ssize_t size) #endif +#if PY_VERSION_HEX < 0x030F00A1 && !defined(PYPY_VERSION) +static inline Py_hash_t +PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) +{ +#if PY_VERSION_HEX >= 0x03000000 + return ((PyASCIIObject*)op)->hash; +#else + return ((PyUnicodeObject*)op)->hash; +#endif +} +#endif + + #ifdef __cplusplus } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 25c7499..dd8c54b 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1611,6 +1611,13 @@ test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyErr_ExceptionMatches(PyExc_TypeError)); PyErr_Clear(); +#ifndef PYPY_VERSION + // Test PyUnstable_Unicode_GET_CACHED_HASH() + Py_hash_t hash = PyObject_Hash(abc); + assert(hash != -1); + assert(PyUnstable_Unicode_GET_CACHED_HASH(abc) == hash); +#endif + Py_DECREF(abc); Py_DECREF(abc0def); Py_RETURN_NONE; From 6c77b6b2809d897fe5a887358f7d1cfe12353bef Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 20:01:04 +0200 Subject: [PATCH 76/87] PyConfig_Get() is also available on Python 3.8 (#156) --- pythoncapi_compat.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 85c6cd5..96d462e 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1991,7 +1991,7 @@ static inline int Py_fclose(FILE *file) #endif -#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +#if 0x03080000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) static inline PyObject* PyConfig_Get(const char *name) { @@ -2032,7 +2032,9 @@ PyConfig_Get(const char *name) PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), +#if 0x03090000 <= PY_VERSION_HEX PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), +#endif PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), From e510a7b0bae5452b7f4d130f6f638565f18f8dc9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 15 Oct 2025 13:27:47 +0200 Subject: [PATCH 77/87] Make PyUnstable_Unicode_GET_CACHED_HASH return -1 on PyPy (#157) Co-authored-by: Victor Stinner --- docs/api.rst | 2 +- pythoncapi_compat.h | 7 +++++-- tests/test_pythoncapi_compat_cext.c | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7b64c64..3f30e53 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -68,7 +68,7 @@ Python 3.15 See `PyUnstable_Unicode_GET_CACHED_HASH() documentation `__. - Not available on PyPy. + On PyPy, always returns ``-1``. Python 3.14 diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 96d462e..b16075f 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2572,11 +2572,14 @@ PyTuple_FromArray(PyObject *const *array, Py_ssize_t size) #endif -#if PY_VERSION_HEX < 0x030F00A1 && !defined(PYPY_VERSION) +#if PY_VERSION_HEX < 0x030F00A1 static inline Py_hash_t PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) { -#if PY_VERSION_HEX >= 0x03000000 +#ifdef PYPY_VERSION + (void)op; // unused argument + return -1; +#elif PY_VERSION_HEX >= 0x03000000 return ((PyASCIIObject*)op)->hash; #else return ((PyUnicodeObject*)op)->hash; diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index dd8c54b..fed0822 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1611,8 +1611,10 @@ test_unicode(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) assert(PyErr_ExceptionMatches(PyExc_TypeError)); PyErr_Clear(); -#ifndef PYPY_VERSION // Test PyUnstable_Unicode_GET_CACHED_HASH() +#ifdef PYPY_VERSION + assert(PyUnstable_Unicode_GET_CACHED_HASH(abc) == -1); +#else Py_hash_t hash = PyObject_Hash(abc); assert(hash != -1); assert(PyUnstable_Unicode_GET_CACHED_HASH(abc) == hash); From c44469cc02bc857de286f9043b677d545c2030b7 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Nov 2025 17:07:19 -0500 Subject: [PATCH 78/87] Add PyUnstable_TryIncref() and PyUnstable_EnableTryIncRef() (#159) Co-authored-by: Victor Stinner --- pythoncapi_compat.h | 75 +++++++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 48 ++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index b16075f..04378bd 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2231,6 +2231,81 @@ static inline int PyUnstable_Object_IsUniquelyReferenced(PyObject *obj) } #endif +// gh-128926 added PyUnstable_TryIncRef() and PyUnstable_EnableTryIncRef() to +// Python 3.14.0a5. Adapted from _Py_TryIncref() and _PyObject_SetMaybeWeakref(). +#if PY_VERSION_HEX < 0x030E00A5 +static inline int PyUnstable_TryIncRef(PyObject *op) +{ +#ifndef Py_GIL_DISABLED + if (Py_REFCNT(op) > 0) { + Py_INCREF(op); + return 1; + } + return 0; +#else + // _Py_TryIncrefFast() + uint32_t local = _Py_atomic_load_uint32_relaxed(&op->ob_ref_local); + local += 1; + if (local == 0) { + // immortal + return 1; + } + if (_Py_IsOwnedByCurrentThread(op)) { + _Py_INCREF_STAT_INC(); + _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, local); +#ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +#endif + return 1; + } + + // _Py_TryIncRefShared() + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); + for (;;) { + // If the shared refcount is zero and the object is either merged + // or may not have weak references, then we cannot incref it. + if (shared == 0 || shared == _Py_REF_MERGED) { + return 0; + } + + if (_Py_atomic_compare_exchange_ssize( + &op->ob_ref_shared, + &shared, + shared + (1 << _Py_REF_SHARED_SHIFT))) { +#ifdef Py_REF_DEBUG + _Py_INCREF_IncRefTotal(); +#endif + _Py_INCREF_STAT_INC(); + return 1; + } + } +#endif +} + +static inline void PyUnstable_EnableTryIncRef(PyObject *op) +{ +#ifdef Py_GIL_DISABLED + // _PyObject_SetMaybeWeakref() + if (_Py_IsImmortal(op)) { + return; + } + for (;;) { + Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); + if ((shared & _Py_REF_SHARED_FLAG_MASK) != 0) { + // Nothing to do if it's in WEAKREFS, QUEUED, or MERGED states. + return; + } + if (_Py_atomic_compare_exchange_ssize( + &op->ob_ref_shared, &shared, shared | _Py_REF_MAYBE_WEAKREF)) { + return; + } + } +#else + (void)op; // unused argument +#endif +} +#endif + #if PY_VERSION_HEX < 0x030F0000 static inline PyObject* diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index fed0822..7d9fea4 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2448,6 +2448,46 @@ test_tuple(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) return test_tuple_fromarray(); } +// Test adapted from CPython's _testcapi/object.c +static int TryIncref_dealloc_called = 0; + +static void +TryIncref_dealloc(PyObject *op) +{ + // PyUnstable_TryIncRef should return 0 if object is being deallocated + assert(Py_REFCNT(op) == 0); + assert(!PyUnstable_TryIncRef(op)); + assert(Py_REFCNT(op) == 0); + + TryIncref_dealloc_called++; + Py_TYPE(op)->tp_free(op); +} + +static PyTypeObject TryIncrefType; + +static PyObject* +test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + TryIncref_dealloc_called = 0; + + PyObject *obj = PyObject_New(PyObject, &TryIncrefType); + if (obj == _Py_NULL) { + return _Py_NULL; + } + + PyUnstable_EnableTryIncRef(obj); + + Py_ssize_t refcount = Py_REFCNT(obj); + assert(PyUnstable_TryIncRef(obj)); + assert(Py_REFCNT(obj) == refcount + 1); + + Py_DECREF(obj); + Py_DECREF(obj); + + assert(TryIncref_dealloc_called == 1); + Py_RETURN_NONE; +} + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, @@ -2504,6 +2544,7 @@ static struct PyMethodDef methods[] = { {"test_uniquely_referenced", test_uniquely_referenced, METH_NOARGS, _Py_NULL}, {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, + {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; @@ -2532,6 +2573,13 @@ module_exec(PyObject *module) return -1; } #endif + TryIncrefType.tp_name = "TryIncrefType"; + TryIncrefType.tp_basicsize = sizeof(PyObject); + TryIncrefType.tp_dealloc = TryIncref_dealloc; + TryIncrefType.tp_free = PyObject_Del; + if (PyType_Ready(&TryIncrefType) < 0) { + return -1; + } return 0; } From 44c8e14bbbb5d5135ae90957036a61397e4df577 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 5 Nov 2025 18:09:53 -0500 Subject: [PATCH 79/87] Remove trailing whitespace (#160) --- pythoncapi_compat.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 04378bd..bb45c18 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2258,7 +2258,7 @@ static inline int PyUnstable_TryIncRef(PyObject *op) #endif return 1; } - + // _Py_TryIncRefShared() Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared); for (;;) { From e3efede9d842e5b11debdc732aca1192d833fd5b Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 7 Nov 2025 15:17:32 -0500 Subject: [PATCH 80/87] Avoid "most vexing parse" warning in certain versions of NVCC (#162) --- pythoncapi_compat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index bb45c18..02a0171 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1992,6 +1992,8 @@ static inline int Py_fclose(FILE *file) #if 0x03080000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + static inline PyObject* PyConfig_Get(const char *name) { @@ -2127,8 +2129,6 @@ PyConfig_Get(const char *name) return Py_NewRef(value); } - PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); - const PyConfig *config = _Py_GetConfig(); void *member = (char *)config + spec->offset; switch (spec->type) { From 11cb80f2652cb2fe5231bf60b9dd98c83a4e25f4 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Sat, 8 Nov 2025 09:49:40 -0500 Subject: [PATCH 81/87] Don't include structmember.h in pythoncapi_compat.h (#161) Avoids conflicts due to names without "Py" prefixes. --- pythoncapi_compat.h | 49 ++++++++++++++--------------- tests/test_pythoncapi_compat_cext.c | 49 +++++++++++++++-------------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 02a0171..cdfdafa 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -25,9 +25,6 @@ extern "C" { #if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) # include "frameobject.h" // PyFrameObject, PyFrame_GetBack() #endif -#if PY_VERSION_HEX < 0x030C00A3 -# include // T_SHORT, READONLY -#endif #ifndef _Py_CAST @@ -1919,33 +1916,33 @@ PyLongWriter_Finish(PyLongWriter *writer) #if PY_VERSION_HEX < 0x030C00A3 -# define Py_T_SHORT T_SHORT -# define Py_T_INT T_INT -# define Py_T_LONG T_LONG -# define Py_T_FLOAT T_FLOAT -# define Py_T_DOUBLE T_DOUBLE -# define Py_T_STRING T_STRING -# define _Py_T_OBJECT T_OBJECT -# define Py_T_CHAR T_CHAR -# define Py_T_BYTE T_BYTE -# define Py_T_UBYTE T_UBYTE -# define Py_T_USHORT T_USHORT -# define Py_T_UINT T_UINT -# define Py_T_ULONG T_ULONG -# define Py_T_STRING_INPLACE T_STRING_INPLACE -# define Py_T_BOOL T_BOOL -# define Py_T_OBJECT_EX T_OBJECT_EX -# define Py_T_LONGLONG T_LONGLONG -# define Py_T_ULONGLONG T_ULONGLONG -# define Py_T_PYSSIZET T_PYSSIZET +# define Py_T_SHORT 0 +# define Py_T_INT 1 +# define Py_T_LONG 2 +# define Py_T_FLOAT 3 +# define Py_T_DOUBLE 4 +# define Py_T_STRING 5 +# define _Py_T_OBJECT 6 +# define Py_T_CHAR 7 +# define Py_T_BYTE 8 +# define Py_T_UBYTE 9 +# define Py_T_USHORT 10 +# define Py_T_UINT 11 +# define Py_T_ULONG 12 +# define Py_T_STRING_INPLACE 13 +# define Py_T_BOOL 14 +# define Py_T_OBJECT_EX 16 +# define Py_T_LONGLONG 17 +# define Py_T_ULONGLONG 18 +# define Py_T_PYSSIZET 19 # if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) -# define _Py_T_NONE T_NONE +# define _Py_T_NONE 20 # endif -# define Py_READONLY READONLY -# define Py_AUDIT_READ READ_RESTRICTED -# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +# define Py_READONLY 1 +# define Py_AUDIT_READ 2 +# define _Py_WRITE_RESTRICTED 4 #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 7d9fea4..e8ada23 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2,6 +2,7 @@ #undef NDEBUG #include "pythoncapi_compat.h" +#include // T_SHORT, READONLY #ifdef NDEBUG # error "assertions must be enabled" @@ -2118,32 +2119,32 @@ test_long_stdint(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) static PyObject * test_structmember(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { - assert(Py_T_SHORT >= 0); - assert(Py_T_INT >= 0); - assert(Py_T_LONG >= 0); - assert(Py_T_FLOAT >= 0); - assert(Py_T_DOUBLE >= 0); - assert(Py_T_STRING >= 0); - assert(_Py_T_OBJECT >= 0); - assert(Py_T_CHAR >= 0); - assert(Py_T_BYTE >= 0); - assert(Py_T_UBYTE >= 0); - assert(Py_T_USHORT >= 0); - assert(Py_T_UINT >= 0); - assert(Py_T_ULONG >= 0); - assert(Py_T_STRING_INPLACE >= 0); - assert(Py_T_BOOL >= 0); - assert(Py_T_OBJECT_EX >= 0); - assert(Py_T_LONGLONG >= 0); - assert(Py_T_ULONGLONG >= 0); - assert(Py_T_PYSSIZET >= 0); + assert(Py_T_SHORT == T_SHORT); + assert(Py_T_SHORT == T_SHORT); + assert(Py_T_INT == T_INT); + assert(Py_T_LONG == T_LONG); + assert(Py_T_FLOAT == T_FLOAT); + assert(Py_T_DOUBLE == T_DOUBLE); + assert(Py_T_STRING == T_STRING); + assert(_Py_T_OBJECT == T_OBJECT); + assert(Py_T_CHAR == T_CHAR); + assert(Py_T_BYTE == T_BYTE); + assert(Py_T_UBYTE == T_UBYTE); + assert(Py_T_USHORT == T_USHORT); + assert(Py_T_UINT == T_UINT); + assert(Py_T_ULONG == T_ULONG); + assert(Py_T_STRING_INPLACE == T_STRING_INPLACE); + assert(Py_T_BOOL == T_BOOL); + assert(Py_T_OBJECT_EX == T_OBJECT_EX); + assert(Py_T_LONGLONG == T_LONGLONG); + assert(Py_T_ULONGLONG == T_ULONGLONG); + assert(Py_T_PYSSIZET == T_PYSSIZET); #if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) - assert(_Py_T_NONE >= 0); + assert(_Py_T_NONE == T_NONE); #endif - - assert(Py_READONLY >= 0); - assert(Py_AUDIT_READ >= 0); - assert(_Py_WRITE_RESTRICTED >= 0); + assert(Py_READONLY == READONLY); + assert(Py_AUDIT_READ == READ_RESTRICTED); + assert(_Py_WRITE_RESTRICTED == PY_WRITE_RESTRICTED); Py_RETURN_NONE; } From 8636bccf29adfa23463f810b3c2830f7cff1e933 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 19:28:12 +0530 Subject: [PATCH 82/87] Add PyUnstable_SetImmortal() (#164) Co-authored-by: Victor Stinner --- docs/api.rst | 5 ++++ docs/changelog.rst | 4 ++++ pythoncapi_compat.h | 31 +++++++++++++++++++++++++ tests/test_pythoncapi_compat_cext.c | 36 +++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 3f30e53..2b61a8d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -70,6 +70,11 @@ Python 3.15 On PyPy, always returns ``-1``. +.. c:function:: int PyUnstable_SetImmortal(PyObject *op) + + See `PyUnstable_SetImmortal() documentation `__. + + Availability: Python 3.13 and newer, not available on PyPy. Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 007a665..c68d88b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +* 2026-02-12: Add functions: + + * ``PyUnstable_SetImmortal()`` + * 2025-10-14: Add functions: * ``PyTuple_FromArray()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index cdfdafa..e26185c 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2659,6 +2659,37 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) } #endif +#if 0x030D0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +// Immortal objects were implemented in Python 3.12, however there is no easy API +// to make objects immortal until 3.14 which has _Py_SetImmortal(). Since +// immortal objects are primarily needed for free-threading, this API is implemented +// for 3.14 using _Py_SetImmortal() and uses private macros on 3.13. +static inline int +PyUnstable_SetImmortal(PyObject *op) +{ + assert(op != NULL); + if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { + return 0; + } +#if 0x030E0000 <= PY_VERSION_HEX + PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); + _Py_SetImmortal(op); +#else + // Python 3.13 doesn't export _Py_SetImmortal() function + if (PyObject_GC_IsTracked(op)) { + PyObject_GC_UnTrack(op); + } +#ifdef Py_GIL_DISABLED + op->ob_tid = _Py_UNOWNED_TID; + op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL; + op->ob_ref_shared = 0; +#else + op->ob_refcnt = _Py_IMMORTAL_REFCNT; +#endif +#endif + return 1; +} +#endif #ifdef __cplusplus } diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index e8ada23..6fd60b2 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2489,6 +2489,39 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +static PyObject * +test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject object; + memset(&object, 0, sizeof(PyObject)); +#ifdef Py_GIL_DISABLED + object.ob_tid = _Py_ThreadId(); + object.ob_gc_bits = 0; + object.ob_ref_local = 1; + object.ob_ref_shared = 0; +#else + object.ob_refcnt = 1; +#endif + object.ob_type = &PyBaseObject_Type; + + int rc = PyUnstable_SetImmortal(&object); + assert(rc == 1); + Py_DECREF(&object); // should not dealloc + + // Check already immortal object + rc = PyUnstable_SetImmortal(&object); + assert(rc == 0); + + // Check unicode objects + PyObject *unicode = PyUnicode_FromString("test"); + rc = PyUnstable_SetImmortal(unicode); + assert(rc == 0); + Py_DECREF(unicode); + Py_RETURN_NONE; +} +#endif + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, @@ -2546,6 +2579,9 @@ static struct PyMethodDef methods[] = { {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 442b4828222167a19ef2920d3ed2815af334cf47 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Feb 2026 15:15:49 +0100 Subject: [PATCH 83/87] Run tests on Python 3.14 and 3.15 (#165) --- .github/workflows/build.yml | 13 +++++++++++-- docs/api.rst | 2 +- runtests.py | 5 +++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ffc74b..8f8b429 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,10 +28,11 @@ jobs: - "3.12" - "3.13" - "3.13t" - # CPython 3.14 final is scheduled for October 2025: - # https://peps.python.org/pep-0719/ - "3.14" - "3.14t" + # CPython 3.15 final is scheduled for October 2026: + # https://peps.python.org/pep-0790/ + - "3.15-dev" # PyPy versions: # - https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md @@ -72,6 +73,10 @@ jobs: python: "3.13" - os: windows-latest python: "3.13t" + - os: windows-latest + python: "3.14" + - os: windows-latest + python: "3.14t" # macOS # Python 3.9 is the oldest version available on macOS/arm64. @@ -87,6 +92,10 @@ jobs: python: "3.13" - os: macos-latest python: "3.13t" + - os: macos-latest + python: "3.14" + - os: macos-latest + python: "3.14t" # Ubuntu: test deadsnakes Python versions which are not supported by # GHA python-versions. diff --git a/docs/api.rst b/docs/api.rst index 2b61a8d..a8c4c6f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,7 +7,7 @@ functions for old Python versions. Supported Python versions: -* Python 3.6 - 3.14 +* Python 3.6 - 3.15 * PyPy 2.7 and PyPy 3.6 - 3.10 Python 2.7 and Python 3.5 are no longer officially supported since GitHub diff --git a/runtests.py b/runtests.py index c858516..fdee19e 100755 --- a/runtests.py +++ b/runtests.py @@ -41,6 +41,11 @@ "python3.11", "python3.12", "python3.13", + "python3.13t", + "python3.14", + "python3.14t", + "python3.15", + "python3.15t", # PyPy "pypy", From 5fc66531810bc915a8f444168ddbb2306294fea9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Feb 2026 15:43:44 +0100 Subject: [PATCH 84/87] Fix tests on python3.14t (#166) * Add Py_MOD_GIL_NOT_USED to the C/C++ extension. * Add "t" suffix to the Python version on free-threaded build. * On Python 3.14 and newer, call sys._clear_internal_caches() instead of sys._clear_type_cache() to avoid a deprecation warning. --- tests/test_pythoncapi_compat.py | 24 +++++++++++++++++++----- tests/test_pythoncapi_compat_cext.c | 3 +++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/test_pythoncapi_compat.py b/tests/test_pythoncapi_compat.py index 8480415..47ffd9b 100644 --- a/tests/test_pythoncapi_compat.py +++ b/tests/test_pythoncapi_compat.py @@ -19,6 +19,11 @@ except ImportError: # Python 2 faulthandler = None +try: + import sysconfig +except ImportError: + # Python 3.1 and older + sysconfig = None # test.utils from utils import run_command, command_stdout @@ -49,9 +54,7 @@ def display_title(title): if not VERBOSE: return - ver = sys.version_info - title = "Python %s.%s: %s" % (ver.major, ver.minor, title) - + title = "%s: %s" % (python_version(), title) print(title) print("=" * len(title)) print() @@ -94,10 +97,13 @@ def _run_tests(tests, verbose): test_func() +_HAS_CLEAR_INTERNAL_CACHES = hasattr(sys, '_clear_internal_caches') _HAS_CLEAR_TYPE_CACHE = hasattr(sys, '_clear_type_cache') def _refleak_cleanup(): - if _HAS_CLEAR_TYPE_CACHE: + if _HAS_CLEAR_INTERNAL_CACHES: + sys._clear_internal_caches() + elif _HAS_CLEAR_TYPE_CACHE: sys._clear_type_cache() gc.collect() @@ -134,10 +140,18 @@ def python_version(): python_impl = "PyPy" else: python_impl = 'Python' - return "%s %s.%s (%s build)" % (python_impl, ver.major, ver.minor, build) + pyver = "%s.%s" % (ver.major, ver.minor) + if ver >= (3, 13): + if sysconfig.get_config_var('Py_GIL_DISABLED'): + # Free-threaded build + pyver += "t" + return "%s %s (%s build)" % (python_impl, pyver, build) def run_tests(module_name, lang): + if VERBOSE: + print("") + title = "Test %s (%s)" % (module_name, lang) display_title(title) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 6fd60b2..132708d 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2624,6 +2624,9 @@ module_exec(PyObject *module) #if PY_VERSION_HEX >= 0x03050000 static PyModuleDef_Slot module_slots[] = { {Py_mod_exec, _Py_CAST(void*, module_exec)}, +#if PY_VERSION_HEX >= 0x030D0000 + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif {0, _Py_NULL} }; #endif From 6290c7b190eb5c16ffb83ba46ade8852969c99eb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Feb 2026 16:33:01 +0100 Subject: [PATCH 85/87] Tests C11, C++17 and C++20 (#167) --- tests/setup.py | 57 +++++++++++++++++------------ tests/test_pythoncapi_compat.py | 51 ++++++++++++++------------ tests/test_pythoncapi_compat_cext.c | 14 ++++++- 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/tests/setup.py b/tests/setup.py index 33a0e66..e394bce 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -40,11 +40,7 @@ '-Wformat-nonliteral', '-Wformat-security', )) - CFLAGS = COMMON_FLAGS + [ - # Use C99 for pythoncapi_compat.c which initializes PyModuleDef with a - # mixture of designated and non-designated initializers - '-std=c99', - ] + CFLAGS = COMMON_FLAGS else: # C compiler flags for MSVC COMMON_FLAGS.extend(( @@ -54,6 +50,27 @@ CFLAGS = list(COMMON_FLAGS) CXXFLAGS = list(COMMON_FLAGS) +if not MSVC: + C_VERSIONS = ('c99', 'c11') +else: + # MSVC doesn't support /std:c99 flag + C_VERSIONS = ('c11',) + +if not MSVC: + CXX_VERSIONS = [ + ('test_pythoncapi_compat_cpp03ext', ['-std=c++03']), + ('test_pythoncapi_compat_cpp11ext', ['-std=c++11']), + ('test_pythoncapi_compat_cpp14ext', ['-std=c++14']), + ('test_pythoncapi_compat_cpp17ext', ['-std=c++17']), + ('test_pythoncapi_compat_cpp20ext', ['-std=c++20']), + ] +else: + # MSVC doesn't support /std:c++11 + CXX_VERSIONS = [ + ('test_pythoncapi_compat_cppext', None), + ('test_pythoncapi_compat_cpp14ext', ['/std:c++14', '/Zc:__cplusplus']), + ] + def main(): # gh-105776: When "gcc -std=11" is used as the C++ compiler, -std=c11 @@ -75,27 +92,21 @@ def main(): os.environ['CC'] = cmd # C extension - c_ext = Extension( - 'test_pythoncapi_compat_cext', - sources=['test_pythoncapi_compat_cext.c'], - extra_compile_args=CFLAGS) - extensions = [c_ext] + extensions = [] + for std in C_VERSIONS: + if not MSVC: + cflags = CFLAGS + ['-std=%s' % std] + else: + cflags = CFLAGS + ['/std:%s' % std] + c_ext = Extension( + 'test_pythoncapi_compat_cext_%s' % std, + sources=['test_pythoncapi_compat_cext.c'], + extra_compile_args=cflags) + extensions.append(c_ext) if TEST_CXX: # C++ extension - - # MSVC has /std flag but doesn't support /std:c++11 - if not MSVC: - versions = [ - ('test_pythoncapi_compat_cpp03ext', ['-std=c++03']), - ('test_pythoncapi_compat_cpp11ext', ['-std=c++11']), - ] - else: - versions = [ - ('test_pythoncapi_compat_cppext', None), - ('test_pythoncapi_compat_cpp14ext', ['/std:c++14', '/Zc:__cplusplus']), - ] - for name, std_flags in versions: + for name, std_flags in CXX_VERSIONS: flags = list(CXXFLAGS) if std_flags is not None: flags.extend(std_flags) diff --git a/tests/test_pythoncapi_compat.py b/tests/test_pythoncapi_compat.py index 47ffd9b..93005cd 100644 --- a/tests/test_pythoncapi_compat.py +++ b/tests/test_pythoncapi_compat.py @@ -32,19 +32,29 @@ # Windows uses MSVC compiler MSVC = (os.name == "nt") -TESTS = [ - ("test_pythoncapi_compat_cext", "C"), -] +# C++ is only supported on Python 3.6 and newer +TEST_CXX = (sys.version_info >= (3, 6)) + if not MSVC: - TESTS.extend(( + C_TESTS = [ + ("test_pythoncapi_compat_cext_c99", "C99"), + ("test_pythoncapi_compat_cext_c11", "C11"), + ] + CXX_TESTS = [ ("test_pythoncapi_compat_cpp03ext", "C++03"), ("test_pythoncapi_compat_cpp11ext", "C++11"), - )) + ("test_pythoncapi_compat_cpp14ext", "C++14"), + ("test_pythoncapi_compat_cpp17ext", "C++17"), + ("test_pythoncapi_compat_cpp20ext", "C++20"), + ] else: - TESTS.extend(( + C_TESTS = [ + ("test_pythoncapi_compat_cext_c11", "C11"), + ] + CXX_TESTS = [ ("test_pythoncapi_compat_cppext", "C++"), ("test_pythoncapi_compat_cpp14ext", "C++14"), - )) + ] VERBOSE = False @@ -84,9 +94,12 @@ def import_tests(module_name): if not pythonpath: raise Exception("Failed to find the build directory") - sys.path.append(pythonpath) - - return __import__(module_name) + old_sys_path = list(sys.path) + try: + sys.path.append(pythonpath) + return __import__(module_name) + finally: + sys.path[:] = old_sys_path def _run_tests(tests, verbose): @@ -155,18 +168,7 @@ def run_tests(module_name, lang): title = "Test %s (%s)" % (module_name, lang) display_title(title) - try: - testmod = import_tests(module_name) - except ImportError: - # The C extension must always be available - if lang == "C": - raise - - if VERBOSE: - print("%s: skip %s, missing %s extension" - % (python_version(), lang, module_name)) - print() - return + testmod = import_tests(module_name) if VERBOSE: empty_line = False @@ -226,7 +228,10 @@ def main(): build_ext() - for module_name, lang in TESTS: + tests = list(C_TESTS) + if TEST_CXX: + tests += CXX_TESTS + for module_name, lang in tests: run_tests(module_name, lang) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 132708d..19db02e 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -16,14 +16,24 @@ # define PYTHON3 1 #endif -#if defined(__cplusplus) && __cplusplus >= 201402 +#if defined(__cplusplus) && __cplusplus >= 202002L +# define MODULE_NAME test_pythoncapi_compat_cpp20ext +#elif defined(__cplusplus) && __cplusplus >= 201703L +# define MODULE_NAME test_pythoncapi_compat_cpp17ext +#elif defined(__cplusplus) && __cplusplus >= 201402L # define MODULE_NAME test_pythoncapi_compat_cpp14ext -#elif defined(__cplusplus) && __cplusplus >= 201103 +#elif defined(__cplusplus) && __cplusplus >= 201103L # define MODULE_NAME test_pythoncapi_compat_cpp11ext #elif defined(__cplusplus) && !defined(_MSC_VER) # define MODULE_NAME test_pythoncapi_compat_cpp03ext #elif defined(__cplusplus) # define MODULE_NAME test_pythoncapi_compat_cppext +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 202311L +# define MODULE_NAME test_pythoncapi_compat_cext_c23 +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L +# define MODULE_NAME test_pythoncapi_compat_cext_c11 +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define MODULE_NAME test_pythoncapi_compat_cext_c99 #else # define MODULE_NAME test_pythoncapi_compat_cext #endif From f6121eb60f9fe9ff227fc78f9b39d3bd2a81f434 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 12 Feb 2026 16:56:49 +0100 Subject: [PATCH 86/87] Build C and C++ extensions with /W4 on Windows (#168) Build C and C++ extensions with /W4 on Windows on Python 3.12 and newer. Move PyAPI_FUNC() definitions to the file level. Remove also -Wno-typedef-redefinition flag. --- pythoncapi_compat.h | 26 +++++++++++++++++++------- tests/setup.py | 11 ++++++++--- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index e26185c..99970f6 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1569,6 +1569,11 @@ static inline int PyLong_IsZero(PyObject *obj) // gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 #if PY_VERSION_HEX < 0x030E00A0 + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) +PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); +#endif + static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) { if (!PyUnicode_Check(str1)) { @@ -1583,8 +1588,6 @@ static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) } #if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) - PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); - return _PyUnicode_Equal(str1, str2); #elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) return _PyUnicode_EQ(str1, str2); @@ -1607,11 +1610,14 @@ static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) #if PY_VERSION_HEX < 0x030E00A0 + +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); +#endif + static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) { #if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) - PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); - return _Py_HashBytes(ptr, len); #else Py_hash_t hash; @@ -1948,11 +1954,14 @@ PyLongWriter_Finish(PyLongWriter *writer) // gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 #if PY_VERSION_HEX < 0x030E00A4 + +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); +#endif + static inline FILE* Py_fopen(PyObject *path, const char *mode) { #if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) - PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); - return _Py_fopen_obj(path, mode); #else FILE *f; @@ -2664,6 +2673,10 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) // to make objects immortal until 3.14 which has _Py_SetImmortal(). Since // immortal objects are primarily needed for free-threading, this API is implemented // for 3.14 using _Py_SetImmortal() and uses private macros on 3.13. +#if 0x030E0000 <= PY_VERSION_HEX +PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); +#endif + static inline int PyUnstable_SetImmortal(PyObject *op) { @@ -2672,7 +2685,6 @@ PyUnstable_SetImmortal(PyObject *op) return 0; } #if 0x030E0000 <= PY_VERSION_HEX - PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); _Py_SetImmortal(op); #else // Python 3.13 doesn't export _Py_SetImmortal() function diff --git a/tests/setup.py b/tests/setup.py index e394bce..b25bc8d 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -32,9 +32,6 @@ '-Wall', '-Wextra', # Extra warnings '-Wconversion', - # /usr/lib64/pypy3.7/include/pyport.h:68:20: error: redefinition of typedef - # 'Py_hash_t' is a C11 feature - '-Wno-typedef-redefinition', # Formatting checks '-Wformat', '-Wformat-nonliteral', @@ -47,6 +44,14 @@ # Treat all compiler warnings as compiler errors '/WX', )) + # Python 3.11 and older emits C4100 "unreferenced parameter" warnings + # on Py_UNUSED() parameters. Py_UNUSED() was modified in Python 3.12 + # to support MSVC. + if sys.version_info >= (3, 12): + COMMON_FLAGS.extend(( + # Display warnings level 1 to 4 + '/W4', + )) CFLAGS = list(COMMON_FLAGS) CXXFLAGS = list(COMMON_FLAGS) From 75a796764d63327268d195e2d5c044e564d0dada Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Mar 2026 12:10:30 +0100 Subject: [PATCH 87/87] PyUnicodeWriter_WriteRepr(NULL) now writes "" (#170) --- docs/api.rst | 1 + pythoncapi_compat.h | 5 +++++ tests/test_pythoncapi_compat_cext.c | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index a8c4c6f..2681a4d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -262,6 +262,7 @@ Not supported: * ``PyInitConfig_SetStrList()`` * ``PyType_GetBaseByToken()`` * ``PyUnicodeWriter_DecodeUTF8Stateful()`` +* ``PyUnicodeWriter_WriteUCS4()`` * ``Py_InitializeFromInitConfig()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 99970f6..08b6915 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1425,6 +1425,11 @@ PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) static inline int PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) { + if (obj == NULL) { + return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, + "", 6); + } + PyObject *str = PyObject_Repr(obj); if (str == NULL) { return -1; diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 19db02e..1074266 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1908,13 +1908,17 @@ test_unicodewriter(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args)) goto error; } Py_CLEAR(str); + if (PyUnicodeWriter_WriteRepr(writer, NULL) < 0) { + goto error; + } { PyObject *result = PyUnicodeWriter_Finish(writer); if (result == NULL) { return NULL; } - assert(PyUnicode_EqualToUTF8(result, "var=long non-ASCII valu\xC3\xA9 'repr'")); + const char *expected = "var=long non-ASCII valu\xC3\xA9 'repr'"; + assert(PyUnicode_EqualToUTF8(result, expected)); Py_DECREF(result); }