Skip to content

Commit

Permalink
Regex based finding
Browse files Browse the repository at this point in the history
  • Loading branch information
flying-sheep committed Apr 23, 2024
1 parent 16a6e1d commit d9b52ad
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 53 deletions.
53 changes: 27 additions & 26 deletions src/virtualenv/discovery/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable

from virtualenv.info import IS_WIN

Expand Down Expand Up @@ -124,17 +124,16 @@ def propose_interpreters( # noqa: C901, PLR0912
yield interpreter, True
# finally just find on path, the path order matters (as the candidates are less easy to control by end user)
tested_exes = set()
find_candidates = path_exe_finder(spec)
for pos, path in enumerate(get_paths(env)):
logging.debug(LazyPathDump(pos, path, env))
for candidate, match in possible_specs(spec):
found = check_path(Path(candidate), path)
if found is not None:
exe = found.absolute()
if exe not in tested_exes:
tested_exes.add(exe)
interpreter = PathPythonInfo.from_exe(exe, app_data, raise_on_error=False, env=env)
if interpreter is not None:
yield interpreter, match
for exe, impl_must_match in find_candidates(path):
if exe in tested_exes:
continue
tested_exes.add(exe)
interpreter = PathPythonInfo.from_exe(str(exe), app_data, raise_on_error=False, env=env)
if interpreter is not None:
yield interpreter, impl_must_match


def get_paths(env: Mapping[str, str]) -> Generator[Path, None, None]:
Expand Down Expand Up @@ -172,22 +171,24 @@ def __repr__(self) -> str:
return content


def check_path(candidate: Path, path: Path) -> Path | None:
if sys.platform == "win32" and candidate.suffix != ".exe":
candidate = candidate.with_name(f"{candidate.name}.exe")
if candidate.is_file():
return candidate
candidate = path.joinpath(candidate)
if candidate.is_file():
return candidate
return None


def possible_specs(spec: PythonSpec) -> Generator[tuple[str, bool], None, None]:
# 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
yield spec.str_spec, False
# 5. or from the spec we can deduce a name on path that matches
yield from spec.generate_names()
def path_exe_finder(spec: PythonSpec) -> Callable[[Path], Generator[tuple[Path, bool], None, None]]:
"""Given a spec, return a function that can be called on a path to find all matching files in it."""
pat = spec.generate_re(windows=sys.platform == "win32")
direct = spec.str_spec
if sys.platform == "win32":
direct = f"{direct}.exe"

def path_exes(path: Path) -> Generator[tuple[Path, bool], None, None]:
# 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts
yield (path / direct), False
# 5. or from the spec we can deduce if a name on path matches
for exe in path.iterdir():
match = pat.fullmatch(exe.name)
if match:
# the implementation must match when we find “python[ver]”
yield exe.absolute(), match["impl"] == "python"

return path_exes


class PathPythonInfo(PythonInfo):
Expand Down
2 changes: 1 addition & 1 deletion src/virtualenv/discovery/py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def current(cls, app_data=None):
return cls._current

@classmethod
def current_system(cls, app_data=None):
def current_system(cls, app_data=None) -> PythonInfo:
"""
This locates the current host interpreter information. This might be different than what we run into in case
the host python has been upgraded from underneath us.
Expand Down
49 changes: 23 additions & 26 deletions src/virtualenv/discovery/py_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@

from __future__ import annotations

import contextlib
import os
import re
from collections import OrderedDict

from virtualenv.info import fs_is_case_sensitive

PATTERN = re.compile(r"^(?P<impl>[a-zA-Z]+)?(?P<version>[0-9.]+)?(?:-(?P<arch>32|64))?$")


class PythonSpec:
"""Contains specification about a Python Interpreter."""

def __init__(self, str_spec, implementation, major, minor, micro, architecture, path) -> None: # noqa: PLR0913
def __init__( # noqa: PLR0913
self,
str_spec: str,
implementation: str | None,
major: int | None,
minor: int | None,
micro: int | None,
architecture: int | None,
path: str | None,
) -> None:
self.str_spec = str_spec
self.implementation = implementation
self.major = major
Expand All @@ -25,7 +30,7 @@ def __init__(self, str_spec, implementation, major, minor, micro, architecture,
self.path = path

@classmethod
def from_string_spec(cls, string_spec): # noqa: C901, PLR0912
def from_string_spec(cls, string_spec: str): # noqa: C901, PLR0912
impl, major, minor, micro, arch, path = None, None, None, None, None, None
if os.path.isabs(string_spec): # noqa: PLR1702
path = string_spec
Expand Down Expand Up @@ -67,26 +72,18 @@ def _int_or_none(val):

return cls(string_spec, impl, major, minor, micro, arch, path)

def generate_names(self):
impls = OrderedDict()
if self.implementation:
# first consider implementation as it is
impls[self.implementation] = False
if fs_is_case_sensitive():
# for case sensitive file systems consider lower and upper case versions too
# trivia: MacBooks and all pre 2018 Windows-es were case insensitive by default
impls[self.implementation.lower()] = False
impls[self.implementation.upper()] = False
impls["python"] = True # finally consider python as alias, implementation must match now
version = self.major, self.minor, self.micro
with contextlib.suppress(ValueError):
version = version[: version.index(None)]

for impl, match in impls.items():
for at in range(len(version), -1, -1):
cur_ver = version[0:at]
spec = f"{impl}{'.'.join(str(i) for i in cur_ver)}"
yield spec, match
def generate_re(self, *, windows: bool) -> re.Pattern:
"""Generate a regular expression for matching against a filename."""
version = r"{}(\.{}(\.{})?)?".format(
*(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)}"
suffix = r"\.exe" if windows else ""
# Try matching `direct` first, so the `direct` group is filled when possible.
return re.compile(
rf"(?P<impl>{impl})(?P<v>{version}){suffix}$",
flags=re.IGNORECASE,
)

@property
def is_abs(self):
Expand Down

0 comments on commit d9b52ad

Please sign in to comment.