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 3f30e53..2681a4d 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 @@ -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 ----------- @@ -257,6 +262,7 @@ Not supported: * ``PyInitConfig_SetStrList()`` * ``PyType_GetBaseByToken()`` * ``PyUnicodeWriter_DecodeUTF8Stateful()`` +* ``PyUnicodeWriter_WriteUCS4()`` * ``Py_InitializeFromInitConfig()`` 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..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; @@ -1569,6 +1574,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 +1593,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 +1615,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 +1959,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; @@ -2659,6 +2673,40 @@ 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. +#if 0x030E0000 <= PY_VERSION_HEX +PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); +#endif + +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 + _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/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", diff --git a/tests/setup.py b/tests/setup.py index 33a0e66..b25bc8d 100755 --- a/tests/setup.py +++ b/tests/setup.py @@ -32,28 +32,50 @@ '-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', '-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(( # 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) +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 +97,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 8480415..93005cd 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 @@ -27,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 @@ -49,9 +64,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() @@ -81,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): @@ -94,10 +110,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,25 +153,22 @@ 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) - 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 @@ -212,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 e8ada23..1074266 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 @@ -1898,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); } @@ -2489,6 +2503,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 +2593,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} }; @@ -2588,6 +2638,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