55import logging
66import re
77import sys
8+ import sysconfig
89from abc import ABC , abstractmethod
910from pathlib import Path
1011from typing import TYPE_CHECKING , Any , List , NamedTuple , cast
@@ -30,6 +31,7 @@ class PythonInfo(NamedTuple):
3031 implementation : str
3132 version_info : VersionInfo
3233 version : str
34+ free_threaded : bool
3335 is_64 : bool
3436 platform : str
3537 extra : dict [str , Any ]
@@ -51,11 +53,14 @@ def version_dot(self) -> str:
5153 r"""
5254 ^(?!py$) # don't match 'py' as it doesn't provide any info
5355 (?P<impl>py|pypy|cpython|jython|graalpy|rustpython|ironpython) # the interpreter; most users will simply use 'py'
54- (?P<version>[2-9]\.?[0-9]?[0-9]?)?$ # the version; one of: MAJORMINOR, MAJOR.MINOR
56+ (?:
57+ (?P<version>[2-9]\.?[0-9]?[0-9]?) # the version; one of: MAJORMINOR, MAJOR.MINOR
58+ (?P<threaded>t?) # version followed by t for free-threading
59+ )?$
5560 """ ,
5661 re .VERBOSE ,
5762)
58- PY_FACTORS_RE_EXPLICIT_VERSION = re .compile (r"^((?P<impl>cpython|pypy)-)?(?P<version>[2-9]\.[0-9]+)$" )
63+ PY_FACTORS_RE_EXPLICIT_VERSION = re .compile (r"^((?P<impl>cpython|pypy)-)?(?P<version>[2-9]\.[0-9]+)(?P<threaded>t?) $" )
5964
6065
6166class Python (ToxEnv , ABC ):
@@ -100,6 +105,7 @@ def validate_base_python(value: list[str]) -> list[str]:
100105 )
101106 self .conf .add_constant ("py_dot_ver" , "<python major>.<python minor>" , value = self .py_dot_ver )
102107 self .conf .add_constant ("py_impl" , "python implementation" , value = self .py_impl )
108+ self .conf .add_constant ("py_free_threaded" , "is no-gil interpreted" , value = self .py_free_threaded )
103109
104110 def _default_set_env (self ) -> dict [str , str ]:
105111 env = super ()._default_set_env ()
@@ -111,6 +117,9 @@ def _default_set_env(self) -> dict[str, str]:
111117 def py_dot_ver (self ) -> str :
112118 return self .base_python .version_dot
113119
120+ def py_free_threaded (self ) -> bool :
121+ return self .base_python .free_threaded
122+
114123 def py_impl (self ) -> str :
115124 return self .base_python .impl_lower
116125
@@ -145,7 +154,7 @@ def extract_base_python(cls, env_name: str) -> str | None:
145154 match = PY_FACTORS_RE_EXPLICIT_VERSION .match (env_name )
146155 if match :
147156 found = match .groupdict ()
148- candidates .append (f"{ 'pypy' if found ['impl' ] == 'pypy' else '' } { found ['version' ]} " )
157+ candidates .append (f"{ 'pypy' if found ['impl' ] == 'pypy' else '' } { found ['version' ]} { found [ 'threaded' ] } " )
149158 else :
150159 for factor in env_name .split ("-" ):
151160 match = PY_FACTORS_RE .match (factor )
@@ -163,7 +172,8 @@ def _python_spec_for_sys_executable(cls) -> PythonSpec:
163172 implementation = sys .implementation .name
164173 version = sys .version_info
165174 bits = "64" if sys .maxsize > 2 ** 32 else "32"
166- string_spec = f"{ implementation } { version .major } { version .minor } -{ bits } "
175+ threaded = "t" if sysconfig .get_config_var ("Py_GIL_DISABLED" ) == 1 else ""
176+ string_spec = f"{ implementation } { version .major } { version .minor } { threaded } -{ bits } "
167177 return PythonSpec .from_string_spec (string_spec )
168178
169179 @classmethod
@@ -186,7 +196,7 @@ def _validate_base_python(
186196 spec_base = cls .python_spec_for_path (path )
187197 if any (
188198 getattr (spec_base , key ) != getattr (spec_name , key )
189- for key in ("implementation" , "major" , "minor" , "micro" , "architecture" )
199+ for key in ("implementation" , "major" , "minor" , "micro" , "architecture" , "free_threaded" )
190200 if getattr (spec_name , key ) is not None
191201 ):
192202 msg = f"env name { env_name } conflicting with base python { base_python } "
0 commit comments