from __future__ import annotations import abc from collections.abc import Iterable from contextlib import AbstractContextManager as ContextManager from typing import TYPE_CHECKING, Literal, Protocol from pip._vendor.packaging.version import Version from pip._internal.locations import get_scheme from pip._internal.metadata import get_default_environment, get_environment from pip._internal.utils.packaging import get_requirement if TYPE_CHECKING: from pip._internal.req.req_install import InstallRequirement BuildIsolationMode = Literal["off", "virtual", "venv"] def _dedup(a: str, b: str) -> tuple[str] | tuple[str, str]: return (a, b) if a != b else (a,) class Prefix: # TODO: simplify this data model when the legacy subprocess installer is removed def __init__(self, path: str, *, venv_executable: str | None = None) -> None: self.path = path self.setup = False scheme = get_scheme("", prefix=path) self.bin_dir = scheme.scripts self.lib_dirs = _dedup(scheme.purelib, scheme.platlib) self.venv_executable = venv_executable class BuildEnvironmentInstaller(Protocol): """ Interface for installing build dependencies into an isolated build environment. """ def install( self, requirements: Iterable[str], prefix: Prefix, *, kind: str, for_req: InstallRequirement | None, ) -> None: ... class BuildEnvironment(ContextManager[None], metaclass=abc.ABCMeta): """Creates and manages an isolated environment to install build deps""" lib_dirs: list[str] python_executable: str @abc.abstractmethod def __init__(self, installer: BuildEnvironmentInstaller): ... def check_requirements( self, reqs: Iterable[str] ) -> tuple[set[tuple[str, str]], set[str]]: """Return 2 sets: - conflicting requirements: set of (installed, wanted) reqs tuples - missing requirements: set of reqs """ missing = set() conflicting = set() if reqs: env = ( get_environment(self.lib_dirs) if hasattr(self, "lib_dirs") else get_default_environment() ) for req_str in reqs: req = get_requirement(req_str) # We're explicitly evaluating with an empty extra value, since build # environments are not provided any mechanism to select specific extras. if req.marker is not None and not req.marker.evaluate({"extra": ""}): continue dist = env.get_distribution(req.name) if not dist: missing.add(req_str) continue if isinstance(dist.version, Version): installed_req_str = f"{req.name}=={dist.version}" else: installed_req_str = f"{req.name}==={dist.version}" if not req.specifier.contains(dist.version, prereleases=True): conflicting.add((installed_req_str, req_str)) # FIXME: Consider direct URL? return conflicting, missing @abc.abstractmethod def install_requirements( self, requirements: Iterable[str], prefix_as_string: str, *, kind: str, for_req: InstallRequirement | None = None, ) -> None: ...