diff --git a/docs/configuration/index.md b/docs/configuration/index.md index f558e45ff..73c4ff990 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -372,7 +372,10 @@ wheel.py-api = "cp38" Scikit-build-core will only target ABI3 if the version of Python is equal to or newer than the one you set. `${SKBUILD_SABI_COMPONENT}` is set to -`Development.SABIModule` when targeting ABI3, and is an empty string otherwise. +`Development.SABIModule` when targeting ABI3 or ABI3T, and is an empty string +otherwise. For free-threaded Python (PEP 703), you can use `cp315t` to target +the free-threaded stable ABI, which sets `Py_TARGET_ABI3T` (if using CMake +4.4+). If you are not using CPython at all, you can specify any version of Python is fine: diff --git a/docs/guide/build.md b/docs/guide/build.md index 487e8b7ef..679a68a00 100644 --- a/docs/guide/build.md +++ b/docs/guide/build.md @@ -189,7 +189,8 @@ The three new items here (compared to SDists) are the [compatibility tags][]: `py3` for pure Python wheels, or `py312` (etc) for compiled wheels. - `abi tag`: The interpreter ABI this was built for. `none` for pure Python wheels or compiled wheels that don't use the Python API, `abi3` for stable ABI - / limited API wheels, and `cp312` (etc) for normal compiled wheels. + / limited API wheels, `abi3t` for free-threaded stable ABI wheels, and `cp312` + (etc) for normal compiled wheels. - `platform tag`: This is the platform the wheel is valid on, such as `any`, `linux_x86_64`, or `manylinux_2_17_x86_64`. diff --git a/docs/reference/configs.md b/docs/reference/configs.md index c2738a64e..d1b690672 100644 --- a/docs/reference/configs.md +++ b/docs/reference/configs.md @@ -605,8 +605,11 @@ print(mk_skbuild_docs()) You can also set this to "cp38" to enable the CPython 3.8+ Stable ABI / Limited API (only on CPython and if the version is sufficient, - otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to - ignore Python ABI compatibility. The ABI tag is inferred from this tag. + otherwise this has no effect). For free-threaded Python, you can use + "cp315t" to enable the free-threaded stable ABI (only on CPython + free-threaded builds and if the version is sufficient). Or you can set + it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI + tag is inferred from this tag. This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable. ``` diff --git a/src/scikit_build_core/builder/builder.py b/src/scikit_build_core/builder/builder.py index 65bc9f637..afcae657c 100644 --- a/src/scikit_build_core/builder/builder.py +++ b/src/scikit_build_core/builder/builder.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses +import enum import os import re import shlex @@ -35,6 +36,12 @@ DIR = Path(__file__).parent.resolve() +class _SabiMode(enum.Enum): + NONE = enum.auto() + ABI3 = enum.auto() + ABI3T = enum.auto() + + def __dir__() -> list[str]: return __all__ @@ -209,27 +216,48 @@ def configure( ) cache_config["SKBUILD_PROJECT_VERSION_FULL"] = str(version) - if limited_api is None: - if self.settings.wheel.py_api.startswith("cp3"): - target_minor_version = int(self.settings.wheel.py_api[3:]) - limited_api = target_minor_version <= sys.version_info.minor + py_api = self.settings.wheel.py_api + gil_disabled = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + + sabi = _SabiMode.NONE + if limited_api is True: + # Handle externally-set limited_api (e.g. from setuptools) + if sys.implementation.name != "cpython": + logger.info("PyPy doesn't support the Limited API, ignoring") + elif gil_disabled: + sabi = _SabiMode.ABI3T else: - limited_api = False - - if limited_api and sys.implementation.name != "cpython": - limited_api = False - logger.info("PyPy doesn't support the Limited API, ignoring") - - if limited_api and sysconfig.get_config_var("Py_GIL_DISABLED"): - limited_api = False - logger.info( - "Free-threaded Python doesn't support the Limited API currently, ignoring" - ) + sabi = _SabiMode.ABI3 + elif limited_api is None and py_api.startswith("cp3"): + target_minor_version = int(py_api[3:].rstrip("t")) + if sys.implementation.name != "cpython": + logger.info("py-api {} requires CPython, ignoring") + elif py_api.endswith("t"): + # Free-threaded stable ABI (PEP 803 / abi3t) + if gil_disabled and target_minor_version <= sys.version_info.minor: + sabi = _SabiMode.ABI3T + else: + logger.info( + "py-api {} requires free-threaded CPython >= 3.{}, ignoring", + py_api, + target_minor_version, + ) + else: + # Classic stable ABI (abi3) + target_minor_version = int(py_api[3:]) + if gil_disabled: + logger.info( + "Free-threaded Python doesn't support the classic Limited API, ignoring" + ) + elif target_minor_version <= sys.version_info.minor: + sabi = _SabiMode.ABI3 python_library = get_python_library(self.config.env, abi3=False) - python_sabi_library = ( - get_python_library(self.config.env, abi3=True) if limited_api else None - ) + python_sabi_library = None + if sabi == _SabiMode.ABI3T: + python_sabi_library = get_python_library(self.config.env, abi3t=True) + elif sabi == _SabiMode.ABI3: + python_sabi_library = get_python_library(self.config.env, abi3=True) python_include_dir = get_python_include_dir() numpy_include_dir = get_numpy_include_dir() @@ -265,20 +293,28 @@ def configure( if numpy_include_dir: cache_config[f"{prefix}_NumPy_INCLUDE_DIR"] = numpy_include_dir - cache_config["SKBUILD_SOABI"] = get_soabi(self.config.env, abi3=limited_api) + cache_config["SKBUILD_SOABI"] = get_soabi( + self.config.env, + abi3=(sabi == _SabiMode.ABI3), + abi3t=(sabi == _SabiMode.ABI3T), + ) # Allow CMakeLists to detect this is supposed to be a limited ABI build cache_config["SKBUILD_SABI_COMPONENT"] = ( - "Development.SABIModule" if limited_api else "" + "Development.SABIModule" if sabi != _SabiMode.NONE else "" ) # Allow users to detect the version requested in settings - py_api = self.settings.wheel.py_api - cache_config["SKBUILD_SABI_VERSION"] = ( - f"{py_api[2]}.{py_api[3:]}" - if limited_api and py_api.startswith("cp") - else "" - ) + if sabi != _SabiMode.NONE and py_api.startswith("cp"): + version_str = py_api[2:] + if version_str.endswith("t"): + version_str = version_str[:-1] + cache_config["SKBUILD_SABI_VERSION"] = f"{version_str[0]}.{version_str[1:]}" + else: + cache_config["SKBUILD_SABI_VERSION"] = "" + + if sabi == _SabiMode.ABI3T: + cache_config["Py_TARGET_ABI3T"] = "1" if cache_entries: cache_config.update(cache_entries) diff --git a/src/scikit_build_core/builder/sysconfig.py b/src/scikit_build_core/builder/sysconfig.py index 781681862..0a7879be3 100644 --- a/src/scikit_build_core/builder/sysconfig.py +++ b/src/scikit_build_core/builder/sysconfig.py @@ -44,7 +44,9 @@ def __dir__() -> list[str]: return __all__ -def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path | None: +def get_python_library( + env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False +) -> Path | None: # When cross-compiling, check DIST_EXTRA_CONFIG first config_file = env.get("DIST_EXTRA_CONFIG", None) if config_file and Path(config_file).is_file(): @@ -53,21 +55,24 @@ def get_python_library(env: Mapping[str, str], *, abi3: bool = False) -> Path | result = cp.get("build_ext", "library_dirs", fallback="") if result: logger.info("Reading DIST_EXTRA_CONFIG:build_ext.library_dirs={}", result) - minor = "" if abi3 else sys.version_info[1] - if env.get("SETUPTOOLS_EXT_SUFFIX", "").endswith("t.pyd"): - return Path(result) / f"python3{minor}t.lib" - return Path(result) / f"python3{minor}.lib" + minor = "" if (abi3 or abi3t) else sys.version_info[1] + suffix = "t" if abi3t else "" + return Path(result) / f"python3{minor}{suffix}.lib" libdirstr = sysconfig.get_config_var("LIBDIR") ldlibrarystr = sysconfig.get_config_var("LDLIBRARY") librarystr = sysconfig.get_config_var("LIBRARY") - if abi3: + if abi3 or abi3t: + if abi3t and sysconfig.get_config_var("Py_GIL_DISABLED"): + replacement = f"python3{sys.version_info[1]}t" + target = "python3t" + else: + replacement = f"python3{sys.version_info[1]}" + target = "python3" if ldlibrarystr is not None: - ldlibrarystr = ldlibrarystr.replace( - f"python3{sys.version_info[1]}", "python3" - ) + ldlibrarystr = ldlibrarystr.replace(replacement, target) if librarystr is not None: - librarystr = librarystr.replace(f"python3{sys.version_info[1]}", "python3") + librarystr = librarystr.replace(replacement, target) libdir: Path | None = libdirstr and Path(libdirstr) ldlibrary: Path | None = ldlibrarystr and Path(ldlibrarystr) @@ -158,7 +163,11 @@ def get_cmake_platform(env: Mapping[str, str] | None) -> str: return PLAT_TO_CMAKE.get(plat, plat) -def get_soabi(env: Mapping[str, str], *, abi3: bool = False) -> str: +def get_soabi( + env: Mapping[str, str], *, abi3: bool = False, abi3t: bool = False +) -> str: + if abi3t: + return "" if sysconfig.get_platform().startswith("win") else "abi3t" if abi3: return "" if sysconfig.get_platform().startswith("win") else "abi3" @@ -226,6 +235,11 @@ def info_print( get_python_library(os.environ, abi3=True), color=color, ) + rich_print( + "{bold}Detected ABI3T Python Library:", + get_python_library(os.environ, abi3t=True), + color=color, + ) rich_print( "{bold}Detected Python Include Directory:", get_python_include_dir(), @@ -251,6 +265,11 @@ def info_print( get_soabi(os.environ, abi3=True), color=color, ) + rich_print( + "{color}Detected ABI3T SOABI:", + get_soabi(os.environ, abi3t=True), + color=color, + ) rich_print( "{bold}Detected ABI flags:", get_abi_flags(), diff --git a/src/scikit_build_core/builder/wheel_tag.py b/src/scikit_build_core/builder/wheel_tag.py index 5b81e9b0e..098411c54 100644 --- a/src/scikit_build_core/builder/wheel_tag.py +++ b/src/scikit_build_core/builder/wheel_tag.py @@ -24,6 +24,35 @@ def __dir__() -> list[str]: return __all__ +class _PyTag: + """Helper for interrogating a single Python ABI tag like 'cp39' or 'cp315t'.""" + + def __init__(self, tag: str) -> None: + self._tag = tag + + @property + def is_classic_abi3(self) -> bool: + return self._tag.startswith("cp3") and self._tag[3:].isdecimal() + + @property + def is_ft_abi3(self) -> bool: + return ( + self._tag.startswith("cp3") + and self._tag.endswith("t") + and len(self._tag) > 4 + and self._tag[3:-1].isdecimal() + ) + + @property + def minor(self) -> int: + if self.is_ft_abi3: + return int(self._tag[3:-1]) + return int(self._tag[3:]) + + def __str__(self) -> str: + return self._tag + + @dataclasses.dataclass(frozen=True) class WheelTag: pyvers: list[str] @@ -99,30 +128,56 @@ def compute_best( if py_api: pyvers_new = py_api.split(".") - if all(x.startswith("cp3") and x[3:].isdecimal() for x in pyvers_new): - if len(pyvers_new) != 1: - msg = "Unexpected py-api, must be a single cp version (e.g. cp39), not {py_api}" - raise AssertionError(msg) + pytags = [_PyTag(x) for x in pyvers_new] + gil_disabled = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + if all(t.is_classic_abi3 or t.is_ft_abi3 for t in pytags): if root_is_purelib: msg = f"Unexpected py-api, since platlib is set to false, must be Pythonless (e.g. py2.py3), not {py_api}" raise AssertionError(msg) - minor = int(pyvers_new[0][3:]) - if ( - sys.implementation.name == "cpython" - and minor <= sys.version_info.minor - and not sysconfig.get_config_var("Py_GIL_DISABLED") - ): - pyvers = pyvers_new - abi = "abi3" - else: - msg = "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high or free-threaded" - logger.debug(msg, sys.implementation.name, minor) + classic_tags = [t for t in pytags if t.is_classic_abi3] + ft_tags = [t for t in pytags if t.is_ft_abi3] + + if sys.implementation.name == "cpython" and gil_disabled: + # Free-threaded: only accept cp3XXt tags + if ft_tags: + target = ft_tags[0] + if target.minor <= sys.version_info.minor: + pyvers = [str(target)] + abi = "abi3t" + else: + logger.debug( + "Ignoring py-api, version (3.{}) is too high", + target.minor, + ) + elif classic_tags: + logger.debug( + "Ignoring py-api, free-threaded Python doesn't support the classic Stable ABI" + ) + # Classic CPython + elif classic_tags: + target = classic_tags[0] + if ( + sys.implementation.name == "cpython" + and target.minor <= sys.version_info.minor + ): + pyvers = [str(target)] + abi = "abi3" + else: + logger.debug( + "Ignoring py-api, not a CPython interpreter ({}) or version (3.{}) is too high", + sys.implementation.name, + target.minor, + ) + elif ft_tags: + logger.debug( + "Ignoring py-api, free-threaded CPython is required for abi3t" + ) elif all(x.startswith("py") and x[2:].isdecimal() for x in pyvers_new): pyvers = pyvers_new abi = "none" else: - msg = f"Unexpected py-api, must be abi3 (e.g. cp39) or Pythonless (e.g. py2.py3), not {py_api}" + msg = f"Unexpected py-api, must be abi3 (e.g. cp39), abi3t (e.g. cp315t), or Pythonless (e.g. py2.py3), not {py_api}" raise AssertionError(msg) return cls(pyvers=pyvers, abis=[abi], archs=plats, build_tag=build_tag) @@ -174,7 +229,7 @@ def as_tags_set(self) -> frozenset[packaging.tags.Tag]: parser.add_argument( "--abi", default="", - help="Specify py-api, like 'cp38' or 'py3'", + help="Specify py-api, like 'cp38', 'cp315t', or 'py3'", ) parser.add_argument( "--purelib", @@ -182,5 +237,5 @@ def as_tags_set(self) -> frozenset[packaging.tags.Tag]: help="Specify a non-platlib (pure) tag", ) args = parser.parse_args() - tag = WheelTag.compute_best(args.archs, args.abi, root_is_purelib=args.purelib) - print(tag) # noqa: T201 + comp_tag = WheelTag.compute_best(args.archs, args.abi, root_is_purelib=args.purelib) + print(comp_tag) # noqa: T201 diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 63cf7474c..567fa0f97 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -289,8 +289,11 @@ class WheelSettings: You can also set this to "cp38" to enable the CPython 3.8+ Stable ABI / Limited API (only on CPython and if the version is sufficient, - otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to - ignore Python ABI compatibility. The ABI tag is inferred from this tag. + otherwise this has no effect). For free-threaded Python, you can use + "cp315t" to enable the free-threaded stable ABI (only on CPython + free-threaded builds and if the version is sufficient). Or you can set + it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI + tag is inferred from this tag. This value is used to construct ``SKBUILD_SABI_COMPONENT`` CMake variable. """ diff --git a/tests/test_builder.py b/tests/test_builder.py index d6de6fe4b..ed64fc340 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import platform import pprint @@ -24,10 +26,43 @@ CMakeSettings, CMakeSettingsDefine, ScikitBuildSettings, + SearchSettings, WheelSettings, ) +class VersionInfo: + """Minimal sys.version_info replacement for monkeypatching in tests.""" + + def __init__( + self, + major: int, + minor: int, + micro: int = 0, + releaselevel: str = "final", + serial: int = 0, + ) -> None: + self.major = major + self.minor = minor + self.micro = micro + self.releaselevel = releaselevel + self.serial = serial + + @classmethod + def from_str(cls, version: str) -> VersionInfo: + major, minor, *rest = (int(x) for x in version.split(".")) + micro = rest[0] if rest else 0 + return cls(major, minor, micro) + + def __getitem__(self, index: int) -> int | str: + return (self.major, self.minor, self.micro, self.releaselevel, self.serial)[ + index + ] + + def __ge__(self, other: tuple[int, ...]) -> bool: + return (self.major, self.minor, self.micro) >= other[:3] + + # The envvar_higher case shouldn't happen, but the compiler should cause the # correct failure @pytest.mark.parametrize( @@ -96,6 +131,10 @@ def test_get_python_library_xcompile(tmp_path): assert lib2 assert lib2 == Path("C:\\Python\\libs\\python3.lib") + lib3 = get_python_library(env, abi3t=True) + assert lib3 + assert lib3 == Path("C:\\Python\\libs\\python3t.lib") + @pytest.mark.parametrize("archs", [["x86_64"], ["arm64", "universal2"]]) def test_builder_macos_arch(monkeypatch, archs): @@ -149,6 +188,101 @@ def test_build_tool_args(): ) +def configure_builder_with_limited_api( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + *, + limited_api: bool | None, + py_api: str = "", +) -> str: + source_dir = tmp_path / "src" + source_dir.mkdir() + + config = CMaker( + CMake(Version("3.30"), Path("cmake")), + source_dir=source_dir, + build_dir=tmp_path / "build", + build_type="Release", + ) + monkeypatch.setattr(config, "configure", unittest.mock.Mock()) + + builder = Builder( + settings=ScikitBuildSettings( + search=SearchSettings(site_packages=False), + wheel=WheelSettings(py_api=py_api), + ), + config=config, + ) + monkeypatch.setattr(Builder, "_get_entry_point_search_path", lambda *_: {}) + + builder.configure(defines={}, limited_api=limited_api) + return config.init_cache_file.read_text(encoding="utf-8") + + +def patch_cpython_runtime(monkeypatch: pytest.MonkeyPatch) -> None: + implementation = vars(sys.implementation).copy() + implementation["name"] = "cpython" + monkeypatch.setattr( + sys, + "implementation", + SimpleNamespace(**implementation), + ) + + +def test_builder_limited_api_override_classic(tmp_path, monkeypatch): + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + patch_cpython_runtime(monkeypatch) + + cache = configure_builder_with_limited_api(tmp_path, monkeypatch, limited_api=True) + + expected_soabi = "" if sysconfig.get_platform().startswith("win") else "abi3" + assert "Development.SABIModule" in cache + assert f"set(SKBUILD_SOABI [===[{expected_soabi}]===] CACHE STRING" in cache + assert "Py_TARGET_ABI3T" not in cache + + +def test_builder_limited_api_override_free_threaded(tmp_path, monkeypatch): + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: "t" if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + patch_cpython_runtime(monkeypatch) + + cache = configure_builder_with_limited_api(tmp_path, monkeypatch, limited_api=True) + + expected_soabi = "" if sysconfig.get_platform().startswith("win") else "abi3t" + assert "Development.SABIModule" in cache + assert f"set(SKBUILD_SOABI [===[{expected_soabi}]===] CACHE STRING" in cache + assert "set(Py_TARGET_ABI3T [===[1]===] CACHE STRING" in cache + + +def test_builder_limited_api_auto_free_threaded(tmp_path, monkeypatch): + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: "t" if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + patch_cpython_runtime(monkeypatch) + monkeypatch.setattr(sys, "version_info", VersionInfo(3, 15)) + + cache = configure_builder_with_limited_api( + tmp_path, monkeypatch, limited_api=None, py_api="cp315t" + ) + + expected_soabi = "" if sysconfig.get_platform().startswith("win") else "abi3t" + assert "Development.SABIModule" in cache + assert f"set(SKBUILD_SOABI [===[{expected_soabi}]===] CACHE STRING" in cache + assert "set(Py_TARGET_ABI3T [===[1]===] CACHE STRING" in cache + + @pytest.mark.parametrize( ("minver", "archs", "answer"), [ @@ -269,6 +403,63 @@ def test_wheel_tag_with_abi_darwin(monkeypatch): assert str(tags) == "py2.py3-none-macosx_10_10_x86_64" +def test_wheel_tag_with_abi3t_darwin(monkeypatch): + """Test cp315t free-threaded stable ABI tag.""" + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: "t" if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.setattr(sys, "implementation", SimpleNamespace(name="cpython")) + monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10") + monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", "")) + + monkeypatch.setattr(sys, "version_info", VersionInfo(3, 15)) + + tags = WheelTag.compute_best(["x86_64"], py_api="cp315t") + assert str(tags) == "cp315t-abi3t-macosx_10_10_x86_64" + + tags = WheelTag.compute_best(["x86_64"], py_api="cp316t") + assert "abi3t" not in str(tags) + assert "cp316t" not in str(tags) + + +def test_wheel_tag_with_classic_abi3_ignored_on_free_threaded(monkeypatch): + """Test classic abi3 requests fall back to default tags on free-threaded Python.""" + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: "t" if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10") + monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", "")) + + default_tags = WheelTag.compute_best(["x86_64"]) + tags = WheelTag.compute_best(["x86_64"], py_api="cp38") + assert tags == default_tags + + +def test_wheel_tag_with_abi3t_ignored_on_classic(monkeypatch): + """Test cp315t falls back to default tags on classic (non-free-threaded) Python.""" + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: None if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + monkeypatch.setattr(sys, "platform", "darwin") + monkeypatch.setenv("MACOSX_DEPLOYMENT_TARGET", "10.10") + monkeypatch.setattr(platform, "mac_ver", lambda: ("10.9.2", "", "")) + + default_tags = WheelTag.compute_best(["x86_64"]) + tags = WheelTag.compute_best(["x86_64"], py_api="cp315t") + assert tags == default_tags + + def test_wheel_tag_host_platform_override(monkeypatch): """Test that _PYTHON_HOST_PLATFORM environment variable overrides platform detection.""" get_config_var = sysconfig.get_config_var @@ -288,6 +479,24 @@ def test_wheel_tag_host_platform_override(monkeypatch): assert str(tags) == "py3-none-emscripten_4_0_9_wasm32" +def test_get_soabi_abi3t(monkeypatch): + get_config_var = sysconfig.get_config_var + monkeypatch.setattr( + sysconfig, + "get_config_var", + lambda x: "t" if x == "Py_GIL_DISABLED" else get_config_var(x), + ) + + from scikit_build_core.builder.sysconfig import get_soabi + + assert get_soabi({}, abi3t=True) == ( + "" if sysconfig.get_platform().startswith("win") else "abi3t" + ) + assert get_soabi({}, abi3=True) == ( + "" if sysconfig.get_platform().startswith("win") else "abi3" + ) + + def test_generator_args(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): monkeypatch.chdir(tmp_path) builder = Builder(