Skip to content

Commit 0462959

Browse files
authored
fix: free threaded Python (#741)
1 parent cffaf48 commit 0462959

File tree

16 files changed

+139
-29
lines changed

16 files changed

+139
-29
lines changed

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,25 @@ jobs:
175175
- name: Test min package
176176
run: pytest -ra --showlocals -Wdefault
177177

178+
manylinux:
179+
name: Manylinux on 🐍 3.13 • Free-threaded
180+
runs-on: ubuntu-latest
181+
timeout-minutes: 40
182+
container: quay.io/pypa/musllinux_1_2_x86_64:latest
183+
steps:
184+
- uses: actions/checkout@v4
185+
with:
186+
fetch-depth: 0
187+
188+
- name: Prepare venv
189+
run: python3.13t -m venv /venv
190+
191+
- name: Install deps
192+
run: /venv/bin/pip install -e .[test] ninja
193+
194+
- name: Test package
195+
run: /venv/bin/pytest
196+
178197
cygwin:
179198
name: Tests on 🐍 3.9 • cygwin
180199
runs-on: windows-latest

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,17 @@ repos:
119119
language: pygrep
120120
entry: tool\.cmake
121121
exclude: .pre-commit-config.yaml
122+
123+
- repo: https://github.com/henryiii/validate-pyproject-schema-store
124+
rev: 2024.04.29
125+
hooks:
126+
- id: validate-pyproject
127+
128+
- repo: https://github.com/python-jsonschema/check-jsonschema
129+
rev: 0.28.3
130+
hooks:
131+
- id: check-dependabot
132+
- id: check-github-workflows
133+
- id: check-readthedocs
134+
- id: check-metaschema
135+
files: \.schema\.json

docs/cmakelists.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,17 @@ When defining your module, if you only support the Stable ABI after some point,
132132
you should use (for example for 3.11):
133133

134134
```cmake
135-
if(Python_VERSION VERSION_GREATER_EQUAL 3.11 AND Python_INTERPRETER_ID STREQUAL Python)
136-
python_add_library(some_ext MODULE USE_SABI 3.11 ...)
135+
if(NOT "${SKBUILD_SABI_COMPONENT}" STREQUAL "")
136+
python_add_library(some_ext MODULE WITH_SOABI USE_SABI 3.11 ...)
137137
else()
138138
python_add_library(some_ext MODULE WITH_SOABI ...)
139139
endif()
140140
```
141141

142-
This will define `Py_LIMITED_API` for you.
142+
This will define `Py_LIMITED_API` for you. If you want to support building
143+
directly from CMake, you need to protect this for Python version,
144+
`Python_INTERPRETER_ID STREQUAL Python`, and free-threading Python 3.13+ doesn't
145+
support ABI3 either.
143146

144147
If you are using `nanobind`'s `nanobind_add_module`, the `STABLE_ABI` flag does
145148
this automatically for you for 3.12+.

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ classifiers = [
2626
"Programming Language :: Python :: 3.10",
2727
"Programming Language :: Python :: 3.11",
2828
"Programming Language :: Python :: 3.12",
29+
"Programming Language :: Python :: 3.13",
2930
"Development Status :: 4 - Beta",
3031
"Typing :: Typed",
3132
]
@@ -51,7 +52,8 @@ pyproject = [
5152
test = [
5253
"build >=0.8",
5354
"cattrs >=22.2.0",
54-
"pip >=22",
55+
"pip >=22; python_version<'3.13'",
56+
"pip >=24.1b1; python_version>='3.13'",
5557
"pybind11 >=2.11",
5658
"pytest >=7.0", # 7.2+ recommended for better tracebacks with ExceptionGroup
5759
"pytest-subprocess >=1.5",

src/scikit_build_core/builder/builder.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def configure(
114114
cache_entries: Mapping[str, str | Path] | None = None,
115115
name: str | None = None,
116116
version: Version | None = None,
117-
limited_abi: bool | None = None,
117+
limited_api: bool | None = None,
118118
configure_args: Iterable[str] = (),
119119
) -> None:
120120
cmake_defines = {
@@ -162,16 +162,26 @@ def configure(
162162
cache_config["SKBUILD_PROJECT_VERSION"] = version.base_version
163163
cache_config["SKBUILD_PROJECT_VERSION_FULL"] = str(version)
164164

165-
if limited_abi is None:
165+
if limited_api is None:
166166
if self.settings.wheel.py_api.startswith("cp3"):
167167
target_minor_version = int(self.settings.wheel.py_api[3:])
168-
limited_abi = target_minor_version <= sys.version_info.minor
168+
limited_api = target_minor_version <= sys.version_info.minor
169169
else:
170-
limited_abi = False
170+
limited_api = False
171+
172+
if limited_api and sys.implementation.name != "cpython":
173+
limited_api = False
174+
logger.info("PyPy doesn't support the Limited API, ignoring")
175+
176+
if limited_api and sysconfig.get_config_var("Py_GIL_DISABLED"):
177+
limited_api = False
178+
logger.info(
179+
"Free-threaded Python doesn't support the Limited API currently, ignoring"
180+
)
171181

172182
python_library = get_python_library(self.config.env, abi3=False)
173183
python_sabi_library = (
174-
get_python_library(self.config.env, abi3=True) if limited_abi else None
184+
get_python_library(self.config.env, abi3=True) if limited_api else None
175185
)
176186
python_include_dir = get_python_include_dir()
177187
numpy_include_dir = get_numpy_include_dir()
@@ -196,11 +206,11 @@ def configure(
196206
if numpy_include_dir:
197207
cache_config[f"{prefix}_NumPy_INCLUDE_DIR"] = numpy_include_dir
198208

199-
cache_config["SKBUILD_SOABI"] = get_soabi(self.config.env, abi3=limited_abi)
209+
cache_config["SKBUILD_SOABI"] = get_soabi(self.config.env, abi3=limited_api)
200210

201211
# Allow CMakeLists to detect this is supposed to be a limited ABI build
202212
cache_config["SKBUILD_SABI_COMPONENT"] = (
203-
"Development.SABIModule" if limited_abi else ""
213+
"Development.SABIModule" if limited_api else ""
204214
)
205215

206216
if cache_entries:

src/scikit_build_core/builder/wheel_tag.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dataclasses
44
import itertools
55
import sys
6+
import sysconfig
67
from typing import TYPE_CHECKING
78

89
import packaging.tags
@@ -104,11 +105,12 @@ def compute_best(
104105
if (
105106
sys.implementation.name == "cpython"
106107
and minor <= sys.version_info.minor
108+
and not sysconfig.get_config_var("Py_GIL_DISABLED")
107109
):
108110
pyvers = pyvers_new
109111
abi = "abi3"
110112
else:
111-
msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high"
113+
msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high or free-threaded"
112114
logger.debug(msg, sys.implementation.name, minor)
113115
elif all(x.startswith("py") and x[2:].isdecimal() for x in pyvers_new):
114116
pyvers = pyvers_new

src/scikit_build_core/setuptools/build_cmake.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def run(self) -> None:
141141
name=dist.get_name(),
142142
version=Version(dist.get_version()),
143143
defines={},
144-
limited_abi=limited_api,
144+
limited_api=limited_api,
145145
configure_args=configure_args,
146146
)
147147

tests/conftest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def pep518_wheelhouse(tmp_path_factory: pytest.TempPathFactory) -> Path:
5252
"build",
5353
"cython",
5454
"hatchling",
55-
"pip>=23",
55+
"pip>=23; python_version<'3.13'",
56+
"pip>=24.1b1; python_version>='3.13'",
5657
"pybind11",
5758
"setuptools",
5859
"virtualenv",
@@ -94,6 +95,8 @@ def __init__(self, env_dir: Path, *, wheelhouse: Path | None = None) -> None:
9495
self.purelib = Path(
9596
self.execute("import sysconfig; print(sysconfig.get_path('purelib'))")
9697
)
98+
if sys.version_info >= (3, 13):
99+
self.run("pip", "install", "-U", "pip>=24.1b1")
97100

98101
@overload
99102
def run(self, *args: str, capture: Literal[True]) -> str: ...
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cmake_minimum_required(VERSION 3.15...3.26)
1+
cmake_minimum_required(VERSION 3.15...3.29)
22

33
project(
44
${SKBUILD_PROJECT_NAME}
@@ -7,9 +7,13 @@ project(
77

88
find_package(
99
Python
10-
COMPONENTS Interpreter Development.SABIModule
10+
COMPONENTS Interpreter Development.Module ${SKBUILD_SABI_COMPONENT}
1111
REQUIRED)
1212

13-
python_add_library(abi3_example MODULE abi3_example.c WITH_SOABI USE_SABI 3.7)
13+
if(NOT "${SKBUILD_SABI_COMPONENT}" STREQUAL "")
14+
python_add_library(abi3_example MODULE abi3_example.c WITH_SOABI USE_SABI 3.7)
15+
else()
16+
python_add_library(abi3_example MODULE abi3_example.c WITH_SOABI)
17+
endif()
1418

1519
install(TARGETS abi3_example DESTINATION .)

tests/packages/cython_pxd_editable/pkg1/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ find_package(
77
REQUIRED)
88

99
add_custom_command(
10-
OUTPUT src/pkg1/one.c
11-
DEPENDS src/pkg1/one.pyx
10+
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/src/pkg1/one.c"
11+
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/pkg1/one.pyx"
1212
VERBATIM
1313
COMMAND
1414
Python::Interpreter -m cython

tests/packages/cython_pxd_editable/pkg1/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ name = "pkg1"
1010
version = "1.0.0"
1111

1212
[tool.scikit-build]
13-
wheel.packages = ["src/pkg1"]
13+
ninja.make-fallback = false

tests/packages/cython_pxd_editable/pkg2/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ name = "pkg2"
1010
version = "1.0.0"
1111

1212
[tool.scikit-build]
13-
wheel.packages = ["src/pkg2"]
13+
ninja.make-fallback = false

tests/test_builder.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ def test_build_tool_args():
157157
],
158158
)
159159
def test_wheel_tag(monkeypatch, minver, archs, answer):
160+
get_config_var = sysconfig.get_config_var
161+
monkeypatch.setattr(
162+
sysconfig,
163+
"get_config_var",
164+
lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x),
165+
)
160166
monkeypatch.setattr(sys, "platform", "darwin")
161167
monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", minver)
162168
monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", ""))
@@ -168,6 +174,12 @@ def test_wheel_tag(monkeypatch, minver, archs, answer):
168174

169175
@pytest.mark.parametrize("archs", ["x86_64" "arm64" "universal2"])
170176
def test_wheel_build_tag(monkeypatch, archs):
177+
get_config_var = sysconfig.get_config_var
178+
monkeypatch.setattr(
179+
sysconfig,
180+
"get_config_var",
181+
lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x),
182+
)
171183
monkeypatch.setattr(sys, "platform", "darwin")
172184
monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.12")
173185
monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", ""))
@@ -178,6 +190,12 @@ def test_wheel_build_tag(monkeypatch, archs):
178190

179191

180192
def test_wheel_tag_expand(monkeypatch):
193+
get_config_var = sysconfig.get_config_var
194+
monkeypatch.setattr(
195+
sysconfig,
196+
"get_config_var",
197+
lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x),
198+
)
181199
monkeypatch.setattr(sys, "platform", "darwin")
182200
monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10")
183201
monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", ""))
@@ -195,6 +213,12 @@ def test_wheel_tag_expand(monkeypatch):
195213

196214

197215
def test_wheel_tag_expand_11(monkeypatch):
216+
get_config_var = sysconfig.get_config_var
217+
monkeypatch.setattr(
218+
sysconfig,
219+
"get_config_var",
220+
lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x),
221+
)
198222
monkeypatch.setattr(sys, "platform", "darwin")
199223
monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "11.2")
200224
monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", ""))
@@ -209,6 +233,12 @@ def test_wheel_tag_expand_11(monkeypatch):
209233

210234

211235
def test_wheel_tag_with_abi_darwin(monkeypatch):
236+
get_config_var = sysconfig.get_config_var
237+
monkeypatch.setattr(
238+
sysconfig,
239+
"get_config_var",
240+
lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x),
241+
)
212242
monkeypatch.setattr(sys, "platform", "darwin")
213243
monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10")
214244
monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", ""))

tests/test_editable.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ def test_cython_pxd(monkeypatch, tmp_path, editable, editable_mode, isolated):
7373
tmp_path1.mkdir()
7474
process_package(package1, tmp_path1, monkeypatch)
7575

76-
isolated.install("pip>23", "cython", "scikit-build-core")
76+
ninja = [
77+
"ninja" for f in isolated.wheelhouse.iterdir() if f.name.startswith("ninja-")
78+
]
79+
cmake = [
80+
"cmake" for f in isolated.wheelhouse.iterdir() if f.name.startswith("cmake-")
81+
]
82+
83+
isolated.install("pip>23", "cython", "scikit-build-core", *ninja, *cmake)
7784

7885
isolated.install(
7986
"-v",

tests/test_pyproject_abi3.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515

1616
@pytest.mark.compile()
1717
@pytest.mark.configure()
18-
@pytest.mark.skipif(
19-
sys.implementation.name == "pypy", reason="pypy does not support abi3"
20-
)
2118
@pytest.mark.skipif(
2219
sysconfig.get_platform().startswith(("msys", "mingw")),
2320
reason="abi3 FindPython on MSYS/MinGW reports not found",
@@ -34,7 +31,14 @@ def test_abi3_wheel(tmp_path, monkeypatch, virtualenv):
3431
out = build_wheel(str(dist))
3532
(wheel,) = dist.glob("abi3_example-0.0.1-*.whl")
3633
assert wheel == dist / out
37-
assert "-cp37-abi3-" in out
34+
abi3 = sys.implementation.name == "cpython" and not sysconfig.get_config_var(
35+
"Py_GIL_DISABLED"
36+
)
37+
38+
if abi3:
39+
assert "-cp37-abi3-" in out
40+
else:
41+
assert "-cp37-abi3-" not in out
3842

3943
if sys.version_info >= (3, 8):
4044
with wheel.open("rb") as f:
@@ -47,11 +51,19 @@ def test_abi3_wheel(tmp_path, monkeypatch, virtualenv):
4751
(so_file,) = file_names
4852

4953
if sysconfig.get_platform().startswith("win"):
50-
assert so_file == "abi3_example.pyd"
54+
if sys.implementation.name == "cpython":
55+
assert so_file == "abi3_example.pyd"
56+
else:
57+
assert so_file.endswith(".pyd")
5158
elif sys.platform.startswith("cygwin"):
52-
assert so_file == "abi3_example.abi3.dll"
53-
else:
59+
if abi3:
60+
assert so_file == "abi3_example.abi3.dll"
61+
else:
62+
assert so_file != "abi3_example.abi3.dll"
63+
elif abi3:
5464
assert so_file == "abi3_example.abi3.so"
65+
else:
66+
assert so_file != "abi3_example.abi3.so"
5567

5668
virtualenv.install(wheel)
5769

tests/test_setuptools_abi3.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
@pytest.mark.skipif(
2121
sys.implementation.name == "pypy", reason="pypy does not support abi3"
2222
)
23+
@pytest.mark.skipif(
24+
sysconfig.get_config_var("Py_GIL_DISABLED"),
25+
reason="Free-threaded Python does not support abi3",
26+
)
2327
@pytest.mark.skipif(
2428
SYSCONFIGPLAT.startswith(("msys", "mingw")),
2529
reason="abi3 FindPython on MSYS/MinGW reports not found",

0 commit comments

Comments
 (0)