From 8298b3578a0493413bae704cf05edd0fe9e1a9c6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:32:43 +0200 Subject: [PATCH 1/8] Check for 'static inline' Build a C extension with two C files to check that all pythoncapi_compat.h functions are declared with "static inline". --- tests/extension.c | 34 +++++++++++++++++++++++++++++ tests/setup.py | 20 +++++++++++++++++ tests/static_inline.c | 6 +++++ tests/test_pythoncapi_compat_cext.c | 8 +++---- 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 tests/extension.c create mode 100644 tests/static_inline.c diff --git a/tests/extension.c b/tests/extension.c new file mode 100644 index 0000000..a162e75 --- /dev/null +++ b/tests/extension.c @@ -0,0 +1,34 @@ +// C extension made of two C files (extension.c and static_inline.c) to detect +// when a pythoncapi_compat.h function is implemented without "static inline". + +#ifndef MODULE_NAME +# error "MODULE_NAME macro is not set" +#endif + +#include "pythoncapi_compat.h" + +#define STR(NAME) #NAME +#define CONCAT(a, b) a ## b + +// No method and no module exec method needed, most basic C extension + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + STR(MODULE_NAME), // m_name + _Py_NULL, // m_doc + 0, // m_size + _Py_NULL, // m_methods + _Py_NULL, // m_slots + _Py_NULL, // m_traverse + _Py_NULL, // m_clear + _Py_NULL, // m_free +}; + + +#define INIT_FUNC CONCAT(PyInit_, MODULE_NAME) + +PyMODINIT_FUNC +INIT_FUNC(void) +{ + return PyModule_Create(&module_def); +} diff --git a/tests/setup.py b/tests/setup.py index ba79156..6f746c3 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -202,6 +202,26 @@ def add_common_flags(flags, limited): language='c++') extensions.append(ext) + # Build a C extension with two C files to detect when a pythoncapi_compat.h + # function is implemented without "static inline". + sources = [ + 'extension.c', + 'static_inline.c', + ] + for limited in (False, True): + name = 'test_pythoncapi_compat_ext' + assert name != C_EXTENSION_PREFIX + flags = list(COMMON_FLAGS) + if limited: + name += LIMITED_SUFFIX + flags.append(limited_flag) + flags.append(f'-DMODULE_NAME={name}') + ext = Extension( + name, + sources=sources, + extra_compile_args=flags) + extensions.append(ext) + setup(name="test_pythoncapi_compat", ext_modules=extensions) diff --git a/tests/static_inline.c b/tests/static_inline.c new file mode 100644 index 0000000..4e29f04 --- /dev/null +++ b/tests/static_inline.c @@ -0,0 +1,6 @@ +// C extension made of two C files (extension.c and static_inline.c) to detect +// when a pythoncapi_compat.h function is implemented without "static inline". + +#include "pythoncapi_compat.h" + +// No function defined, only #include "pythoncapi_compat.h" diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index d7383fe..1ef5946 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1,6 +1,10 @@ // Always enable assertions #undef NDEBUG +#ifndef MODULE_NAME +# error "MODULE_NAME macro is not set" +#endif + #include "pythoncapi_compat.h" #include // T_SHORT, READONLY @@ -8,10 +12,6 @@ # error "assertions must be enabled" #endif -#ifndef MODULE_NAME -# error "MODULE_NAME macro is not set" -#endif - #define _STR(NAME) #NAME #define STR(NAME) _STR(NAME) #define _CONCAT(a, b) a ## b From 316fc01d3e6fd02dd0e0f9346f4911119a38d4b0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:46:44 +0200 Subject: [PATCH 2/8] PyPy On PyPy, don't define PyVectorcall_NARGS() and PyObject_Vectorcall() when targeting the limited C API. PyPy always define these functions, even if Py_LIMITED_API macro is defined. --- pythoncapi_compat.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index c6fe172..2adb1da 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -687,7 +687,8 @@ _Py_COMP_DIAG_POP // bpo-36974 added PyVectorcall_NARGS() to Python 3.8b1 #if (PY_VERSION_HEX < _Py_PACK_VERSION(3, 8) \ - || (defined(Py_LIMITED_API) && PY_VERSION_HEX < _Py_PACK_VERSION(3, 12))) + || (defined(Py_LIMITED_API) && PY_VERSION_HEX < _Py_PACK_VERSION(3, 12) \ + && !defined(PYPY_VERSION))) static inline Py_ssize_t PyVectorcall_NARGS(size_t n) { return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; @@ -697,7 +698,8 @@ static inline Py_ssize_t PyVectorcall_NARGS(size_t n) // gh-105922 added PyObject_Vectorcall() to Python 3.9 #if (PY_VERSION_HEX < _Py_PACK_VERSION(3, 9) \ - || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 12))) + || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 12) \ + && !defined(PYPY_VERSION))) static inline PyObject* PyObject_Vectorcall(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames) From 67fdf4a7607bf92d83d31534b272b4c3cf5a6f27 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:53:33 +0200 Subject: [PATCH 3/8] Fix STR/CONCAT macros --- tests/extension.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/extension.c b/tests/extension.c index a162e75..f14eefc 100644 --- a/tests/extension.c +++ b/tests/extension.c @@ -7,8 +7,10 @@ #include "pythoncapi_compat.h" -#define STR(NAME) #NAME -#define CONCAT(a, b) a ## b +#define _STR(NAME) #NAME +#define STR(NAME) _STR(NAME) +#define _CONCAT(a, b) a ## b +#define CONCAT(a, b) _CONCAT(a, b) // No method and no module exec method needed, most basic C extension From 7b13796a87abebfa80ba709720edbfd3759ca660 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:56:03 +0200 Subject: [PATCH 4/8] Don't test limited C AP on PyPy --- tests/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/setup.py b/tests/setup.py index 6f746c3..ff86cb4 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -209,6 +209,8 @@ def add_common_flags(flags, limited): 'static_inline.c', ] for limited in (False, True): + if limited and not TEST_LIMITED_C_API: + continue name = 'test_pythoncapi_compat_ext' assert name != C_EXTENSION_PREFIX flags = list(COMMON_FLAGS) From 42b87c50dd31c81f0931e40cae3e08f96cb7120a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:55:56 +0200 Subject: [PATCH 5/8] Revert "PyPy" This reverts commit 316fc01d3e6fd02dd0e0f9346f4911119a38d4b0. --- pythoncapi_compat.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 2adb1da..c6fe172 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -687,8 +687,7 @@ _Py_COMP_DIAG_POP // bpo-36974 added PyVectorcall_NARGS() to Python 3.8b1 #if (PY_VERSION_HEX < _Py_PACK_VERSION(3, 8) \ - || (defined(Py_LIMITED_API) && PY_VERSION_HEX < _Py_PACK_VERSION(3, 12) \ - && !defined(PYPY_VERSION))) + || (defined(Py_LIMITED_API) && PY_VERSION_HEX < _Py_PACK_VERSION(3, 12))) static inline Py_ssize_t PyVectorcall_NARGS(size_t n) { return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; @@ -698,8 +697,7 @@ static inline Py_ssize_t PyVectorcall_NARGS(size_t n) // gh-105922 added PyObject_Vectorcall() to Python 3.9 #if (PY_VERSION_HEX < _Py_PACK_VERSION(3, 9) \ - || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 12) \ - && !defined(PYPY_VERSION))) + || (defined(Py_LIMITED_API) && Py_LIMITED_API+0 < _Py_PACK_VERSION(3, 12))) static inline PyObject* PyObject_Vectorcall(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames) From baf7f5378c2b1b1ea5024a0bf1595f2325e7f594 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 09:56:59 +0200 Subject: [PATCH 6/8] Update doc --- docs/api.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index de44b8d..caccea8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,6 +10,8 @@ Supported Python versions: * Python 3.6 - 3.16 * PyPy 3.6 - 3.12 +The limited C API is not supported on PyPy. + A C11 subset (without optional features) is required, like ``static inline`` functions: see `PEP 7 `_. From 3608d6c9f1966816b644b52c1815a2faba467482 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 10:01:16 +0200 Subject: [PATCH 7/8] Support Python 3.15 Free Threading --- tests/extension.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/extension.c b/tests/extension.c index f14eefc..bf45704 100644 --- a/tests/extension.c +++ b/tests/extension.c @@ -12,6 +12,44 @@ #define _CONCAT(a, b) a ## b #define CONCAT(a, b) _CONCAT(a, b) +// Use PySlot API on Python 3.15 and newer and on limited C API 3.15 and newer +#if (PY_VERSION_HEX >= _Py_PACK_VERSION(3, 15) \ + && (!defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15))) + +PyABIInfo_VAR(abi_info); + +static PySlot module_slots[] = { + PySlot_PTR_STATIC(Py_mod_abi, &abi_info), + PySlot_PTR_STATIC(Py_mod_name, (void*)STR(MODULE_NAME)), + PySlot_PTR_STATIC(Py_mod_state_size, 0), + PySlot_PTR_STATIC(Py_mod_gil, Py_MOD_GIL_NOT_USED), + {0, 0, {0}, {0}} +}; + +#define EXPORT_FUNC CONCAT(PyModExport_, MODULE_NAME) + +PyMODEXPORT_FUNC +EXPORT_FUNC(void) +{ + return module_slots; +} + +#ifdef MS_WINDOWS +# define INIT_FUNC CONCAT(PyInit_, MODULE_NAME) + +// Workaround for https://github.com/pypa/distutils/issues/387 +// https://docs.python.org/3.16/howto/abi3t-migration.html#module-export-hook +PyMODINIT_FUNC +INIT_FUNC(void) +{ + PyErr_SetString(PyExc_SystemError, + "PyInit_* called for module with PyModExport_*"); + return NULL; +} +#endif + +#else + // No method and no module exec method needed, most basic C extension static struct PyModuleDef module_def = { @@ -34,3 +72,4 @@ INIT_FUNC(void) { return PyModule_Create(&module_def); } +#endif From 3b0416680b68dd59685190700ebe2a373c76b3f2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Oct 2026 10:04:32 +0200 Subject: [PATCH 8/8] Doc --- docs/api.rst | 2 +- tests/extension.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index caccea8..ec0ad0e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,7 +10,7 @@ Supported Python versions: * Python 3.6 - 3.16 * PyPy 3.6 - 3.12 -The limited C API is not supported on PyPy. +The limited C API is only supported on CPython. A C11 subset (without optional features) is required, like ``static inline`` functions: see `PEP 7 `_. diff --git a/tests/extension.c b/tests/extension.c index bf45704..f0d3e5b 100644 --- a/tests/extension.c +++ b/tests/extension.c @@ -12,6 +12,10 @@ #define _CONCAT(a, b) a ## b #define CONCAT(a, b) _CONCAT(a, b) + +// No method and no module exec method needed, most basic C extension + + // Use PySlot API on Python 3.15 and newer and on limited C API 3.15 and newer #if (PY_VERSION_HEX >= _Py_PACK_VERSION(3, 15) \ && (!defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= _Py_PACK_VERSION(3, 15))) @@ -50,8 +54,6 @@ INIT_FUNC(void) #else -// No method and no module exec method needed, most basic C extension - static struct PyModuleDef module_def = { PyModuleDef_HEAD_INIT, STR(MODULE_NAME), // m_name