From 7cfa06bbd00c46eeed33e109cd578c708269d278 Mon Sep 17 00:00:00 2001 From: Kevin Neal Date: Fri, 23 May 2025 23:46:56 -0700 Subject: [PATCH 1/2] feat: free-threaded python support --- pyproject.toml | 2 +- src/tox_uv/_venv.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b80cdc..7ba0360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dynamic = [ ] dependencies = [ "packaging>=24.2", - "tox>=4.24.1,<5", + "tox>=4.26,<5", "typing-extensions>=4.12.2; python_version<'3.10'", "uv>=0.5.31,<1", ] diff --git a/src/tox_uv/_venv.py b/src/tox_uv/_venv.py index 1d93362..922fc49 100644 --- a/src/tox_uv/_venv.py +++ b/src/tox_uv/_venv.py @@ -6,6 +6,7 @@ import logging import os import sys +import sysconfig from abc import ABC from functools import cached_property from importlib.resources import as_file, files @@ -131,6 +132,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR is_64=sys.maxsize > 2**32, platform=sys.platform, extra={}, + free_threaded=sysconfig.get_config_var("Py_GIL_DISABLED") == 1, ) base_path = Path(base) if base_path.is_absolute(): @@ -142,6 +144,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR is_64=info.architecture == 64, # noqa: PLR2004 platform=info.platform, extra={"executable": base}, + free_threaded=info.free_threaded, ) spec = PythonSpec.from_string_spec(base) return PythonInfo( @@ -157,6 +160,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR is_64=spec.architecture == 64, # noqa: PLR2004 platform=sys.platform, extra={"architecture": spec.architecture}, + free_threaded=spec.free_threaded, ) return None # pragma: no cover @@ -256,25 +260,28 @@ def env_version_spec(self) -> str: imp = self.base_python.impl_lower executable = self.base_python.extra.get("executable") architecture = self.base_python.extra.get("architecture") + free_threaded = self.base_python.free_threaded if executable: version_spec = str(executable) elif ( architecture is None and (base.major, base.minor) == sys.version_info[:2] and (sys.implementation.name.lower() == imp) + and ((sysconfig.get_config_var("Py_GIL_DISABLED") == 1) == free_threaded) ): version_spec = sys.executable else: uv_imp = imp or "" + free_threaded_tag = "+freethreaded" if free_threaded else "" if not base.major: version_spec = f"{uv_imp}" elif not base.minor: - version_spec = f"{uv_imp}{base.major}" + version_spec = f"{uv_imp}{base.major}{free_threaded_tag}" elif architecture is not None and self.base_python.platform == "win32": uv_arch = {32: "x86", 64: "x86_64"}[architecture] - version_spec = f"{uv_imp}-{base.major}.{base.minor}-windows-{uv_arch}-none" + version_spec = f"{uv_imp}-{base.major}.{base.minor}{free_threaded_tag}-windows-{uv_arch}-none" else: - version_spec = f"{uv_imp}{base.major}.{base.minor}" + version_spec = f"{uv_imp}{base.major}.{base.minor}{free_threaded_tag}" return version_spec @cached_property From 2b12473cb8c231bef835891200475ef81baacc5b Mon Sep 17 00:00:00 2001 From: Kevin Neal Date: Sun, 25 May 2025 22:21:31 -0700 Subject: [PATCH 2/2] test: add functional test coverage for free threaded --- tests/test_tox_uv_venv.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_tox_uv_venv.py b/tests/test_tox_uv_venv.py index e23a3af..e57b434 100644 --- a/tests/test_tox_uv_venv.py +++ b/tests/test_tox_uv_venv.py @@ -447,6 +447,14 @@ def test_get_python_architecture(base_python: str, architecture: int | None) -> assert python_info.extra["architecture"] == architecture +@pytest.mark.parametrize(("base_python", "is_free_threaded"), [("py313", False), ("py313t", True)]) +def test_get_python_free_threaded(base_python: str, is_free_threaded: int | None) -> None: + uv_venv = _TestUvVenv(create_args=mock.Mock()) + python_info = uv_venv.get_python_info(base_python) + assert python_info is not None + assert python_info.free_threaded == is_free_threaded + + def test_env_version_spec_no_architecture() -> None: uv_venv = _TestUvVenv(create_args=mock.MagicMock()) python_info = PythonInfo( @@ -510,3 +518,25 @@ def test_env_version_spec_architecture_configured_overwrite_sys_exe() -> None: ) uv_venv.set_base_python(python_info) assert uv_venv.env_version_spec() == f"cpython-{major}.{minor}-windows-x86-none" + + +def test_env_version_spec_free_threaded() -> None: + uv_venv = _TestUvVenv(create_args=mock.MagicMock()) + python_info = PythonInfo( + implementation="cpython", + version_info=VersionInfo( + major=3, + minor=13, + micro=3, + releaselevel="", + serial=0, + ), + version="", + is_64=True, + platform="win32", + extra={"architecture": None}, + free_threaded=True, + ) + uv_venv.set_base_python(python_info) + with mock.patch("sys.version_info", (0, 0, 0)): # prevent picking sys.executable + assert uv_venv.env_version_spec() == "cpython3.13+freethreaded"