Skip to content

Commit 227ea6a

Browse files
authored
feat: free-threaded python support (#203)
1 parent 9c6fbf6 commit 227ea6a

File tree

3 files changed

+41
-4
lines changed

3 files changed

+41
-4
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dynamic = [
4141
]
4242
dependencies = [
4343
"packaging>=24.2",
44-
"tox>=4.24.1,<5",
44+
"tox>=4.26,<5",
4545
"typing-extensions>=4.12.2; python_version<'3.10'",
4646
"uv>=0.5.31,<1",
4747
]

src/tox_uv/_venv.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import os
88
import sys
9+
import sysconfig
910
from abc import ABC
1011
from functools import cached_property
1112
from importlib.resources import as_file, files
@@ -131,6 +132,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR
131132
is_64=sys.maxsize > 2**32,
132133
platform=sys.platform,
133134
extra={},
135+
free_threaded=sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
134136
)
135137
base_path = Path(base)
136138
if base_path.is_absolute():
@@ -142,6 +144,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR
142144
is_64=info.architecture == 64, # noqa: PLR2004
143145
platform=info.platform,
144146
extra={"executable": base},
147+
free_threaded=info.free_threaded,
145148
)
146149
spec = PythonSpec.from_string_spec(base)
147150
return PythonInfo(
@@ -157,6 +160,7 @@ def _get_python(self, base_python: list[str]) -> PythonInfo | None: # noqa: PLR
157160
is_64=spec.architecture == 64, # noqa: PLR2004
158161
platform=sys.platform,
159162
extra={"architecture": spec.architecture},
163+
free_threaded=spec.free_threaded,
160164
)
161165

162166
return None # pragma: no cover
@@ -256,25 +260,28 @@ def env_version_spec(self) -> str:
256260
imp = self.base_python.impl_lower
257261
executable = self.base_python.extra.get("executable")
258262
architecture = self.base_python.extra.get("architecture")
263+
free_threaded = self.base_python.free_threaded
259264
if executable:
260265
version_spec = str(executable)
261266
elif (
262267
architecture is None
263268
and (base.major, base.minor) == sys.version_info[:2]
264269
and (sys.implementation.name.lower() == imp)
270+
and ((sysconfig.get_config_var("Py_GIL_DISABLED") == 1) == free_threaded)
265271
):
266272
version_spec = sys.executable
267273
else:
268274
uv_imp = imp or ""
275+
free_threaded_tag = "+freethreaded" if free_threaded else ""
269276
if not base.major:
270277
version_spec = f"{uv_imp}"
271278
elif not base.minor:
272-
version_spec = f"{uv_imp}{base.major}"
279+
version_spec = f"{uv_imp}{base.major}{free_threaded_tag}"
273280
elif architecture is not None and self.base_python.platform == "win32":
274281
uv_arch = {32: "x86", 64: "x86_64"}[architecture]
275-
version_spec = f"{uv_imp}-{base.major}.{base.minor}-windows-{uv_arch}-none"
282+
version_spec = f"{uv_imp}-{base.major}.{base.minor}{free_threaded_tag}-windows-{uv_arch}-none"
276283
else:
277-
version_spec = f"{uv_imp}{base.major}.{base.minor}"
284+
version_spec = f"{uv_imp}{base.major}.{base.minor}{free_threaded_tag}"
278285
return version_spec
279286

280287
@cached_property

tests/test_tox_uv_venv.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,14 @@ def test_get_python_architecture(base_python: str, architecture: int | None) ->
447447
assert python_info.extra["architecture"] == architecture
448448

449449

450+
@pytest.mark.parametrize(("base_python", "is_free_threaded"), [("py313", False), ("py313t", True)])
451+
def test_get_python_free_threaded(base_python: str, is_free_threaded: int | None) -> None:
452+
uv_venv = _TestUvVenv(create_args=mock.Mock())
453+
python_info = uv_venv.get_python_info(base_python)
454+
assert python_info is not None
455+
assert python_info.free_threaded == is_free_threaded
456+
457+
450458
def test_env_version_spec_no_architecture() -> None:
451459
uv_venv = _TestUvVenv(create_args=mock.MagicMock())
452460
python_info = PythonInfo(
@@ -510,3 +518,25 @@ def test_env_version_spec_architecture_configured_overwrite_sys_exe() -> None:
510518
)
511519
uv_venv.set_base_python(python_info)
512520
assert uv_venv.env_version_spec() == f"cpython-{major}.{minor}-windows-x86-none"
521+
522+
523+
def test_env_version_spec_free_threaded() -> None:
524+
uv_venv = _TestUvVenv(create_args=mock.MagicMock())
525+
python_info = PythonInfo(
526+
implementation="cpython",
527+
version_info=VersionInfo(
528+
major=3,
529+
minor=13,
530+
micro=3,
531+
releaselevel="",
532+
serial=0,
533+
),
534+
version="",
535+
is_64=True,
536+
platform="win32",
537+
extra={"architecture": None},
538+
free_threaded=True,
539+
)
540+
uv_venv.set_base_python(python_info)
541+
with mock.patch("sys.version_info", (0, 0, 0)): # prevent picking sys.executable
542+
assert uv_venv.env_version_spec() == "cpython3.13+freethreaded"

0 commit comments

Comments
 (0)