From 232008aa245e8d70b0107650d6a88ffcd26f1be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vard=C3=AB?= Date: Tue, 29 Sep 2026 01:29:18 +0200 Subject: [PATCH 1/2] add support for PEP 803 abi3t free-threaded stable ABI --- hatch_rs/plugin.py | 1 + hatch_rs/structs.py | 73 ++++++++++++++- hatch_rs/tests/test_project_basic/Cargo.lock | 92 ++++++------------- hatch_rs/tests/test_project_basic/Cargo.toml | 2 +- .../test_project_editable_debug/Cargo.lock | 92 ++++++------------- .../test_project_editable_debug/Cargo.toml | 2 +- .../tests/test_project_pyodide/Cargo.lock | 4 +- .../tests/test_project_pyodide/Cargo.toml | 2 +- hatch_rs/tests/test_structs.py | 66 ++++++++++++- 9 files changed, 198 insertions(+), 136 deletions(-) diff --git a/hatch_rs/plugin.py b/hatch_rs/plugin.py index 8b0a0bb..2d8ee25 100644 --- a/hatch_rs/plugin.py +++ b/hatch_rs/plugin.py @@ -90,6 +90,7 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None: build_data["pure_python"] = False build_data["tag"] = wheel_tag( abi3=config.abi3, + free_threaded=config.free_threaded, resolved_target=build_plan.resolved_target, platform_tag=config.wheel_platform_tag, ) diff --git a/hatch_rs/structs.py b/hatch_rs/structs.py index f135a56..2483da0 100644 --- a/hatch_rs/structs.py +++ b/hatch_rs/structs.py @@ -1,5 +1,7 @@ from __future__ import annotations +import sys +import sysconfig from ctypes import CDLL from dataclasses import dataclass from glob import glob @@ -122,6 +124,42 @@ def _env_truthy(value: str | None) -> bool: return value.strip().lower() not in ("", "0", "false", "no", "off") +def _resolve_free_threaded( + free_threaded: bool | None = None, + *, + python_version: tuple[int, int] | None = None, +) -> bool: + """Return True when targeting or executing under free-threaded CPython.""" + if free_threaded is not None: + if free_threaded and python_version is not None and python_version < (3, 13): + raise ValueError(f"Free-threaded builds require Python 3.13 or newer, got {python_version[0]}.{python_version[1]}.") + return free_threaded + + if python_version is not None and python_version < (3, 13): + return False + + env_val = environ.get("HATCH_RUST_FREE_THREADED") + if env_val is not None: + return _env_truthy(env_val) + + if python_version is not None and python_version != (version_info.major, version_info.minor): + return False + + if sysconfig.get_config_var("Py_GIL_DISABLED") == 1: + return True + + if "t" in getattr(sys, "abiflags", ""): + return True + + if hasattr(sys, "abi_info") and sys.abi_info.free_threaded: + return True + + if hasattr(sys, "_is_gil_enabled"): + return not sys._is_gil_enabled() + + return False + + def _normalize_machine(machine: str) -> str: normalized = machine.lower().replace("-", "_") aliases = { @@ -219,19 +257,24 @@ def python_extension_name( source_stem: str, *, abi3: bool = False, + free_threaded: bool | None = None, platform: str | None = None, python_version: tuple[int, int] | None = None, ) -> str: """Render the Python extension filename for a Cargo cdylib artifact stem.""" platform = _normalize_platform(platform or environ.get("HATCH_RUST_PLATFORM", sys_platform)) module_name = source_stem.removeprefix("lib") + major, minor = python_version or (version_info.major, version_info.minor) if platform == "win32": return f"{module_name}.pyd" if platform == "emscripten": - major, minor = python_version or (version_info.major, version_info.minor) return f"{module_name}.cpython-{major}{minor}-wasm32-emscripten.so" if abi3: - return f"{module_name}.abi3.so" + return ( + f"{module_name}.abi3t.so" + if _resolve_free_threaded(free_threaded, python_version=(major, minor)) and (major, minor) >= (3, 15) + else f"{module_name}.abi3.so" + ) return f"{module_name}.so" @@ -321,6 +364,7 @@ def _wheel_platform(resolved_target: ResolvedTarget, platform_tag: str | None) - def wheel_tag( *, abi3: bool = False, + free_threaded: bool | None = None, target: str | None = None, platform: str | None = None, machine: str | None = None, @@ -331,7 +375,18 @@ def wheel_tag( """Render a wheel tag for the resolved Rust target using packaging.tags.""" resolved = resolved_target or _resolve_target(target, platform=platform, machine=machine) version = python_version or (version_info.major, version_info.minor) - abis = [f"cp{version[0]}{version[1]}"] if resolved.platform == "emscripten" else (["abi3"] if abi3 else None) + is_freethreaded = _resolve_free_threaded(free_threaded, python_version=version) + if resolved.platform == "emscripten": + abis = [f"cp{version[0]}{version[1]}"] + elif abi3: + if is_freethreaded: + abis = ["abi3t"] if version >= (3, 15) else [f"cp{version[0]}{version[1]}t"] + else: + abis = ["abi3"] + elif is_freethreaded: + abis = [f"cp{version[0]}{version[1]}t"] + else: + abis = None return str(next(cpython_tags(python_version=version, abis=abis, platforms=[_wheel_platform(resolved, platform_tag)]))) @@ -646,6 +701,11 @@ class HatchRustBuildConfig(BaseModel): default=False, description="If True, build the extension with Python's ABI3 compatibility.", ) + free_threaded: bool | None = Field( + default=None, + alias="free-threaded", + description="If True, build for free-threaded CPython. If None, auto-detected from environment.", + ) target: str | None = Field( default=None, @@ -1035,7 +1095,12 @@ def _format_destination( ) -> Path: artifact = planned_artifact.artifact extension_stem = artifact.python_extension_name or _cargo_artifact_stem(source) - python_extension = python_extension_name(extension_stem, abi3=self.abi3, platform=planned_artifact.resolved_target.platform) + python_extension = python_extension_name( + extension_stem, + abi3=self.abi3, + free_threaded=self.free_threaded, + platform=planned_artifact.resolved_target.platform, + ) values = { "module": self.module, "target": planned_artifact.resolved_target.triple, diff --git a/hatch_rs/tests/test_project_basic/Cargo.lock b/hatch_rs/tests/test_project_basic/Cargo.lock index 175c2c2..354bb44 100644 --- a/hatch_rs/tests/test_project_basic/Cargo.lock +++ b/hatch_rs/tests/test_project_basic/Cargo.lock @@ -2,65 +2,44 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "memoffset" -version = "0.9.1" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -74,37 +53,33 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "indoc", "inventory", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -112,9 +87,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -124,37 +99,36 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -163,18 +137,12 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unindent" -version = "0.2.4" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" diff --git a/hatch_rs/tests/test_project_basic/Cargo.toml b/hatch_rs/tests/test_project_basic/Cargo.toml index 54792fb..fa69e1e 100644 --- a/hatch_rs/tests/test_project_basic/Cargo.toml +++ b/hatch_rs/tests/test_project_basic/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25", features = ["abi3-py39", "extension-module", "multiple-pymethods"] } +pyo3 = { version = "0.29.2", features = ["abi3-py39", "extension-module", "multiple-pymethods"] } [profile.release] panic = 'abort' diff --git a/hatch_rs/tests/test_project_editable_debug/Cargo.lock b/hatch_rs/tests/test_project_editable_debug/Cargo.lock index 175c2c2..354bb44 100644 --- a/hatch_rs/tests/test_project_editable_debug/Cargo.lock +++ b/hatch_rs/tests/test_project_editable_debug/Cargo.lock @@ -2,65 +2,44 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] [[package]] name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "memoffset" -version = "0.9.1" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -74,37 +53,33 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "indoc", "inventory", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -112,9 +87,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -124,37 +99,36 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -163,18 +137,12 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unindent" -version = "0.2.4" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" diff --git a/hatch_rs/tests/test_project_editable_debug/Cargo.toml b/hatch_rs/tests/test_project_editable_debug/Cargo.toml index 8fb6b75..81a6f00 100644 --- a/hatch_rs/tests/test_project_editable_debug/Cargo.toml +++ b/hatch_rs/tests/test_project_editable_debug/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25", features = ["abi3-py39", "extension-module", "multiple-pymethods"] } +pyo3 = { version = "0.29.2", features = ["abi3-py39", "extension-module", "multiple-pymethods"] } [profile.release] panic = 'abort' diff --git a/hatch_rs/tests/test_project_pyodide/Cargo.lock b/hatch_rs/tests/test_project_pyodide/Cargo.lock index d59f44b..014fddf 100644 --- a/hatch_rs/tests/test_project_pyodide/Cargo.lock +++ b/hatch_rs/tests/test_project_pyodide/Cargo.lock @@ -127,6 +127,6 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" diff --git a/hatch_rs/tests/test_project_pyodide/Cargo.toml b/hatch_rs/tests/test_project_pyodide/Cargo.toml index 4305006..5e60c8d 100644 --- a/hatch_rs/tests/test_project_pyodide/Cargo.toml +++ b/hatch_rs/tests/test_project_pyodide/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.29.0", features = ["abi3", "extension-module"] } +pyo3 = { version = "0.29.2", features = ["abi3", "extension-module"] } [profile.release] panic = "abort" diff --git a/hatch_rs/tests/test_structs.py b/hatch_rs/tests/test_structs.py index d97f742..c353f85 100644 --- a/hatch_rs/tests/test_structs.py +++ b/hatch_rs/tests/test_structs.py @@ -6,7 +6,14 @@ import pytest -from hatch_rs.structs import HatchRustBuildPlan, executable_name, python_extension_name, resolve_target_triple, shared_library_name, wheel_tag +from hatch_rs.structs import ( + HatchRustBuildPlan, + executable_name, + python_extension_name, + resolve_target_triple, + shared_library_name, + wheel_tag, +) @pytest.fixture(autouse=True) @@ -14,6 +21,7 @@ def clear_cargo_target_dir(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("CARGO_TARGET_DIR", raising=False) monkeypatch.delenv("CARGO_BUILD_TARGET", raising=False) monkeypatch.delenv("PYODIDE_ABI_VERSION", raising=False) + monkeypatch.delenv("HATCH_RUST_FREE_THREADED", raising=False) @pytest.mark.parametrize( @@ -70,12 +78,47 @@ def test_resolve_target_triple_rejects_macos_universal2_as_rust_target(): ({"target": "aarch64-apple-darwin"}, "cp311-cp311-macosx_11_0_arm64"), ({"target": "x86_64-pc-windows-msvc"}, "cp311-cp311-win_amd64"), ({"abi3": True, "target": "aarch64-unknown-linux-musl"}, "cp311-abi3-musllinux_1_2_aarch64"), - ({"target": "x86_64-unknown-linux-gnu", "platform_tag": "manylinux_2_28_x86_64"}, "cp311-cp311-manylinux_2_28_x86_64"), + ( + {"target": "x86_64-unknown-linux-gnu", "platform_tag": "manylinux_2_28_x86_64"}, + "cp311-cp311-manylinux_2_28_x86_64", + ), + ( + {"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)}, + "cp315-abi3t-linux_x86_64", + ), + ( + {"abi3": True, "free_threaded": True, "target": "aarch64-apple-darwin", "python_version": (3, 15)}, + "cp315-abi3t-macosx_11_0_arm64", + ), + ( + {"abi3": True, "free_threaded": True, "target": "x86_64-pc-windows-msvc", "python_version": (3, 15)}, + "cp315-abi3t-win_amd64", + ), + ( + {"abi3": True, "free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 13)}, + "cp313-cp313t-linux_x86_64", + ), + ( + {"free_threaded": True, "target": "x86_64-unknown-linux-gnu", "python_version": (3, 15)}, + "cp315-cp315t-linux_x86_64", + ), ], ) def test_wheel_tag_uses_packaging_tags(monkeypatch: pytest.MonkeyPatch, kwargs: dict[str, object], expected: str): monkeypatch.delenv("AUDITWHEEL_PLAT", raising=False) - assert wheel_tag(python_version=(3, 11), **kwargs) == expected + kwargs_copy = dict(kwargs) + python_version = kwargs_copy.pop("python_version", (3, 11)) + assert wheel_tag(python_version=python_version, **kwargs_copy) == expected + + +def test_wheel_tag_uses_free_threaded_env(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("HATCH_RUST_FREE_THREADED", "1") + assert wheel_tag(abi3=True, target="aarch64-apple-darwin", python_version=(3, 15)) == "cp315-abi3t-macosx_11_0_arm64" + + +def test_wheel_tag_rejects_free_threaded_for_python_under_313(): + with pytest.raises(ValueError, match="Free-threaded builds require Python 3.13 or newer"): + wheel_tag(free_threaded=True, target="x86_64-unknown-linux-gnu", python_version=(3, 12)) def test_wheel_tag_uses_auditwheel_platform(monkeypatch: pytest.MonkeyPatch): @@ -124,6 +167,23 @@ def test_python_extension_name(source_stem: str, platform: str, abi3: bool, expe assert python_extension_name(source_stem, abi3=abi3, platform=platform, python_version=(3, 14)) == expected +@pytest.mark.parametrize( + ("source_stem", "platform", "expected"), + [ + ("libproject", "linux", "project.abi3t.so"), + ("libproject", "darwin", "project.abi3t.so"), + ("project", "win32", "project.pyd"), + ], +) +def test_python_extension_name_abi3t(source_stem: str, platform: str, expected: str): + assert python_extension_name(source_stem, abi3=True, free_threaded=True, platform=platform, python_version=(3, 15)) == expected + + +def test_python_extension_name_rejects_free_threaded_for_python_under_313(): + with pytest.raises(ValueError, match="Free-threaded builds require Python 3.13 or newer"): + python_extension_name("libproject", abi3=True, free_threaded=True, platform="linux", python_version=(3, 12)) + + def test_build_plan_copies_emscripten_python_extension(tmp_path): plan = HatchRustBuildPlan( module="project", From f6209a1b3aa4f25026fce10d4ec6d37f131afb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vard=C3=AB?= Date: Tue, 29 Sep 2026 01:30:06 +0200 Subject: [PATCH 2/2] improve test environment robustness --- hatch_rs/tests/test_projects.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hatch_rs/tests/test_projects.py b/hatch_rs/tests/test_projects.py index f9ddb42..e7179ad 100644 --- a/hatch_rs/tests/test_projects.py +++ b/hatch_rs/tests/test_projects.py @@ -1,5 +1,5 @@ from json import loads -from os import environ, listdir +from os import environ, listdir, pathsep from os.path import dirname from pathlib import Path from shutil import rmtree @@ -18,9 +18,9 @@ def _subprocess_env(): env = environ.copy() pythonpath = str(REPO_ROOT) if env.get("PYTHONPATH"): - pythonpath = f"{pythonpath}:{env['PYTHONPATH']}" + pythonpath = f"{pythonpath}{pathsep}{env['PYTHONPATH']}" env["PYTHONPATH"] = pythonpath - env["PATH"] = f"{dirname(executable)}:{env['PATH']}" + env["PATH"] = f"{dirname(executable)}{pathsep}{env['PATH']}" return env @@ -319,6 +319,8 @@ def test_editable_debug(self): # install project in development mode (editable install) check_call( [ + executable, + "-m", "pip", "install", "--verbose",