From 3bdccbd0b5c89fc5bb770e2d3568d16d4e00d2e9 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Sun, 12 Jul 2026 20:50:42 -0700 Subject: [PATCH 1/3] convert: allow per-interpreter GIL We don't have any special GIL handling so this is fine. Signed-off-by: Benjamin Gilbert --- openslide/_convert.c | 1 + 1 file changed, 1 insertion(+) diff --git a/openslide/_convert.c b/openslide/_convert.c index 4427c72a..e1c17d3b 100644 --- a/openslide/_convert.c +++ b/openslide/_convert.c @@ -105,6 +105,7 @@ static PyMethodDef _convert_methods[] = { static PyModuleDef_Slot _convert_slots[] = { #if PY_VERSION_HEX >= 0x030D0000 && !defined(Py_LIMITED_API) {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, #endif {0, NULL} }; From 0dfd2d321d81188a43a19cca8f8d80674738268d Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Oct 2026 00:10:56 -0500 Subject: [PATCH 2/3] setup.py: clean up Stable ABI terminology in comments Notwithstanding the `py_limited_api` kwarg name, we really mean the Stable ABI when referring to binary artifacts. Signed-off-by: Benjamin Gilbert --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 3a89c30d..4734abbf 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ with open(Path(__file__).parent / 'openslide/_version.py') as _fh: exec(_fh.read()) # instantiates __version__ # noqa: S102 -# use the Limited API on Python 3.11+ on GIL builds; build release-specific +# use the Stable ABI on Python 3.11+ on GIL builds; build release-specific # wheels on older or free-threaded Python _abi3 = sys.version_info >= (3, 11) and not sysconfig.get_config_var('Py_GIL_DISABLED') @@ -19,12 +19,12 @@ ['openslide/_convert.c'], # hide symbols that aren't in the Limited API define_macros=[('Py_LIMITED_API', '0x030b0000')] if _abi3 else [], - # tag extension module for Limited API + # tag extension module for Stable ABI py_limited_api=_abi3, ), ], options={ - # tag wheel for Limited API + # tag wheel for Stable ABI 'bdist_wheel': {'py_limited_api': 'cp311'} if _abi3 else {}, }, # From 129f2f6661ee615bf46f1965419dde219c27ca2b Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Tue, 20 Oct 2026 00:13:56 -0500 Subject: [PATCH 3/3] Target Stable ABI on free-threaded Python 3.15+ Building for the new Stable ABI creates a single wheel for both GILful and free-threaded Python 3.15+ (cp315-abi3.abi3t) but must happen in a free-threaded interpreter until setuptools drops that requirement. Fixes: https://github.com/openslide/openslide-python/issues/340 Signed-off-by: Benjamin Gilbert --- .github/workflows/python.yml | 10 ++++++---- openslide/_convert.c | 35 +++++++++++++++++++++++++++++++++++ setup.py | 30 ++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index db972b25..c7602893 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -135,9 +135,10 @@ jobs: fi mkdir -p "artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}" mv dist/* "artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}" - # from system builds, save version-specific wheels and oldest abi3 wheel + # from system builds, save version-specific wheels, oldest abi3 wheel, + # and oldest abi3.abi3t wheel python -c 'import sys, sysconfig - if "${{ matrix.openslide }}" == "system" and (sys.version_info < (3, 12) or sysconfig.get_config_var("Py_GIL_DISABLED")): + if "${{ matrix.openslide }}" == "system" and (sys.version_info < (3, 12) or (sys.version_info < (3, 16) and sysconfig.get_config_var("Py_GIL_DISABLED"))): print("archive_wheel=1")' >> $GITHUB_ENV - name: Install run: pip install artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}/*.whl @@ -210,9 +211,10 @@ jobs: python -m build -w mkdir -p "artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}" mv dist/*.whl "artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}" - # from zip builds, save version-specific wheels and oldest abi3 wheel + # from zip builds, save version-specific wheels, oldest abi3 wheel, + # and oldest abi3.abi3t wheel python -c 'import sys, sysconfig - if "${{ matrix.openslide }}" == "zip" and (sys.version_info < (3, 12) or sysconfig.get_config_var("Py_GIL_DISABLED")): + if "${{ matrix.openslide }}" == "zip" and (sys.version_info < (3, 12) or (sys.version_info < (3, 16) and sysconfig.get_config_var("Py_GIL_DISABLED"))): print("archive_wheel=1")' >> $GITHUB_ENV - name: Install run: pip install artifacts/whl/${{ needs.pre-commit.outputs.dist-base }}/*.whl diff --git a/openslide/_convert.c b/openslide/_convert.c index e1c17d3b..8f5f1767 100644 --- a/openslide/_convert.c +++ b/openslide/_convert.c @@ -102,6 +102,40 @@ static PyMethodDef _convert_methods[] = { {NULL, NULL, 0, NULL} }; +#ifdef Py_TARGET_ABI3T +PyABIInfo_VAR(abi_info); + +static PySlot _convert_slot_array[] = { + PySlot_STATIC_DATA(Py_mod_abi, &abi_info), + PySlot_STATIC_DATA(Py_mod_name, "_convert"), + PySlot_STATIC_DATA(Py_mod_methods, _convert_methods), + PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), + PySlot_DATA(Py_mod_multiple_interpreters, + Py_MOD_PER_INTERPRETER_GIL_SUPPORTED), + PySlot_END +}; + +PyMODEXPORT_FUNC +PyModExport__convert(void) +{ + return _convert_slot_array; +} + +#ifdef _WIN32 +// distutils insists on exporting a PyInit_ function on Windows +// https://github.com/pypa/distutils/issues/387 +// https://github.com/python/cpython/issues/158098 +PyMODINIT_FUNC +PyInit__convert(void) +{ + PyErr_SetString(PyExc_SystemError, + "PyInit_* called for module with PyModExport_*"); + return NULL; +} +#endif + +#else + static PyModuleDef_Slot _convert_slots[] = { #if PY_VERSION_HEX >= 0x030D0000 && !defined(Py_LIMITED_API) {Py_mod_gil, Py_MOD_GIL_NOT_USED}, @@ -124,3 +158,4 @@ PyInit__convert(void) { return PyModuleDef_Init(&_convert_module); } +#endif diff --git a/setup.py b/setup.py index 4734abbf..dbdca38d 100644 --- a/setup.py +++ b/setup.py @@ -3,14 +3,24 @@ import sysconfig from setuptools import Extension, setup +from setuptools import __version__ as _setuptools_ver # Load version string with open(Path(__file__).parent / 'openslide/_version.py') as _fh: exec(_fh.read()) # instantiates __version__ # noqa: S102 -# use the Stable ABI on Python 3.11+ on GIL builds; build release-specific -# wheels on older or free-threaded Python -_abi3 = sys.version_info >= (3, 11) and not sysconfig.get_config_var('Py_GIL_DISABLED') +# Use the free-threaded Stable ABI (abi3t) only on free-threaded Python +# 3.15+ with setuptools 84.2+; Python supports building abi3t from +# GIL-enabled interpreters but setuptools doesn't yet. Use the +# non-free-threaded Stable API (abi3) on Python 3.11+ with GIL enabled, and +# release-specific wheels in all other cases. +_freethread = sysconfig.get_config_var('Py_GIL_DISABLED') +_abi3t = ( + sys.version_info >= (3, 15) + and _freethread + and tuple(int(c) for c in _setuptools_ver.split('.')) >= (84, 2) +) +_abi3 = sys.version_info >= (3, 11) and not _freethread setup( ext_modules=[ @@ -18,14 +28,22 @@ 'openslide._convert', ['openslide/_convert.c'], # hide symbols that aren't in the Limited API - define_macros=[('Py_LIMITED_API', '0x030b0000')] if _abi3 else [], + define_macros=[('Py_TARGET_ABI3T', '0x030f0000')] + if _abi3t + else [('Py_LIMITED_API', '0x030b0000')] + if _abi3 + else [], # tag extension module for Stable ABI - py_limited_api=_abi3, + py_limited_api=_abi3 or _abi3t, ), ], options={ # tag wheel for Stable ABI - 'bdist_wheel': {'py_limited_api': 'cp311'} if _abi3 else {}, + 'bdist_wheel': {'py_limited_api': 'cp315'} + if _abi3t + else {'py_limited_api': 'cp311'} + if _abi3 + else {}, }, # # setuptools < 61 compatibility for distro packages building from source