From 4feb6d3dfb13bae2e3a12f97c6480c371fb02d1e Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 29 Nov 2024 11:26:36 +0100 Subject: [PATCH] Add free-threaded Python support to PythonSpec --- src/virtualenv/discovery/py_spec.py | 17 ++++++++++++----- tests/unit/discovery/test_py_spec.py | 8 ++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/virtualenv/discovery/py_spec.py b/src/virtualenv/discovery/py_spec.py index dcd84f423..00719a543 100644 --- a/src/virtualenv/discovery/py_spec.py +++ b/src/virtualenv/discovery/py_spec.py @@ -5,7 +5,7 @@ import os import re -PATTERN = re.compile(r"^(?P[a-zA-Z]+)?(?P[0-9.]+)?(?:-(?P32|64))?$") +PATTERN = re.compile(r"^(?P[a-zA-Z]+)?(?P[0-9.]+)?(?Pt)?(?:-(?P32|64))?$") class PythonSpec: @@ -20,18 +20,21 @@ def __init__( # noqa: PLR0913 micro: int | None, architecture: int | None, path: str | None, + *, + free_threaded: bool = False, ) -> None: self.str_spec = str_spec self.implementation = implementation self.major = major self.minor = minor self.micro = micro + self.free_threaded = free_threaded self.architecture = architecture self.path = path @classmethod def from_string_spec(cls, string_spec: str): # noqa: C901, PLR0912 - impl, major, minor, micro, arch, path = None, None, None, None, None, None + impl, major, minor, micro, threaded, arch, path = None, None, None, None, False, None, None if os.path.isabs(string_spec): # noqa: PLR1702 path = string_spec else: @@ -65,12 +68,13 @@ def _int_or_none(val): impl = groups["impl"] if impl in {"py", "python"}: impl = None + threaded = bool(groups["threaded"]) arch = _int_or_none(groups["arch"]) if not ok: path = string_spec - return cls(string_spec, impl, major, minor, micro, arch, path) + return cls(string_spec, impl, major, minor, micro, arch, path, free_threaded=threaded) def generate_re(self, *, windows: bool) -> re.Pattern: """Generate a regular expression for matching against a filename.""" @@ -78,6 +82,7 @@ def generate_re(self, *, windows: bool) -> re.Pattern: *(r"\d+" if v is None else v for v in (self.major, self.minor, self.micro)) ) impl = "python" if self.implementation is None else f"python|{re.escape(self.implementation)}" + mod = "t" if self.free_threaded else "" suffix = r"\.exe" if windows else "" version_conditional = ( "?" @@ -89,7 +94,7 @@ def generate_re(self, *, windows: bool) -> re.Pattern: ) # Try matching `direct` first, so the `direct` group is filled when possible. return re.compile( - rf"(?P{impl})(?P{version}){version_conditional}{suffix}$", + rf"(?P{impl})(?P{version}){version_conditional}{mod}{suffix}$", flags=re.IGNORECASE, ) @@ -105,6 +110,8 @@ def satisfies(self, spec): return False if spec.architecture is not None and spec.architecture != self.architecture: return False + if spec.free_threaded != self.free_threaded: + return False for our, req in zip((self.major, self.minor, self.micro), (spec.major, spec.minor, spec.micro)): if req is not None and our is not None and our != req: @@ -113,7 +120,7 @@ def satisfies(self, spec): def __repr__(self) -> str: name = type(self).__name__ - params = "implementation", "major", "minor", "micro", "architecture", "path" + params = "implementation", "major", "minor", "micro", "architecture", "path", "free_threaded" return f"{name}({', '.join(f'{k}={getattr(self, k)}' for k in params if getattr(self, k) is not None)})" diff --git a/tests/unit/discovery/test_py_spec.py b/tests/unit/discovery/test_py_spec.py index 765686645..c574862bf 100644 --- a/tests/unit/discovery/test_py_spec.py +++ b/tests/unit/discovery/test_py_spec.py @@ -45,6 +45,14 @@ def test_spec_satisfies_arch(): assert spec_2.satisfies(spec_1) is False +def test_spec_satisfies_free_threaded(): + spec_1 = PythonSpec.from_string_spec("python3.13t") + spec_2 = PythonSpec.from_string_spec("python3.13") + + assert spec_1.satisfies(spec_1) is True + assert spec_2.satisfies(spec_1) is False + + @pytest.mark.parametrize( ("req", "spec"), [("py", "python"), ("jython", "jython"), ("CPython", "cpython")],