import collections from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import ( InstallationError, UnsupportedPythonVersion, ) from pip._internal.utils.misc import ( dist_in_site_packages, dist_in_usersite, get_installed_distributions, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv from .candidates import ( AlreadyInstalledCandidate, EditableCandidate, ExtrasCandidate, LinkCandidate, RequiresPythonCandidate, ) from .requirements import ( ExplicitRequirement, RequiresPythonRequirement, SpecifierRequirement, ) if MYPY_CHECK_RUNNING: from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution from pip._vendor.resolvelib import ResolutionImpossible from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import InstallRequirementProvider from .base import Candidate, Requirement from .candidates import BaseCandidate C = TypeVar("C") Cache = Dict[Link, C] class Factory(object): def __init__( self, finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider use_user_site, # type: bool force_reinstall, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None self.finder = finder self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) self._make_install_req_from_spec = make_install_req self._use_user_site = use_user_site self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] if not ignore_installed: self._installed_dists = { canonicalize_name(dist.project_name): dist for dist in get_installed_distributions() } else: self._installed_dists = {} def _make_candidate_from_dist( self, dist, # type: Distribution extras, # type: Set[str] parent, # type: InstallRequirement ): # type: (...) -> Candidate base = AlreadyInstalledCandidate(dist, parent, factory=self) if extras: return ExtrasCandidate(base, extras) return base def _make_candidate_from_link( self, link, # type: Link extras, # type: Set[str] parent, # type: InstallRequirement name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate # TODO: Check already installed candidate, and use it if the link and # editable flag match. if parent.editable: if link not in self._editable_candidate_cache: self._editable_candidate_cache[link] = EditableCandidate( link, parent, factory=self, name=name, version=version, ) base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: self._link_candidate_cache[link] = LinkCandidate( link, parent, factory=self, name=name, version=version, ) base = self._link_candidate_cache[link] if extras: return ExtrasCandidate(base, extras) return base def iter_found_candidates(self, ireq, extras): # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] name = canonicalize_name(ireq.req.name) # We use this to ensure that we only yield a single candidate for # each version (the finder's preferred one for that version). The # requirement needs to return only one candidate per version, so we # implement that logic here so that requirements using this helper # don't all have to do the same thing later. version_candidates = collections.OrderedDict( ) # type: Dict[_BaseVersion, Candidate] # Yield the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from # the index instead. installed_version = None if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] installed_version = installed_dist.parsed_version if ireq.req.specifier.contains( installed_version, prereleases=True ): candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, parent=ireq, ) version_candidates[installed_version] = candidate found = self.finder.find_best_candidate( project_name=ireq.req.name, specifier=ireq.req.specifier, hashes=ireq.hashes(trust_internet=False), ) for ican in found.iter_applicable(): if ican.version == installed_version: continue candidate = self._make_candidate_from_link( link=ican.link, extras=extras, parent=ireq, name=name, version=ican.version, ) version_candidates[ican.version] = candidate return six.itervalues(version_candidates) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement if ireq.link: # TODO: Get name and version from ireq, if possible? # Specifically, this might be needed in "name @ URL" # syntax - need to check where that syntax is handled. cand = self._make_candidate_from_link( ireq.link, extras=set(ireq.extras), parent=ireq, ) return ExplicitRequirement(cand) return SpecifierRequirement(ireq, factory=self) def make_requirement_from_spec(self, specifier, comes_from): # type: (str, InstallRequirement) -> Requirement ireq = self._make_install_req_from_spec(specifier, comes_from) return self.make_requirement_from_install_req(ireq) def make_requires_python_requirement(self, specifier): # type: (Optional[SpecifierSet]) -> Optional[Requirement] if self._ignore_requires_python or specifier is None: return None return RequiresPythonRequirement(specifier, self._python_candidate) def should_reinstall(self, candidate): # type: (Candidate) -> bool # TODO: Are there more cases this needs to return True? Editable? dist = self._installed_dists.get(candidate.name) if dist is None: # Not installed, no uninstallation required. return False # We're installing into global site. The current installation must # be uninstalled, no matter it's in global or user site, because the # user site installation has precedence over global. if not self._use_user_site: return True # We're installing into user site. Remove the user site installation. if dist_in_usersite(dist): return True # We're installing into user site, but the installed incompatible # package is in global site. We can't uninstall that, and would let # the new user installation to "shadow" it. But shadowing won't work # in virtual environments, so we error out. if running_under_virtualenv() and dist_in_site_packages(dist): raise InstallationError( "Will not install to the user site because it will " "lack sys.path precedence to {} in {}".format( dist.project_name, dist.location, ) ) return False def _report_requires_python_error( self, requirement, # type: RequiresPythonRequirement parent, # type: Candidate ): # type: (...) -> UnsupportedPythonVersion template = ( "Package {package!r} requires a different Python: " "{version} not in {specifier!r}" ) message = template.format( package=parent.name, version=self._python_candidate.version, specifier=str(requirement.specifier), ) return UnsupportedPythonVersion(message) def get_installation_error(self, e): # type: (ResolutionImpossible) -> Optional[InstallationError] for cause in e.causes: if isinstance(cause.requirement, RequiresPythonRequirement): return self._report_requires_python_error( cause.requirement, cause.parent, ) return None