from __future__ import annotations
import logging
import os
import sys
from typing import TYPE_CHECKING, Iterator, Mapping, MutableMapping
from ._discover import Discover
from ._info import PythonInfo
from ._spec import PythonSpec
if TYPE_CHECKING:
from argparse import ArgumentParser, Namespace
[docs]class Builtin(Discover):
def __init__(self, options: Namespace) -> None:
super().__init__(options)
self.python_spec = options.python if options.python else [sys.executable]
self.try_first_with = options.try_first_with
[docs] @classmethod
def add_parser_arguments(cls, parser: ArgumentParser) -> None:
parser.add_argument(
"-p",
"--python",
dest="python",
metavar="py",
type=str,
action="append",
default=[],
help="interpreter based on what to create environment (path/identifier) "
"- by default use the interpreter where the tool is installed - first found wins",
)
parser.add_argument(
"--try-first-with",
dest="try_first_with",
metavar="py_exe",
type=str,
action="append",
default=[],
help="try first these interpreters before starting the discovery",
)
[docs] def run(self) -> PythonInfo | None:
for python_spec in self.python_spec:
result = get_interpreter(python_spec, self.try_first_with, self._env)
if result is not None:
return result
return None
def __repr__(self) -> str:
spec = self.python_spec[0] if len(self.python_spec) == 1 else self.python_spec
return f"{self.__class__.__name__} discover of python_spec={spec!r}"
[docs]def get_interpreter(
key: str,
try_first_with: list[str],
env: MutableMapping[str, str] | None = None,
) -> PythonInfo | None:
spec = PythonSpec.from_string_spec(key)
logging.info("find interpreter for spec %r", spec)
proposed_paths = set()
env = os.environ if env is None else env
for interpreter, impl_must_match in propose_interpreters(spec, try_first_with, env):
if interpreter is None:
continue
lookup_key = interpreter.system_executable, impl_must_match
if lookup_key in proposed_paths:
continue
logging.info("proposed %s", interpreter)
if interpreter.satisfies(spec, impl_must_match):
logging.debug("accepted %s", interpreter)
return interpreter
proposed_paths.add(lookup_key)
return None
def propose_interpreters( # noqa: C901, PLR0912
spec: PythonSpec,
try_first_with: list[str],
env: MutableMapping[str, str] | None = None,
) -> Iterator[tuple[PythonInfo | None, bool]]:
# 0. tries with first
env = os.environ if env is None else env
for py_exe in try_first_with:
path = os.path.abspath(py_exe) # noqa: PTH100
try:
os.lstat(path) # Windows Store Python does not work with os.path.exists, but does for os.lstat
except OSError:
pass
else:
yield PythonInfo.from_exe(os.path.abspath(path), env=env), True # noqa: PTH100
# 1. if it's a path and exists
if spec.path is not None:
try:
os.lstat(spec.path) # Windows Store Python does not work with os.path.exists, but does for os.lstat
except OSError:
if spec.is_abs:
raise
else:
yield PythonInfo.from_exe(os.path.abspath(spec.path), env=env), True # noqa: PTH100
if spec.is_abs:
return
else:
# 2. otherwise tries with the current
yield PythonInfo.current_system(), True
# 3. otherwise fallbacks to platform default logic
if sys.platform == "win32":
from ._windows import propose_interpreters
for interpreter in propose_interpreters(spec, env):
yield interpreter, True
# finally, find on the path, the path order matters (as the candidates are less easy to control by end user)
paths = get_paths(env)
tested_exes = set()
for pos, path in enumerate(paths):
path_str = str(path)
logging.debug(LazyPathDump(pos, path_str, env))
for candidate, match in possible_specs(spec):
found = check_path(candidate, path_str)
if found is not None:
exe = os.path.abspath(found) # noqa: PTH100
if exe not in tested_exes:
tested_exes.add(exe)
got = PathPythonInfo.from_exe(exe, raise_on_error=False, env=env)
if got is not None:
yield got, match
def get_paths(env: Mapping[str, str]) -> list[str]:
path = env.get("PATH", None)
if path is None:
if sys.platform == "win32": # pragma: win32 cover
path = os.defpath
else: # pragma: win32 cover
path = os.confstr("CS_PATH") or os.defpath
return [] if not path else [p for p in path.split(os.pathsep) if os.path.exists(p)] # noqa: PTH110
class LazyPathDump:
def __init__(self, pos: int, path: str, env: Mapping[str, str]) -> None:
self.pos = pos
self.path = path
self.env = env
def __repr__(self) -> str:
content = f"discover PATH[{self.pos}]={self.path}"
if self.env.get("_VIRTUALENV_DEBUG"): # this is the over the board debug
content += " with =>"
for file_name in os.listdir(self.path):
try:
file_path = os.path.join(self.path, file_name) # noqa: PTH118
if os.path.isdir(file_path) or not os.access(file_path, os.X_OK): # noqa: PTH112
continue
except OSError:
pass
content += " "
content += file_name
return content
def check_path(candidate: str, path: str) -> str | None:
_, ext = os.path.splitext(candidate) # noqa: PTH122
if sys.platform == "win32" and ext != ".exe":
candidate = f"{candidate}.exe"
if os.path.isfile(candidate): # noqa: PTH113
return candidate
candidate = os.path.join(path, candidate) # noqa: PTH118
if os.path.isfile(candidate): # noqa: PTH113
return candidate
return None
def possible_specs(spec: PythonSpec) -> Iterator[tuple[str, bool]]:
# 4. then maybe it's something exact on PATH - if it was a direct lookup implementation no longer counts
if spec.str_spec is not None:
yield spec.str_spec, False
# 5. or from the spec we can deduce a name on path that matches
yield from spec.generate_names()
[docs]class PathPythonInfo(PythonInfo):
"""python info from a path."""
__all__ = [
"get_interpreter",
"Builtin",
"PathPythonInfo",
]