Skip to content

Commit 00c55f0

Browse files
ngoldbaumCarreaucjames23
authored
Add CI coverage for free-threaded Python 3.14 (#2125)
* add free-threaded builds to github actions CI matrix * fix test_default_as_json * find FT build in prefered versoin * sysconf * freethreaded * use +t in one more test * fix one more test * fix padding --------- Co-authored-by: M Bussonnier <bussonniermatthias@gmail.com> Co-authored-by: Cary Hawkins <hawkinscary23@gmail.com>
1 parent 962260e commit 00c55f0

6 files changed

Lines changed: 41 additions & 11 deletions

File tree

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ jobs:
2424
fail-fast: false
2525
matrix:
2626
os: [ubuntu-latest, windows-latest, macos-latest]
27-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
27+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
2828

2929
steps:
3030
- uses: actions/checkout@v4
3131

3232
- name: Set up Python ${{ matrix.python-version }}
33-
uses: actions/setup-python@v5
33+
uses: actions/setup-python@v6
3434
with:
3535
python-version: ${{ matrix.python-version }}
3636

src/hatch/cli/run/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,30 @@ def run(ctx: click.Context, args: tuple[str, ...]):
9090
if "python" not in config and (requires_python := metadata.get("requires-python")) is not None:
9191
import re
9292
import sys
93+
import sysconfig
9394

9495
from packaging.specifiers import SpecifierSet
9596

9697
from hatch.python.distributions import DISTRIBUTIONS
9798

9899
current_version = ".".join(map(str, sys.version_info[:2]))
100+
if bool(sysconfig.get_config_var("Py_GIL_DISABLED")):
101+
current_version += "t"
102+
103+
# Strip "t" suffix for distribution lookup since DISTRIBUTIONS keys don't include it
104+
current_version_base = current_version.rstrip("t")
99105
distributions = [name for name in DISTRIBUTIONS if re.match(r"^\d+\.\d+$", name)]
100-
distributions.sort(key=lambda name: name != current_version)
106+
distributions.sort(key=lambda name: name != current_version_base)
101107

102108
python_constraint = SpecifierSet(requires_python)
103109
for distribution in distributions:
104110
# Try an artificially high patch version to account for
105111
# common cases like `>=3.11.4` or `>=3.10,<3.11`
106112
if python_constraint.contains(f"{distribution}.100"):
107-
config["python"] = distribution
113+
# Only set config["python"] if it doesn't match the current Python's base version
114+
# This allows free-threaded builds (e.g. 3.14t) to match their base version (3.14)
115+
if distribution != current_version_base:
116+
config["python"] = distribution
108117
break
109118
else:
110119
app.abort(f"Unable to satisfy Python version constraint: {requires_python}")

src/hatch/env/internal/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ def get_default_config() -> dict[str, Any]:
2121
"cov-combine": "coverage combine",
2222
"cov-report": "coverage report",
2323
},
24-
"matrix": [{"python": ["3.14", "3.13", "3.12", "3.11", "3.10"]}],
24+
"matrix": [{"python": ["3.14", "3.14t", "3.13", "3.12", "3.11", "3.10"]}],
2525
}

src/hatch/env/virtual.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import sys
5+
import sysconfig
56
from contextlib import contextmanager, nullcontext, suppress
67
from functools import cached_property
78
from os.path import isabs
@@ -15,6 +16,8 @@
1516
from hatch.utils.structures import EnvVars
1617
from hatch.venv.core import UVVirtualEnv, VirtualEnv
1718

19+
FREETHREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
20+
1821
if TYPE_CHECKING:
1922
from collections.abc import Callable, Iterable
2023

@@ -282,7 +285,12 @@ def check_compatibility(self):
282285

283286
@cached_property
284287
def _preferred_python_version(self):
285-
return f"{sys.version_info.major}.{sys.version_info.minor}"
288+
version = f"{sys.version_info.major}.{sys.version_info.minor}"
289+
290+
if FREETHREADED_BUILD:
291+
version += "t"
292+
293+
return version
286294

287295
@cached_property
288296
def parent_python(self):

tests/cli/env/test_show.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def test_default_as_json(hatch, temp_dir, config_file):
7272
"hatch-build",
7373
"hatch-static-analysis",
7474
"hatch-test.py3.14",
75+
"hatch-test.py3.14t",
7576
"hatch-test.py3.13",
7677
"hatch-test.py3.12",
7778
"hatch-test.py3.11",

tests/cli/run/test_run.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import sysconfig
34

45
import pytest
56

@@ -11,6 +12,8 @@
1112
from hatch.utils.structures import EnvVars
1213
from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT, DEFAULT_CONFIG_FILE
1314

15+
FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
16+
1417

1518
@pytest.fixture(scope="module")
1619
def available_python_version():
@@ -1214,6 +1217,9 @@ def test_incompatible_missing_python(hatch, helpers, temp_dir, config_file):
12141217
data_path.mkdir()
12151218

12161219
known_version = "".join(map(str, sys.version_info[:2]))
1220+
if FREE_THREADED_BUILD:
1221+
known_version += "t"
1222+
12171223
project = Project(project_path)
12181224
helpers.update_project_environment(project, "default", {"skip-install": True, **project.config.envs["default"]})
12191225
helpers.update_project_environment(project, "test", {"matrix": [{"python": [known_version, "9000"]}]})
@@ -1222,15 +1228,18 @@ def test_incompatible_missing_python(hatch, helpers, temp_dir, config_file):
12221228
result = hatch(
12231229
"run", "test:python", "-c", "import os,sys;open('test.txt', 'a').write(sys.executable+os.linesep[-1])"
12241230
)
1225-
12261231
padding = "─"
1227-
if len(known_version) < 3:
1228-
padding += "─"
1232+
if FREE_THREADED_BUILD:
1233+
pre_padding = ""
1234+
else:
1235+
pre_padding = "─"
1236+
if len(known_version) < 3:
1237+
padding += "─"
12291238

12301239
assert result.exit_code == 0, result.output
12311240
assert result.output == helpers.dedent(
12321241
f"""
1233-
───────────────────────────────── test.py{known_version} ─────────────────────────────────{padding}
1242+
─────────────────────────────────{pre_padding} test.py{known_version} ─────────────────────────────────{padding}
12341243
Creating environment: test.py{known_version}
12351244
Checking dependencies
12361245
@@ -2575,6 +2584,9 @@ def test_python_version_constraint_from_tool_config(self, hatch, helpers, temp_d
25752584
# Use the current minor version so that the current Python
25762585
# will be used and distributions don't have to be downloaded
25772586
major, minor = sys.version_info[:2]
2587+
python_version = f"{major}.{minor}"
2588+
if FREE_THREADED_BUILD:
2589+
python_version += "t"
25782590

25792591
script.write_text(
25802592
helpers.dedent(
@@ -2583,7 +2595,7 @@ def test_python_version_constraint_from_tool_config(self, hatch, helpers, temp_d
25832595
# requires-python = ">9000"
25842596
#
25852597
# [tool.hatch]
2586-
# python = "{major}.{minor}"
2598+
# python = "{python_version}"
25872599
# ///
25882600
import pathlib
25892601
import sys

0 commit comments

Comments
 (0)