2020-05-18 10:35:09 +02:00
|
|
|
import collections
|
2020-05-21 12:09:35 +02:00
|
|
|
import logging
|
2020-05-18 10:35:09 +02:00
|
|
|
|
|
|
|
from pip._vendor import six
|
2020-04-07 13:02:39 +02:00
|
|
|
from pip._vendor.packaging.utils import canonicalize_name
|
2020-04-02 13:49:45 +02:00
|
|
|
|
2020-04-10 16:56:53 +02:00
|
|
|
from pip._internal.exceptions import (
|
|
|
|
InstallationError,
|
|
|
|
UnsupportedPythonVersion,
|
|
|
|
)
|
2020-04-08 11:13:13 +02:00
|
|
|
from pip._internal.utils.misc import (
|
|
|
|
dist_in_site_packages,
|
|
|
|
dist_in_usersite,
|
|
|
|
get_installed_distributions,
|
|
|
|
)
|
2020-03-26 20:14:51 +01:00
|
|
|
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
2020-04-08 11:13:13 +02:00
|
|
|
from pip._internal.utils.virtualenv import running_under_virtualenv
|
2020-03-26 20:14:51 +01:00
|
|
|
|
2020-04-02 13:49:45 +02:00
|
|
|
from .candidates import (
|
2020-04-02 19:27:34 +02:00
|
|
|
AlreadyInstalledCandidate,
|
2020-04-04 11:51:43 +02:00
|
|
|
EditableCandidate,
|
2020-04-02 13:49:45 +02:00
|
|
|
ExtrasCandidate,
|
|
|
|
LinkCandidate,
|
|
|
|
RequiresPythonCandidate,
|
|
|
|
)
|
2020-04-01 12:14:36 +02:00
|
|
|
from .requirements import (
|
|
|
|
ExplicitRequirement,
|
2020-04-10 16:56:53 +02:00
|
|
|
RequiresPythonRequirement,
|
2020-04-01 12:14:36 +02:00
|
|
|
SpecifierRequirement,
|
|
|
|
)
|
2020-03-26 20:14:51 +01:00
|
|
|
|
|
|
|
if MYPY_CHECK_RUNNING:
|
2020-05-21 10:14:26 +02:00
|
|
|
from typing import Dict, Iterable, Iterator, Optional, Set, Tuple, TypeVar
|
2020-04-01 12:14:36 +02:00
|
|
|
|
|
|
|
from pip._vendor.packaging.specifiers import SpecifierSet
|
2020-04-03 12:23:35 +02:00
|
|
|
from pip._vendor.packaging.version import _BaseVersion
|
2020-04-02 13:49:45 +02:00
|
|
|
from pip._vendor.pkg_resources import Distribution
|
2020-04-10 16:56:53 +02:00
|
|
|
from pip._vendor.resolvelib import ResolutionImpossible
|
2020-03-26 20:14:51 +01:00
|
|
|
|
|
|
|
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
|
2020-04-04 12:12:38 +02:00
|
|
|
from .candidates import BaseCandidate
|
2020-03-26 20:14:51 +01:00
|
|
|
|
2020-04-03 14:05:05 +02:00
|
|
|
C = TypeVar("C")
|
|
|
|
Cache = Dict[Link, C]
|
2020-05-18 11:15:04 +02:00
|
|
|
VersionCandidates = Dict[_BaseVersion, Candidate]
|
2020-04-03 14:05:05 +02:00
|
|
|
|
2020-03-26 20:14:51 +01:00
|
|
|
|
2020-05-21 12:09:35 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-03-26 20:14:51 +01:00
|
|
|
class Factory(object):
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
finder, # type: PackageFinder
|
|
|
|
preparer, # type: RequirementPreparer
|
|
|
|
make_install_req, # type: InstallRequirementProvider
|
2020-04-08 11:13:13 +02:00
|
|
|
use_user_site, # type: bool
|
2020-04-06 17:13:01 +02:00
|
|
|
force_reinstall, # type: bool
|
2020-04-02 13:49:45 +02:00
|
|
|
ignore_installed, # type: bool
|
2020-04-01 12:14:36 +02:00
|
|
|
ignore_requires_python, # type: bool
|
|
|
|
py_version_info=None, # type: Optional[Tuple[int, ...]]
|
2020-03-26 20:14:51 +01:00
|
|
|
):
|
|
|
|
# type: (...) -> None
|
2020-04-28 11:28:02 +02:00
|
|
|
|
2020-03-27 15:40:05 +01:00
|
|
|
self.finder = finder
|
|
|
|
self.preparer = preparer
|
2020-04-01 12:14:36 +02:00
|
|
|
self._python_candidate = RequiresPythonCandidate(py_version_info)
|
2020-04-01 10:14:44 +02:00
|
|
|
self._make_install_req_from_spec = make_install_req
|
2020-04-08 11:13:13 +02:00
|
|
|
self._use_user_site = use_user_site
|
2020-04-06 17:13:01 +02:00
|
|
|
self._force_reinstall = force_reinstall
|
2020-04-02 13:49:45 +02:00
|
|
|
self._ignore_requires_python = ignore_requires_python
|
2020-04-06 16:40:24 +02:00
|
|
|
|
2020-04-03 14:05:05 +02:00
|
|
|
self._link_candidate_cache = {} # type: Cache[LinkCandidate]
|
2020-04-04 11:51:43 +02:00
|
|
|
self._editable_candidate_cache = {} # type: Cache[EditableCandidate]
|
2020-03-26 20:14:51 +01:00
|
|
|
|
2020-04-06 16:40:24 +02:00
|
|
|
if not ignore_installed:
|
|
|
|
self._installed_dists = {
|
2020-04-13 12:05:24 +02:00
|
|
|
canonicalize_name(dist.project_name): dist
|
2020-04-06 16:40:24 +02:00
|
|
|
for dist in get_installed_distributions()
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
self._installed_dists = {}
|
|
|
|
|
2020-04-02 13:49:45 +02:00
|
|
|
def _make_candidate_from_dist(
|
|
|
|
self,
|
|
|
|
dist, # type: Distribution
|
|
|
|
extras, # type: Set[str]
|
|
|
|
parent, # type: InstallRequirement
|
|
|
|
):
|
|
|
|
# type: (...) -> Candidate
|
2020-04-02 19:27:34 +02:00
|
|
|
base = AlreadyInstalledCandidate(dist, parent, factory=self)
|
2020-04-02 13:49:45 +02:00
|
|
|
if extras:
|
|
|
|
return ExtrasCandidate(base, extras)
|
|
|
|
return base
|
|
|
|
|
|
|
|
def _make_candidate_from_link(
|
2020-03-26 20:14:51 +01:00
|
|
|
self,
|
2020-04-03 12:23:35 +02:00
|
|
|
link, # type: Link
|
|
|
|
extras, # type: Set[str]
|
|
|
|
parent, # type: InstallRequirement
|
|
|
|
name=None, # type: Optional[str]
|
|
|
|
version=None, # type: Optional[_BaseVersion]
|
2020-03-26 20:14:51 +01:00
|
|
|
):
|
|
|
|
# type: (...) -> Candidate
|
2020-04-10 13:53:18 +02:00
|
|
|
# TODO: Check already installed candidate, and use it if the link and
|
|
|
|
# editable flag match.
|
2020-04-04 11:51:43 +02:00
|
|
|
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,
|
|
|
|
)
|
2020-04-04 12:12:38 +02:00
|
|
|
base = self._editable_candidate_cache[link] # type: BaseCandidate
|
2020-04-04 11:51:43 +02:00
|
|
|
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]
|
2020-03-26 20:14:51 +01:00
|
|
|
if extras:
|
|
|
|
return ExtrasCandidate(base, extras)
|
|
|
|
return base
|
|
|
|
|
2020-04-10 13:53:18 +02:00
|
|
|
def iter_found_candidates(self, ireq, extras):
|
|
|
|
# type: (InstallRequirement, Set[str]) -> Iterator[Candidate]
|
|
|
|
name = canonicalize_name(ireq.req.name)
|
2020-05-14 12:33:30 +02:00
|
|
|
|
|
|
|
# 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.
|
2020-05-18 11:15:04 +02:00
|
|
|
candidates = collections.OrderedDict() # type: VersionCandidates
|
2020-05-13 18:52:09 +02:00
|
|
|
|
|
|
|
# Yield the installed version, if it matches, unless the user
|
|
|
|
# specified `--force-reinstall`, when we want the version from
|
|
|
|
# the index instead.
|
2020-05-18 10:35:09 +02:00
|
|
|
installed_version = None
|
2020-05-13 18:52:09 +02:00
|
|
|
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
|
|
|
|
):
|
2020-05-18 10:35:09 +02:00
|
|
|
candidate = self._make_candidate_from_dist(
|
2020-05-13 18:52:09 +02:00
|
|
|
dist=installed_dist,
|
|
|
|
extras=extras,
|
|
|
|
parent=ireq,
|
|
|
|
)
|
2020-05-18 11:15:04 +02:00
|
|
|
candidates[installed_version] = candidate
|
2020-04-10 13:53:18 +02:00
|
|
|
|
|
|
|
found = self.finder.find_best_candidate(
|
|
|
|
project_name=ireq.req.name,
|
|
|
|
specifier=ireq.req.specifier,
|
|
|
|
hashes=ireq.hashes(trust_internet=False),
|
2020-04-06 17:13:01 +02:00
|
|
|
)
|
2020-04-10 13:53:18 +02:00
|
|
|
for ican in found.iter_applicable():
|
2020-05-18 10:35:09 +02:00
|
|
|
if ican.version == installed_version:
|
|
|
|
continue
|
|
|
|
candidate = self._make_candidate_from_link(
|
|
|
|
link=ican.link,
|
|
|
|
extras=extras,
|
|
|
|
parent=ireq,
|
|
|
|
name=name,
|
|
|
|
version=ican.version,
|
|
|
|
)
|
2020-05-18 11:15:04 +02:00
|
|
|
candidates[ican.version] = candidate
|
2020-05-18 10:35:09 +02:00
|
|
|
|
2020-05-18 11:15:04 +02:00
|
|
|
return six.itervalues(candidates)
|
2020-04-02 13:49:45 +02:00
|
|
|
|
2020-04-24 15:29:34 +02:00
|
|
|
def make_requirement_from_install_req(self, ireq):
|
|
|
|
# type: (InstallRequirement) -> Requirement
|
2020-03-26 20:14:51 +01:00
|
|
|
if ireq.link:
|
2020-04-03 12:23:35 +02:00
|
|
|
# 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.
|
2020-05-21 17:06:51 +02:00
|
|
|
candidate = self._make_candidate_from_link(
|
2020-04-26 10:53:34 +02:00
|
|
|
ireq.link, extras=set(ireq.extras), parent=ireq,
|
2020-04-02 13:49:45 +02:00
|
|
|
)
|
2020-05-21 17:06:51 +02:00
|
|
|
return self.make_requirement_from_candidate(candidate)
|
2020-04-02 13:49:45 +02:00
|
|
|
return SpecifierRequirement(ireq, factory=self)
|
2020-04-01 10:14:44 +02:00
|
|
|
|
2020-05-21 17:06:51 +02:00
|
|
|
def make_requirement_from_candidate(self, candidate):
|
2020-05-21 17:48:44 +02:00
|
|
|
# type: (Candidate) -> ExplicitRequirement
|
2020-05-21 17:06:51 +02:00
|
|
|
return ExplicitRequirement(candidate)
|
|
|
|
|
2020-04-01 10:14:44 +02:00
|
|
|
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)
|
2020-04-01 12:14:36 +02:00
|
|
|
|
2020-05-21 10:14:26 +02:00
|
|
|
def make_requirement_from_spec_matching_extras(
|
|
|
|
self,
|
|
|
|
specifier, # type: str
|
|
|
|
comes_from, # type: InstallRequirement
|
|
|
|
requested_extras=(), # type: Iterable[str]
|
|
|
|
):
|
|
|
|
# type: (...) -> Optional[Requirement]
|
|
|
|
ireq = self._make_install_req_from_spec(specifier, comes_from)
|
|
|
|
if not ireq.match_markers(requested_extras):
|
2020-05-21 12:09:35 +02:00
|
|
|
logger.info(
|
|
|
|
"Ignoring %s: markers '%s' don't match your environment",
|
|
|
|
ireq.name, ireq.markers,
|
|
|
|
)
|
2020-05-21 10:14:26 +02:00
|
|
|
return None
|
|
|
|
return self.make_requirement_from_install_req(ireq)
|
|
|
|
|
2020-04-01 12:14:36 +02:00
|
|
|
def make_requires_python_requirement(self, specifier):
|
|
|
|
# type: (Optional[SpecifierSet]) -> Optional[Requirement]
|
|
|
|
if self._ignore_requires_python or specifier is None:
|
|
|
|
return None
|
2020-04-10 16:56:53 +02:00
|
|
|
return RequiresPythonRequirement(specifier, self._python_candidate)
|
2020-04-06 17:13:01 +02:00
|
|
|
|
|
|
|
def should_reinstall(self, candidate):
|
|
|
|
# type: (Candidate) -> bool
|
|
|
|
# TODO: Are there more cases this needs to return True? Editable?
|
2020-04-08 11:13:13 +02:00
|
|
|
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
|
2020-04-10 16:56:53 +02:00
|
|
|
|
|
|
|
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
|