From 1ddd74cbe2ab964c7cb0ce5b58ed23c2c28b1266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Su=C3=A1stegui?= Date: Mon, 1 Jun 2026 05:06:58 -0600 Subject: [PATCH] fix: support free-threaded Python aliases --- src/hatch/python/core.py | 11 ++++++++--- src/hatch/python/distributions.py | 2 ++ src/hatch/python/resolve.py | 21 ++++++++++++++++++--- tests/python/test_core.py | 2 +- tests/python/test_resolve.py | 7 +++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/hatch/python/core.py b/src/hatch/python/core.py index b05d923f9..e63f63f7a 100644 --- a/src/hatch/python/core.py +++ b/src/hatch/python/core.py @@ -2,7 +2,8 @@ from typing import TYPE_CHECKING, Any -from hatch.python.distributions import DISTRIBUTIONS, ORDERED_DISTRIBUTIONS +from hatch.errors import HatchError +from hatch.python.distributions import ORDERED_DISTRIBUTIONS from hatch.python.resolve import get_distribution from hatch.utils.fs import temp_directory @@ -62,7 +63,7 @@ def get_installed(self) -> dict[str, InstalledDistribution]: installed_distributions: list[InstalledDistribution] = [] for path in self.directory.iterdir(): - if not (path.name in DISTRIBUTIONS and path.is_dir()): + if not path.is_dir(): continue metadata_file = path / InstalledDistribution.metadata_filename() @@ -70,7 +71,11 @@ def get_installed(self) -> dict[str, InstalledDistribution]: continue metadata = json.loads(metadata_file.read_text()) - distribution = get_distribution(path.name, source=metadata.get("source", "")) + try: + distribution = get_distribution(path.name, source=metadata.get("source", "")) + except HatchError: + continue + if not (path / distribution.python_path).is_file(): continue diff --git a/src/hatch/python/distributions.py b/src/hatch/python/distributions.py index 324fec070..d5edcd425 100644 --- a/src/hatch/python/distributions.py +++ b/src/hatch/python/distributions.py @@ -9,7 +9,9 @@ '3.11', '3.12', '3.13', + '3.13t', '3.14', + '3.14t', 'pypy2.7', 'pypy3.9', 'pypy3.10', diff --git a/src/hatch/python/resolve.py b/src/hatch/python/resolve.py index 1cf832827..124560b6d 100644 --- a/src/hatch/python/resolve.py +++ b/src/hatch/python/resolve.py @@ -114,7 +114,7 @@ def python_path(self) -> str: if (custom_path := get_custom_path(self.name)) is not None: return custom_path - if self.name == "3.7": + if self.name == "3.7" or "freethreaded" in self.source: if sys.platform == "win32": return r"python\install\python.exe" @@ -159,7 +159,9 @@ def get_distribution(name: str, source: str = "", variant_cpu: str = "", variant if source: return _get_distribution_class(source)(name, source) - if name not in DISTRIBUTIONS: + distribution_name, inferred_variant_gil = _normalize_distribution_name(name) + + if distribution_name not in DISTRIBUTIONS: message = f"Unknown distribution: {name}" raise PythonDistributionUnknownError(message) @@ -177,12 +179,15 @@ def get_distribution(name: str, source: str = "", variant_cpu: str = "", variant if not variant_cpu: variant_cpu = _get_default_variant_cpu(name, system, arch) + if not variant_gil: + variant_gil = inferred_variant_gil + if not variant_gil: variant_gil = _get_default_variant_gil() key = (system, arch, abi, variant_cpu, variant_gil) - keys: dict[tuple, str] = DISTRIBUTIONS[name] + keys: dict[tuple, str] = DISTRIBUTIONS[distribution_name] if key not in keys: message = f"Could not find a default source for {name=} {system=} {arch=} {abi=} {variant_cpu=} {variant_gil=}" raise PythonDistributionResolutionError(message) @@ -269,6 +274,16 @@ def _get_default_variant_gil() -> str: return os.environ.get("HATCH_PYTHON_VARIANT_GIL", "").lower() +def _normalize_distribution_name(name: str) -> tuple[str, str]: + if name.endswith("t"): + base_name = name[:-1] + + if base_name in DISTRIBUTIONS and base_name[0].isdigit(): + return base_name, "freethreaded" + + return name, "" + + def _get_distribution_class(source: str) -> type[Distribution]: if "/python-build-standalone/releases/download/" in source: return CPythonStandaloneDistribution diff --git a/tests/python/test_core.py b/tests/python/test_core.py index c4ccd5101..5280fbdaf 100644 --- a/tests/python/test_core.py +++ b/tests/python/test_core.py @@ -35,7 +35,7 @@ def test_installation(temp_dir, platform, current_arch, name): output = platform.check_command_output([python_path, "-c", "import sys;print(sys.executable)"]).strip() assert output == str(python_path) - major_minor = name.replace("pypy", "") + major_minor = name.rstrip("t").replace("pypy", "") output = platform.check_command_output([python_path, "--version"]).strip() diff --git a/tests/python/test_resolve.py b/tests/python/test_resolve.py index b94a6513c..0f95aa2d3 100644 --- a/tests/python/test_resolve.py +++ b/tests/python/test_resolve.py @@ -157,6 +157,13 @@ def mock_open(path, *args, **kwargs): class TestVariantGIL: + def test_free_threaded_distribution_alias(self): + dist = get_distribution("3.13t") + + assert dist.name == "3.13t" + assert "freethreaded" in dist.source + assert dist.python_path == "python/install/bin/python3" + def test_compatible(self): with EnvVars({"HATCH_PYTHON_VARIANT_GIL": "freethreaded"}): dist = get_distribution("3.13")