Created starter files for the project.
This commit is contained in:
commit
73f0c0db42
1992 changed files with 769897 additions and 0 deletions
Binary file not shown.
Binary file not shown.
20
venv/Lib/site-packages/pip/_internal/resolution/base.py
Normal file
20
venv/Lib/site-packages/pip/_internal/resolution/base.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Callable, List
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.req.req_set import RequirementSet
|
||||
|
||||
InstallRequirementProvider = Callable[
|
||||
[str, InstallRequirement], InstallRequirement
|
||||
]
|
||||
|
||||
|
||||
class BaseResolver(object):
|
||||
def resolve(self, root_reqs, check_supported_wheels):
|
||||
# type: (List[InstallRequirement], bool) -> RequirementSet
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_installation_order(self, req_set):
|
||||
# type: (RequirementSet) -> List[InstallRequirement]
|
||||
raise NotImplementedError()
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,485 @@
|
|||
"""Dependency Resolution
|
||||
|
||||
The dependency resolution in pip is performed as follows:
|
||||
|
||||
for top-level requirements:
|
||||
a. only one spec allowed per project, regardless of conflicts or not.
|
||||
otherwise a "double requirement" exception is raised
|
||||
b. they override sub-dependency requirements.
|
||||
for sub-dependencies
|
||||
a. "first found, wins" (where the order is breadth first)
|
||||
"""
|
||||
|
||||
# The following comment should be removed at some point in the future.
|
||||
# mypy: strict-optional=False
|
||||
# mypy: disallow-untyped-defs=False
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from pip._vendor.packaging import specifiers
|
||||
|
||||
from pip._internal.exceptions import (
|
||||
BestVersionAlreadyInstalled,
|
||||
DistributionNotFound,
|
||||
HashError,
|
||||
HashErrors,
|
||||
UnsupportedPythonVersion,
|
||||
)
|
||||
from pip._internal.req.req_install import check_invalid_constraint_type
|
||||
from pip._internal.req.req_set import RequirementSet
|
||||
from pip._internal.resolution.base import BaseResolver
|
||||
from pip._internal.utils.compatibility_tags import get_supported
|
||||
from pip._internal.utils.logging import indent_log
|
||||
from pip._internal.utils.misc import dist_in_usersite, normalize_version_info
|
||||
from pip._internal.utils.packaging import (
|
||||
check_requires_python,
|
||||
get_requires_python,
|
||||
)
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import DefaultDict, List, Optional, Set, Tuple
|
||||
from pip._vendor import pkg_resources
|
||||
|
||||
from pip._internal.cache import WheelCache
|
||||
from pip._internal.distributions import AbstractDistribution
|
||||
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
|
||||
|
||||
DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_dist_requires_python(
|
||||
dist, # type: pkg_resources.Distribution
|
||||
version_info, # type: Tuple[int, int, int]
|
||||
ignore_requires_python=False, # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
Check whether the given Python version is compatible with a distribution's
|
||||
"Requires-Python" value.
|
||||
|
||||
:param version_info: A 3-tuple of ints representing the Python
|
||||
major-minor-micro version to check.
|
||||
:param ignore_requires_python: Whether to ignore the "Requires-Python"
|
||||
value if the given Python version isn't compatible.
|
||||
|
||||
:raises UnsupportedPythonVersion: When the given Python version isn't
|
||||
compatible.
|
||||
"""
|
||||
requires_python = get_requires_python(dist)
|
||||
try:
|
||||
is_compatible = check_requires_python(
|
||||
requires_python, version_info=version_info,
|
||||
)
|
||||
except specifiers.InvalidSpecifier as exc:
|
||||
logger.warning(
|
||||
"Package %r has an invalid Requires-Python: %s",
|
||||
dist.project_name, exc,
|
||||
)
|
||||
return
|
||||
|
||||
if is_compatible:
|
||||
return
|
||||
|
||||
version = '.'.join(map(str, version_info))
|
||||
if ignore_requires_python:
|
||||
logger.debug(
|
||||
'Ignoring failed Requires-Python check for package %r: '
|
||||
'%s not in %r',
|
||||
dist.project_name, version, requires_python,
|
||||
)
|
||||
return
|
||||
|
||||
raise UnsupportedPythonVersion(
|
||||
'Package {!r} requires a different Python: {} not in {!r}'.format(
|
||||
dist.project_name, version, requires_python,
|
||||
))
|
||||
|
||||
|
||||
class Resolver(BaseResolver):
|
||||
"""Resolves which packages need to be installed/uninstalled to perform \
|
||||
the requested operation without breaking the requirements of any package.
|
||||
"""
|
||||
|
||||
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preparer, # type: RequirementPreparer
|
||||
finder, # type: PackageFinder
|
||||
wheel_cache, # type: Optional[WheelCache]
|
||||
make_install_req, # type: InstallRequirementProvider
|
||||
use_user_site, # type: bool
|
||||
ignore_dependencies, # type: bool
|
||||
ignore_installed, # type: bool
|
||||
ignore_requires_python, # type: bool
|
||||
force_reinstall, # type: bool
|
||||
upgrade_strategy, # type: str
|
||||
py_version_info=None, # type: Optional[Tuple[int, ...]]
|
||||
):
|
||||
# type: (...) -> None
|
||||
super(Resolver, self).__init__()
|
||||
assert upgrade_strategy in self._allowed_strategies
|
||||
|
||||
if py_version_info is None:
|
||||
py_version_info = sys.version_info[:3]
|
||||
else:
|
||||
py_version_info = normalize_version_info(py_version_info)
|
||||
|
||||
self._py_version_info = py_version_info
|
||||
|
||||
self.preparer = preparer
|
||||
self.finder = finder
|
||||
self.wheel_cache = wheel_cache
|
||||
|
||||
self.upgrade_strategy = upgrade_strategy
|
||||
self.force_reinstall = force_reinstall
|
||||
self.ignore_dependencies = ignore_dependencies
|
||||
self.ignore_installed = ignore_installed
|
||||
self.ignore_requires_python = ignore_requires_python
|
||||
self.use_user_site = use_user_site
|
||||
self._make_install_req = make_install_req
|
||||
|
||||
self._discovered_dependencies = \
|
||||
defaultdict(list) # type: DiscoveredDependencies
|
||||
|
||||
def resolve(self, root_reqs, check_supported_wheels):
|
||||
# type: (List[InstallRequirement], bool) -> RequirementSet
|
||||
"""Resolve what operations need to be done
|
||||
|
||||
As a side-effect of this method, the packages (and their dependencies)
|
||||
are downloaded, unpacked and prepared for installation. This
|
||||
preparation is done by ``pip.operations.prepare``.
|
||||
|
||||
Once PyPI has static dependency metadata available, it would be
|
||||
possible to move the preparation to become a step separated from
|
||||
dependency resolution.
|
||||
"""
|
||||
requirement_set = RequirementSet(
|
||||
check_supported_wheels=check_supported_wheels
|
||||
)
|
||||
for req in root_reqs:
|
||||
if req.constraint:
|
||||
check_invalid_constraint_type(req)
|
||||
requirement_set.add_requirement(req)
|
||||
|
||||
# Actually prepare the files, and collect any exceptions. Most hash
|
||||
# exceptions cannot be checked ahead of time, because
|
||||
# _populate_link() needs to be called before we can make decisions
|
||||
# based on link type.
|
||||
discovered_reqs = [] # type: List[InstallRequirement]
|
||||
hash_errors = HashErrors()
|
||||
for req in chain(requirement_set.all_requirements, discovered_reqs):
|
||||
try:
|
||||
discovered_reqs.extend(self._resolve_one(requirement_set, req))
|
||||
except HashError as exc:
|
||||
exc.req = req
|
||||
hash_errors.append(exc)
|
||||
|
||||
if hash_errors:
|
||||
raise hash_errors
|
||||
|
||||
return requirement_set
|
||||
|
||||
def _is_upgrade_allowed(self, req):
|
||||
# type: (InstallRequirement) -> bool
|
||||
if self.upgrade_strategy == "to-satisfy-only":
|
||||
return False
|
||||
elif self.upgrade_strategy == "eager":
|
||||
return True
|
||||
else:
|
||||
assert self.upgrade_strategy == "only-if-needed"
|
||||
return req.user_supplied or req.constraint
|
||||
|
||||
def _set_req_to_reinstall(self, req):
|
||||
# type: (InstallRequirement) -> None
|
||||
"""
|
||||
Set a requirement to be installed.
|
||||
"""
|
||||
# Don't uninstall the conflict if doing a user install and the
|
||||
# conflict is not a user install.
|
||||
if not self.use_user_site or dist_in_usersite(req.satisfied_by):
|
||||
req.should_reinstall = True
|
||||
req.satisfied_by = None
|
||||
|
||||
def _check_skip_installed(self, req_to_install):
|
||||
# type: (InstallRequirement) -> Optional[str]
|
||||
"""Check if req_to_install should be skipped.
|
||||
|
||||
This will check if the req is installed, and whether we should upgrade
|
||||
or reinstall it, taking into account all the relevant user options.
|
||||
|
||||
After calling this req_to_install will only have satisfied_by set to
|
||||
None if the req_to_install is to be upgraded/reinstalled etc. Any
|
||||
other value will be a dist recording the current thing installed that
|
||||
satisfies the requirement.
|
||||
|
||||
Note that for vcs urls and the like we can't assess skipping in this
|
||||
routine - we simply identify that we need to pull the thing down,
|
||||
then later on it is pulled down and introspected to assess upgrade/
|
||||
reinstalls etc.
|
||||
|
||||
:return: A text reason for why it was skipped, or None.
|
||||
"""
|
||||
if self.ignore_installed:
|
||||
return None
|
||||
|
||||
req_to_install.check_if_exists(self.use_user_site)
|
||||
if not req_to_install.satisfied_by:
|
||||
return None
|
||||
|
||||
if self.force_reinstall:
|
||||
self._set_req_to_reinstall(req_to_install)
|
||||
return None
|
||||
|
||||
if not self._is_upgrade_allowed(req_to_install):
|
||||
if self.upgrade_strategy == "only-if-needed":
|
||||
return 'already satisfied, skipping upgrade'
|
||||
return 'already satisfied'
|
||||
|
||||
# Check for the possibility of an upgrade. For link-based
|
||||
# requirements we have to pull the tree down and inspect to assess
|
||||
# the version #, so it's handled way down.
|
||||
if not req_to_install.link:
|
||||
try:
|
||||
self.finder.find_requirement(req_to_install, upgrade=True)
|
||||
except BestVersionAlreadyInstalled:
|
||||
# Then the best version is installed.
|
||||
return 'already up-to-date'
|
||||
except DistributionNotFound:
|
||||
# No distribution found, so we squash the error. It will
|
||||
# be raised later when we re-try later to do the install.
|
||||
# Why don't we just raise here?
|
||||
pass
|
||||
|
||||
self._set_req_to_reinstall(req_to_install)
|
||||
return None
|
||||
|
||||
def _find_requirement_link(self, req):
|
||||
# type: (InstallRequirement) -> Optional[Link]
|
||||
upgrade = self._is_upgrade_allowed(req)
|
||||
best_candidate = self.finder.find_requirement(req, upgrade)
|
||||
if not best_candidate:
|
||||
return None
|
||||
|
||||
# Log a warning per PEP 592 if necessary before returning.
|
||||
link = best_candidate.link
|
||||
if link.is_yanked:
|
||||
reason = link.yanked_reason or '<none given>'
|
||||
msg = (
|
||||
# Mark this as a unicode string to prevent
|
||||
# "UnicodeEncodeError: 'ascii' codec can't encode character"
|
||||
# in Python 2 when the reason contains non-ascii characters.
|
||||
u'The candidate selected for download or install is a '
|
||||
'yanked version: {candidate}\n'
|
||||
'Reason for being yanked: {reason}'
|
||||
).format(candidate=best_candidate, reason=reason)
|
||||
logger.warning(msg)
|
||||
|
||||
return link
|
||||
|
||||
def _populate_link(self, req):
|
||||
# type: (InstallRequirement) -> None
|
||||
"""Ensure that if a link can be found for this, that it is found.
|
||||
|
||||
Note that req.link may still be None - if the requirement is already
|
||||
installed and not needed to be upgraded based on the return value of
|
||||
_is_upgrade_allowed().
|
||||
|
||||
If preparer.require_hashes is True, don't use the wheel cache, because
|
||||
cached wheels, always built locally, have different hashes than the
|
||||
files downloaded from the index server and thus throw false hash
|
||||
mismatches. Furthermore, cached wheels at present have undeterministic
|
||||
contents due to file modification times.
|
||||
"""
|
||||
if req.link is None:
|
||||
req.link = self._find_requirement_link(req)
|
||||
|
||||
if self.wheel_cache is None or self.preparer.require_hashes:
|
||||
return
|
||||
cache_entry = self.wheel_cache.get_cache_entry(
|
||||
link=req.link,
|
||||
package_name=req.name,
|
||||
supported_tags=get_supported(),
|
||||
)
|
||||
if cache_entry is not None:
|
||||
logger.debug('Using cached wheel link: %s', cache_entry.link)
|
||||
if req.link is req.original_link and cache_entry.persistent:
|
||||
req.original_link_is_in_wheel_cache = True
|
||||
req.link = cache_entry.link
|
||||
|
||||
def _get_abstract_dist_for(self, req):
|
||||
# type: (InstallRequirement) -> AbstractDistribution
|
||||
"""Takes a InstallRequirement and returns a single AbstractDist \
|
||||
representing a prepared variant of the same.
|
||||
"""
|
||||
if req.editable:
|
||||
return self.preparer.prepare_editable_requirement(req)
|
||||
|
||||
# satisfied_by is only evaluated by calling _check_skip_installed,
|
||||
# so it must be None here.
|
||||
assert req.satisfied_by is None
|
||||
skip_reason = self._check_skip_installed(req)
|
||||
|
||||
if req.satisfied_by:
|
||||
return self.preparer.prepare_installed_requirement(
|
||||
req, skip_reason
|
||||
)
|
||||
|
||||
# We eagerly populate the link, since that's our "legacy" behavior.
|
||||
self._populate_link(req)
|
||||
abstract_dist = self.preparer.prepare_linked_requirement(req)
|
||||
|
||||
# NOTE
|
||||
# The following portion is for determining if a certain package is
|
||||
# going to be re-installed/upgraded or not and reporting to the user.
|
||||
# This should probably get cleaned up in a future refactor.
|
||||
|
||||
# req.req is only avail after unpack for URL
|
||||
# pkgs repeat check_if_exists to uninstall-on-upgrade
|
||||
# (#14)
|
||||
if not self.ignore_installed:
|
||||
req.check_if_exists(self.use_user_site)
|
||||
|
||||
if req.satisfied_by:
|
||||
should_modify = (
|
||||
self.upgrade_strategy != "to-satisfy-only" or
|
||||
self.force_reinstall or
|
||||
self.ignore_installed or
|
||||
req.link.scheme == 'file'
|
||||
)
|
||||
if should_modify:
|
||||
self._set_req_to_reinstall(req)
|
||||
else:
|
||||
logger.info(
|
||||
'Requirement already satisfied (use --upgrade to upgrade):'
|
||||
' %s', req,
|
||||
)
|
||||
|
||||
return abstract_dist
|
||||
|
||||
def _resolve_one(
|
||||
self,
|
||||
requirement_set, # type: RequirementSet
|
||||
req_to_install, # type: InstallRequirement
|
||||
):
|
||||
# type: (...) -> List[InstallRequirement]
|
||||
"""Prepare a single requirements file.
|
||||
|
||||
:return: A list of additional InstallRequirements to also install.
|
||||
"""
|
||||
# Tell user what we are doing for this requirement:
|
||||
# obtain (editable), skipping, processing (local url), collecting
|
||||
# (remote url or package name)
|
||||
if req_to_install.constraint or req_to_install.prepared:
|
||||
return []
|
||||
|
||||
req_to_install.prepared = True
|
||||
|
||||
abstract_dist = self._get_abstract_dist_for(req_to_install)
|
||||
|
||||
# Parse and return dependencies
|
||||
dist = abstract_dist.get_pkg_resources_distribution()
|
||||
# This will raise UnsupportedPythonVersion if the given Python
|
||||
# version isn't compatible with the distribution's Requires-Python.
|
||||
_check_dist_requires_python(
|
||||
dist, version_info=self._py_version_info,
|
||||
ignore_requires_python=self.ignore_requires_python,
|
||||
)
|
||||
|
||||
more_reqs = [] # type: List[InstallRequirement]
|
||||
|
||||
def add_req(subreq, extras_requested):
|
||||
sub_install_req = self._make_install_req(
|
||||
str(subreq),
|
||||
req_to_install,
|
||||
)
|
||||
parent_req_name = req_to_install.name
|
||||
to_scan_again, add_to_parent = requirement_set.add_requirement(
|
||||
sub_install_req,
|
||||
parent_req_name=parent_req_name,
|
||||
extras_requested=extras_requested,
|
||||
)
|
||||
if parent_req_name and add_to_parent:
|
||||
self._discovered_dependencies[parent_req_name].append(
|
||||
add_to_parent
|
||||
)
|
||||
more_reqs.extend(to_scan_again)
|
||||
|
||||
with indent_log():
|
||||
# We add req_to_install before its dependencies, so that we
|
||||
# can refer to it when adding dependencies.
|
||||
if not requirement_set.has_requirement(req_to_install.name):
|
||||
# 'unnamed' requirements will get added here
|
||||
# 'unnamed' requirements can only come from being directly
|
||||
# provided by the user.
|
||||
assert req_to_install.user_supplied
|
||||
requirement_set.add_requirement(
|
||||
req_to_install, parent_req_name=None,
|
||||
)
|
||||
|
||||
if not self.ignore_dependencies:
|
||||
if req_to_install.extras:
|
||||
logger.debug(
|
||||
"Installing extra requirements: %r",
|
||||
','.join(req_to_install.extras),
|
||||
)
|
||||
missing_requested = sorted(
|
||||
set(req_to_install.extras) - set(dist.extras)
|
||||
)
|
||||
for missing in missing_requested:
|
||||
logger.warning(
|
||||
"%s does not provide the extra '%s'",
|
||||
dist, missing
|
||||
)
|
||||
|
||||
available_requested = sorted(
|
||||
set(dist.extras) & set(req_to_install.extras)
|
||||
)
|
||||
for subreq in dist.requires(available_requested):
|
||||
add_req(subreq, extras_requested=available_requested)
|
||||
|
||||
if not req_to_install.editable and not req_to_install.satisfied_by:
|
||||
# XXX: --no-install leads this to report 'Successfully
|
||||
# downloaded' for only non-editable reqs, even though we took
|
||||
# action on them.
|
||||
req_to_install.successfully_downloaded = True
|
||||
|
||||
return more_reqs
|
||||
|
||||
def get_installation_order(self, req_set):
|
||||
# type: (RequirementSet) -> List[InstallRequirement]
|
||||
"""Create the installation order.
|
||||
|
||||
The installation order is topological - requirements are installed
|
||||
before the requiring thing. We break cycles at an arbitrary point,
|
||||
and make no other guarantees.
|
||||
"""
|
||||
# The current implementation, which we may change at any point
|
||||
# installs the user specified things in the order given, except when
|
||||
# dependencies must come earlier to achieve topological order.
|
||||
order = []
|
||||
ordered_reqs = set() # type: Set[InstallRequirement]
|
||||
|
||||
def schedule(req):
|
||||
if req.satisfied_by or req in ordered_reqs:
|
||||
return
|
||||
if req.constraint:
|
||||
return
|
||||
ordered_reqs.add(req)
|
||||
for dep in self._discovered_dependencies[req.name]:
|
||||
schedule(dep)
|
||||
order.append(req)
|
||||
|
||||
for install_req in req_set.requirements.values():
|
||||
schedule(install_req)
|
||||
return order
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,82 @@
|
|||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import FrozenSet, Iterable, Optional, Tuple
|
||||
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
CandidateLookup = Tuple[
|
||||
Optional["Candidate"],
|
||||
Optional[InstallRequirement],
|
||||
]
|
||||
|
||||
|
||||
def format_name(project, extras):
|
||||
# type: (str, FrozenSet[str]) -> str
|
||||
if not extras:
|
||||
return project
|
||||
canonical_extras = sorted(canonicalize_name(e) for e in extras)
|
||||
return "{}[{}]".format(project, ",".join(canonical_extras))
|
||||
|
||||
|
||||
class Requirement(object):
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
raise NotImplementedError("Subclass should override")
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
return False
|
||||
|
||||
def get_candidate_lookup(self):
|
||||
# type: () -> CandidateLookup
|
||||
raise NotImplementedError("Subclass should override")
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
raise NotImplementedError("Subclass should override")
|
||||
|
||||
|
||||
class Candidate(object):
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
@property
|
||||
def is_installed(self):
|
||||
# type: () -> bool
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
@property
|
||||
def is_editable(self):
|
||||
# type: () -> bool
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
@property
|
||||
def source_link(self):
|
||||
# type: () -> Optional[Link]
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
def iter_dependencies(self, with_requires):
|
||||
# type: (bool) -> Iterable[Optional[Requirement]]
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
raise NotImplementedError("Subclass should override")
|
|
@ -0,0 +1,600 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from pip._vendor.contextlib2 import suppress
|
||||
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.packaging.version import Version
|
||||
|
||||
from pip._internal.exceptions import HashError, MetadataInconsistent
|
||||
from pip._internal.network.lazy_wheel import (
|
||||
HTTPRangeRequestUnsupported,
|
||||
dist_from_wheel_url,
|
||||
)
|
||||
from pip._internal.req.constructors import (
|
||||
install_req_from_editable,
|
||||
install_req_from_line,
|
||||
)
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.logging import indent_log
|
||||
from pip._internal.utils.misc import dist_is_editable, normalize_version_info
|
||||
from pip._internal.utils.packaging import get_requires_python
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from .base import Candidate, format_name
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, FrozenSet, Iterable, Optional, Tuple, Union
|
||||
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
from pip._vendor.pkg_resources import Distribution
|
||||
|
||||
from pip._internal.distributions import AbstractDistribution
|
||||
from pip._internal.models.link import Link
|
||||
|
||||
from .base import Requirement
|
||||
from .factory import Factory
|
||||
|
||||
BaseCandidate = Union[
|
||||
"AlreadyInstalledCandidate",
|
||||
"EditableCandidate",
|
||||
"LinkCandidate",
|
||||
]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_install_req_from_link(link, template):
|
||||
# type: (Link, InstallRequirement) -> InstallRequirement
|
||||
assert not template.editable, "template is editable"
|
||||
if template.req:
|
||||
line = str(template.req)
|
||||
else:
|
||||
line = link.url
|
||||
ireq = install_req_from_line(
|
||||
line,
|
||||
user_supplied=template.user_supplied,
|
||||
comes_from=template.comes_from,
|
||||
use_pep517=template.use_pep517,
|
||||
isolated=template.isolated,
|
||||
constraint=template.constraint,
|
||||
options=dict(
|
||||
install_options=template.install_options,
|
||||
global_options=template.global_options,
|
||||
hashes=template.hash_options
|
||||
),
|
||||
)
|
||||
ireq.original_link = template.original_link
|
||||
ireq.link = link
|
||||
return ireq
|
||||
|
||||
|
||||
def make_install_req_from_editable(link, template):
|
||||
# type: (Link, InstallRequirement) -> InstallRequirement
|
||||
assert template.editable, "template not editable"
|
||||
return install_req_from_editable(
|
||||
link.url,
|
||||
user_supplied=template.user_supplied,
|
||||
comes_from=template.comes_from,
|
||||
use_pep517=template.use_pep517,
|
||||
isolated=template.isolated,
|
||||
constraint=template.constraint,
|
||||
options=dict(
|
||||
install_options=template.install_options,
|
||||
global_options=template.global_options,
|
||||
hashes=template.hash_options
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_install_req_from_dist(dist, template):
|
||||
# type: (Distribution, InstallRequirement) -> InstallRequirement
|
||||
project_name = canonicalize_name(dist.project_name)
|
||||
if template.req:
|
||||
line = str(template.req)
|
||||
elif template.link:
|
||||
line = "{} @ {}".format(project_name, template.link.url)
|
||||
else:
|
||||
line = "{}=={}".format(project_name, dist.parsed_version)
|
||||
ireq = install_req_from_line(
|
||||
line,
|
||||
user_supplied=template.user_supplied,
|
||||
comes_from=template.comes_from,
|
||||
use_pep517=template.use_pep517,
|
||||
isolated=template.isolated,
|
||||
constraint=template.constraint,
|
||||
options=dict(
|
||||
install_options=template.install_options,
|
||||
global_options=template.global_options,
|
||||
hashes=template.hash_options
|
||||
),
|
||||
)
|
||||
ireq.satisfied_by = dist
|
||||
return ireq
|
||||
|
||||
|
||||
class _InstallRequirementBackedCandidate(Candidate):
|
||||
"""A candidate backed by an ``InstallRequirement``.
|
||||
|
||||
This represents a package request with the target not being already
|
||||
in the environment, and needs to be fetched and installed. The backing
|
||||
``InstallRequirement`` is responsible for most of the leg work; this
|
||||
class exposes appropriate information to the resolver.
|
||||
|
||||
:param link: The link passed to the ``InstallRequirement``. The backing
|
||||
``InstallRequirement`` will use this link to fetch the distribution.
|
||||
:param source_link: The link this candidate "originates" from. This is
|
||||
different from ``link`` when the link is found in the wheel cache.
|
||||
``link`` would point to the wheel cache, while this points to the
|
||||
found remote link (e.g. from pypi.org).
|
||||
"""
|
||||
is_installed = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
link, # type: Link
|
||||
source_link, # type: Link
|
||||
ireq, # type: InstallRequirement
|
||||
factory, # type: Factory
|
||||
name=None, # type: Optional[str]
|
||||
version=None, # type: Optional[_BaseVersion]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self._link = link
|
||||
self._source_link = source_link
|
||||
self._factory = factory
|
||||
self._ireq = ireq
|
||||
self._name = name
|
||||
self._version = version
|
||||
self._dist = None # type: Optional[Distribution]
|
||||
self._prepared = False
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({link!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
link=str(self._link),
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
# type: () -> int
|
||||
return hash((self.__class__, self._link))
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (Any) -> bool
|
||||
if isinstance(other, self.__class__):
|
||||
return self._link == other._link
|
||||
return False
|
||||
|
||||
# Needed for Python 2, which does not implement this by default
|
||||
def __ne__(self, other):
|
||||
# type: (Any) -> bool
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def source_link(self):
|
||||
# type: () -> Optional[Link]
|
||||
return self._source_link
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
"""The normalised name of the project the candidate refers to"""
|
||||
if self._name is None:
|
||||
self._name = canonicalize_name(self.dist.project_name)
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
if self._version is None:
|
||||
self._version = self.dist.parsed_version
|
||||
return self._version
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return "{} {} (from {})".format(
|
||||
self.name,
|
||||
self.version,
|
||||
self._link.file_path if self._link.is_file else self._link
|
||||
)
|
||||
|
||||
def _prepare_abstract_distribution(self):
|
||||
# type: () -> AbstractDistribution
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
def _check_metadata_consistency(self):
|
||||
# type: () -> None
|
||||
"""Check for consistency of project name and version of dist."""
|
||||
# TODO: (Longer term) Rather than abort, reject this candidate
|
||||
# and backtrack. This would need resolvelib support.
|
||||
dist = self._dist # type: Distribution
|
||||
name = canonicalize_name(dist.project_name)
|
||||
if self._name is not None and self._name != name:
|
||||
raise MetadataInconsistent(self._ireq, "name", dist.project_name)
|
||||
version = dist.parsed_version
|
||||
if self._version is not None and self._version != version:
|
||||
raise MetadataInconsistent(self._ireq, "version", dist.version)
|
||||
|
||||
def _prepare(self):
|
||||
# type: () -> None
|
||||
if self._prepared:
|
||||
return
|
||||
try:
|
||||
abstract_dist = self._prepare_abstract_distribution()
|
||||
except HashError as e:
|
||||
e.req = self._ireq
|
||||
raise
|
||||
|
||||
self._dist = abstract_dist.get_pkg_resources_distribution()
|
||||
assert self._dist is not None, "Distribution already installed"
|
||||
self._check_metadata_consistency()
|
||||
self._prepared = True
|
||||
|
||||
def _fetch_metadata(self):
|
||||
# type: () -> None
|
||||
"""Fetch metadata, using lazy wheel if possible."""
|
||||
preparer = self._factory.preparer
|
||||
use_lazy_wheel = self._factory.use_lazy_wheel
|
||||
remote_wheel = self._link.is_wheel and not self._link.is_file
|
||||
if use_lazy_wheel and remote_wheel and not preparer.require_hashes:
|
||||
assert self._name is not None
|
||||
logger.info('Collecting %s', self._ireq.req or self._ireq)
|
||||
# If HTTPRangeRequestUnsupported is raised, fallback silently.
|
||||
with indent_log(), suppress(HTTPRangeRequestUnsupported):
|
||||
logger.info(
|
||||
'Obtaining dependency information from %s %s',
|
||||
self._name, self._version,
|
||||
)
|
||||
url = self._link.url.split('#', 1)[0]
|
||||
session = preparer.downloader._session
|
||||
self._dist = dist_from_wheel_url(self._name, url, session)
|
||||
self._check_metadata_consistency()
|
||||
if self._dist is None:
|
||||
self._prepare()
|
||||
|
||||
@property
|
||||
def dist(self):
|
||||
# type: () -> Distribution
|
||||
if self._dist is None:
|
||||
self._fetch_metadata()
|
||||
return self._dist
|
||||
|
||||
def _get_requires_python_specifier(self):
|
||||
# type: () -> Optional[SpecifierSet]
|
||||
requires_python = get_requires_python(self.dist)
|
||||
if requires_python is None:
|
||||
return None
|
||||
try:
|
||||
spec = SpecifierSet(requires_python)
|
||||
except InvalidSpecifier as e:
|
||||
logger.warning(
|
||||
"Package %r has an invalid Requires-Python: %s", self.name, e,
|
||||
)
|
||||
return None
|
||||
return spec
|
||||
|
||||
def iter_dependencies(self, with_requires):
|
||||
# type: (bool) -> Iterable[Optional[Requirement]]
|
||||
if not with_requires:
|
||||
return
|
||||
for r in self.dist.requires():
|
||||
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
|
||||
python_dep = self._factory.make_requires_python_requirement(
|
||||
self._get_requires_python_specifier(),
|
||||
)
|
||||
if python_dep:
|
||||
yield python_dep
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
self._prepare()
|
||||
return self._ireq
|
||||
|
||||
|
||||
class LinkCandidate(_InstallRequirementBackedCandidate):
|
||||
is_editable = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
link, # type: Link
|
||||
template, # type: InstallRequirement
|
||||
factory, # type: Factory
|
||||
name=None, # type: Optional[str]
|
||||
version=None, # type: Optional[_BaseVersion]
|
||||
):
|
||||
# type: (...) -> None
|
||||
source_link = link
|
||||
cache_entry = factory.get_wheel_cache_entry(link, name)
|
||||
if cache_entry is not None:
|
||||
logger.debug("Using cached wheel link: %s", cache_entry.link)
|
||||
link = cache_entry.link
|
||||
ireq = make_install_req_from_link(link, template)
|
||||
|
||||
if (cache_entry is not None and
|
||||
cache_entry.persistent and
|
||||
template.link is template.original_link):
|
||||
ireq.original_link_is_in_wheel_cache = True
|
||||
|
||||
super(LinkCandidate, self).__init__(
|
||||
link=link,
|
||||
source_link=source_link,
|
||||
ireq=ireq,
|
||||
factory=factory,
|
||||
name=name,
|
||||
version=version,
|
||||
)
|
||||
|
||||
def _prepare_abstract_distribution(self):
|
||||
# type: () -> AbstractDistribution
|
||||
return self._factory.preparer.prepare_linked_requirement(
|
||||
self._ireq, parallel_builds=True,
|
||||
)
|
||||
|
||||
|
||||
class EditableCandidate(_InstallRequirementBackedCandidate):
|
||||
is_editable = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
link, # type: Link
|
||||
template, # type: InstallRequirement
|
||||
factory, # type: Factory
|
||||
name=None, # type: Optional[str]
|
||||
version=None, # type: Optional[_BaseVersion]
|
||||
):
|
||||
# type: (...) -> None
|
||||
super(EditableCandidate, self).__init__(
|
||||
link=link,
|
||||
source_link=link,
|
||||
ireq=make_install_req_from_editable(link, template),
|
||||
factory=factory,
|
||||
name=name,
|
||||
version=version,
|
||||
)
|
||||
|
||||
def _prepare_abstract_distribution(self):
|
||||
# type: () -> AbstractDistribution
|
||||
return self._factory.preparer.prepare_editable_requirement(self._ireq)
|
||||
|
||||
|
||||
class AlreadyInstalledCandidate(Candidate):
|
||||
is_installed = True
|
||||
source_link = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dist, # type: Distribution
|
||||
template, # type: InstallRequirement
|
||||
factory, # type: Factory
|
||||
):
|
||||
# type: (...) -> None
|
||||
self.dist = dist
|
||||
self._ireq = make_install_req_from_dist(dist, template)
|
||||
self._factory = factory
|
||||
|
||||
# This is just logging some messages, so we can do it eagerly.
|
||||
# The returned dist would be exactly the same as self.dist because we
|
||||
# set satisfied_by in make_install_req_from_dist.
|
||||
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
|
||||
skip_reason = "already satisfied"
|
||||
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({distribution!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
distribution=self.dist,
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
# type: () -> int
|
||||
return hash((self.__class__, self.name, self.version))
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (Any) -> bool
|
||||
if isinstance(other, self.__class__):
|
||||
return self.name == other.name and self.version == other.version
|
||||
return False
|
||||
|
||||
# Needed for Python 2, which does not implement this by default
|
||||
def __ne__(self, other):
|
||||
# type: (Any) -> bool
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
return canonicalize_name(self.dist.project_name)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
return self.dist.parsed_version
|
||||
|
||||
@property
|
||||
def is_editable(self):
|
||||
# type: () -> bool
|
||||
return dist_is_editable(self.dist)
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return "{} {} (Installed)".format(self.name, self.version)
|
||||
|
||||
def iter_dependencies(self, with_requires):
|
||||
# type: (bool) -> Iterable[Optional[Requirement]]
|
||||
if not with_requires:
|
||||
return
|
||||
for r in self.dist.requires():
|
||||
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
return None
|
||||
|
||||
|
||||
class ExtrasCandidate(Candidate):
|
||||
"""A candidate that has 'extras', indicating additional dependencies.
|
||||
|
||||
Requirements can be for a project with dependencies, something like
|
||||
foo[extra]. The extras don't affect the project/version being installed
|
||||
directly, but indicate that we need additional dependencies. We model that
|
||||
by having an artificial ExtrasCandidate that wraps the "base" candidate.
|
||||
|
||||
The ExtrasCandidate differs from the base in the following ways:
|
||||
|
||||
1. It has a unique name, of the form foo[extra]. This causes the resolver
|
||||
to treat it as a separate node in the dependency graph.
|
||||
2. When we're getting the candidate's dependencies,
|
||||
a) We specify that we want the extra dependencies as well.
|
||||
b) We add a dependency on the base candidate.
|
||||
See below for why this is needed.
|
||||
3. We return None for the underlying InstallRequirement, as the base
|
||||
candidate will provide it, and we don't want to end up with duplicates.
|
||||
|
||||
The dependency on the base candidate is needed so that the resolver can't
|
||||
decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
|
||||
version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
|
||||
respectively forces the resolver to recognise that this is a conflict.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
base, # type: BaseCandidate
|
||||
extras, # type: FrozenSet[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self.base = base
|
||||
self.extras = extras
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}(base={base!r}, extras={extras!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
base=self.base,
|
||||
extras=self.extras,
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
# type: () -> int
|
||||
return hash((self.base, self.extras))
|
||||
|
||||
def __eq__(self, other):
|
||||
# type: (Any) -> bool
|
||||
if isinstance(other, self.__class__):
|
||||
return self.base == other.base and self.extras == other.extras
|
||||
return False
|
||||
|
||||
# Needed for Python 2, which does not implement this by default
|
||||
def __ne__(self, other):
|
||||
# type: (Any) -> bool
|
||||
return not self.__eq__(other)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
"""The normalised name of the project the candidate refers to"""
|
||||
return format_name(self.base.name, self.extras)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
return self.base.version
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return "{} [{}]".format(
|
||||
self.base.format_for_error(),
|
||||
", ".join(sorted(self.extras))
|
||||
)
|
||||
|
||||
@property
|
||||
def is_installed(self):
|
||||
# type: () -> bool
|
||||
return self.base.is_installed
|
||||
|
||||
@property
|
||||
def is_editable(self):
|
||||
# type: () -> bool
|
||||
return self.base.is_editable
|
||||
|
||||
@property
|
||||
def source_link(self):
|
||||
# type: () -> Optional[Link]
|
||||
return self.base.source_link
|
||||
|
||||
def iter_dependencies(self, with_requires):
|
||||
# type: (bool) -> Iterable[Optional[Requirement]]
|
||||
factory = self.base._factory
|
||||
|
||||
# Add a dependency on the exact base
|
||||
# (See note 2b in the class docstring)
|
||||
yield factory.make_requirement_from_candidate(self.base)
|
||||
if not with_requires:
|
||||
return
|
||||
|
||||
# The user may have specified extras that the candidate doesn't
|
||||
# support. We ignore any unsupported extras here.
|
||||
valid_extras = self.extras.intersection(self.base.dist.extras)
|
||||
invalid_extras = self.extras.difference(self.base.dist.extras)
|
||||
for extra in sorted(invalid_extras):
|
||||
logger.warning(
|
||||
"%s %s does not provide the extra '%s'",
|
||||
self.base.name,
|
||||
self.version,
|
||||
extra
|
||||
)
|
||||
|
||||
for r in self.base.dist.requires(valid_extras):
|
||||
requirement = factory.make_requirement_from_spec(
|
||||
str(r), self.base._ireq, valid_extras,
|
||||
)
|
||||
if requirement:
|
||||
yield requirement
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
# We don't return anything here, because we always
|
||||
# depend on the base candidate, and we'll get the
|
||||
# install requirement from that.
|
||||
return None
|
||||
|
||||
|
||||
class RequiresPythonCandidate(Candidate):
|
||||
is_installed = False
|
||||
source_link = None
|
||||
|
||||
def __init__(self, py_version_info):
|
||||
# type: (Optional[Tuple[int, ...]]) -> None
|
||||
if py_version_info is not None:
|
||||
version_info = normalize_version_info(py_version_info)
|
||||
else:
|
||||
version_info = sys.version_info[:3]
|
||||
self._version = Version(".".join(str(c) for c in version_info))
|
||||
|
||||
# We don't need to implement __eq__() and __ne__() since there is always
|
||||
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
|
||||
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
# Avoid conflicting with the PyPI package "Python".
|
||||
return "<Python from Requires-Python>"
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
return self._version
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return "Python {}".format(self.version)
|
||||
|
||||
def iter_dependencies(self, with_requires):
|
||||
# type: (bool) -> Iterable[Optional[Requirement]]
|
||||
return ()
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
return None
|
|
@ -0,0 +1,459 @@
|
|||
import collections
|
||||
import logging
|
||||
|
||||
from pip._vendor import six
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pip._internal.exceptions import (
|
||||
DistributionNotFound,
|
||||
InstallationError,
|
||||
UnsupportedPythonVersion,
|
||||
UnsupportedWheel,
|
||||
)
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.compatibility_tags import get_supported
|
||||
from pip._internal.utils.hashes import Hashes
|
||||
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 (
|
||||
FrozenSet,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
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.cache import CacheEntry, WheelCache
|
||||
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.resolution.base import InstallRequirementProvider
|
||||
|
||||
from .base import Candidate, Requirement
|
||||
from .candidates import BaseCandidate
|
||||
|
||||
C = TypeVar("C")
|
||||
Cache = Dict[Link, C]
|
||||
VersionCandidates = Dict[_BaseVersion, Candidate]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Factory(object):
|
||||
def __init__(
|
||||
self,
|
||||
finder, # type: PackageFinder
|
||||
preparer, # type: RequirementPreparer
|
||||
make_install_req, # type: InstallRequirementProvider
|
||||
wheel_cache, # type: Optional[WheelCache]
|
||||
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, ...]]
|
||||
lazy_wheel=False, # type: bool
|
||||
):
|
||||
# type: (...) -> None
|
||||
self._finder = finder
|
||||
self.preparer = preparer
|
||||
self._wheel_cache = wheel_cache
|
||||
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.use_lazy_wheel = lazy_wheel
|
||||
|
||||
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 = {}
|
||||
|
||||
@property
|
||||
def force_reinstall(self):
|
||||
# type: () -> bool
|
||||
return self._force_reinstall
|
||||
|
||||
def _make_candidate_from_dist(
|
||||
self,
|
||||
dist, # type: Distribution
|
||||
extras, # type: FrozenSet[str]
|
||||
template, # type: InstallRequirement
|
||||
):
|
||||
# type: (...) -> Candidate
|
||||
base = AlreadyInstalledCandidate(dist, template, factory=self)
|
||||
if extras:
|
||||
return ExtrasCandidate(base, extras)
|
||||
return base
|
||||
|
||||
def _make_candidate_from_link(
|
||||
self,
|
||||
link, # type: Link
|
||||
extras, # type: FrozenSet[str]
|
||||
template, # type: InstallRequirement
|
||||
name, # type: Optional[str]
|
||||
version, # type: Optional[_BaseVersion]
|
||||
):
|
||||
# type: (...) -> Candidate
|
||||
# TODO: Check already installed candidate, and use it if the link and
|
||||
# editable flag match.
|
||||
if template.editable:
|
||||
if link not in self._editable_candidate_cache:
|
||||
self._editable_candidate_cache[link] = EditableCandidate(
|
||||
link, template, 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, template, 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,
|
||||
ireqs, # type: Sequence[InstallRequirement]
|
||||
specifier, # type: SpecifierSet
|
||||
):
|
||||
# type: (...) -> Iterable[Candidate]
|
||||
if not ireqs:
|
||||
return ()
|
||||
|
||||
# The InstallRequirement implementation requires us to give it a
|
||||
# "template". Here we just choose the first requirement to represent
|
||||
# all of them.
|
||||
# Hopefully the Project model can correct this mismatch in the future.
|
||||
template = ireqs[0]
|
||||
name = canonicalize_name(template.req.name)
|
||||
|
||||
hashes = Hashes()
|
||||
extras = frozenset() # type: FrozenSet[str]
|
||||
for ireq in ireqs:
|
||||
specifier &= ireq.req.specifier
|
||||
hashes |= ireq.hashes(trust_internet=False)
|
||||
extras |= frozenset(ireq.extras)
|
||||
|
||||
# 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.
|
||||
candidates = collections.OrderedDict() # type: VersionCandidates
|
||||
|
||||
# Get the installed version, if it matches, unless the user
|
||||
# specified `--force-reinstall`, when we want the version from
|
||||
# the index instead.
|
||||
installed_version = None
|
||||
installed_candidate = 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 specifier.contains(installed_version, prereleases=True):
|
||||
installed_candidate = self._make_candidate_from_dist(
|
||||
dist=installed_dist,
|
||||
extras=extras,
|
||||
template=template,
|
||||
)
|
||||
|
||||
found = self._finder.find_best_candidate(
|
||||
project_name=name,
|
||||
specifier=specifier,
|
||||
hashes=hashes,
|
||||
)
|
||||
for ican in found.iter_applicable():
|
||||
if ican.version == installed_version and installed_candidate:
|
||||
candidate = installed_candidate
|
||||
else:
|
||||
candidate = self._make_candidate_from_link(
|
||||
link=ican.link,
|
||||
extras=extras,
|
||||
template=template,
|
||||
name=name,
|
||||
version=ican.version,
|
||||
)
|
||||
candidates[ican.version] = candidate
|
||||
|
||||
# Yield the installed version even if it is not found on the index.
|
||||
if installed_version and installed_candidate:
|
||||
candidates[installed_version] = installed_candidate
|
||||
|
||||
return six.itervalues(candidates)
|
||||
|
||||
def find_candidates(self, requirements, constraint):
|
||||
# type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate]
|
||||
explicit_candidates = set() # type: Set[Candidate]
|
||||
ireqs = [] # type: List[InstallRequirement]
|
||||
for req in requirements:
|
||||
cand, ireq = req.get_candidate_lookup()
|
||||
if cand is not None:
|
||||
explicit_candidates.add(cand)
|
||||
if ireq is not None:
|
||||
ireqs.append(ireq)
|
||||
|
||||
# If none of the requirements want an explicit candidate, we can ask
|
||||
# the finder for candidates.
|
||||
if not explicit_candidates:
|
||||
return self._iter_found_candidates(ireqs, constraint)
|
||||
|
||||
if constraint:
|
||||
name = explicit_candidates.pop().name
|
||||
raise InstallationError(
|
||||
"Could not satisfy constraints for {!r}: installation from "
|
||||
"path or url cannot be constrained to a version".format(name)
|
||||
)
|
||||
|
||||
return (
|
||||
c for c in explicit_candidates
|
||||
if all(req.is_satisfied_by(c) for req in requirements)
|
||||
)
|
||||
|
||||
def make_requirement_from_install_req(self, ireq, requested_extras):
|
||||
# type: (InstallRequirement, Iterable[str]) -> Optional[Requirement]
|
||||
if not ireq.match_markers(requested_extras):
|
||||
logger.info(
|
||||
"Ignoring %s: markers '%s' don't match your environment",
|
||||
ireq.name, ireq.markers,
|
||||
)
|
||||
return None
|
||||
if not ireq.link:
|
||||
return SpecifierRequirement(ireq)
|
||||
if ireq.link.is_wheel:
|
||||
wheel = Wheel(ireq.link.filename)
|
||||
if not wheel.supported(self._finder.target_python.get_tags()):
|
||||
msg = "{} is not a supported wheel on this platform.".format(
|
||||
wheel.filename,
|
||||
)
|
||||
raise UnsupportedWheel(msg)
|
||||
cand = self._make_candidate_from_link(
|
||||
ireq.link,
|
||||
extras=frozenset(ireq.extras),
|
||||
template=ireq,
|
||||
name=canonicalize_name(ireq.name) if ireq.name else None,
|
||||
version=None,
|
||||
)
|
||||
return self.make_requirement_from_candidate(cand)
|
||||
|
||||
def make_requirement_from_candidate(self, candidate):
|
||||
# type: (Candidate) -> ExplicitRequirement
|
||||
return ExplicitRequirement(candidate)
|
||||
|
||||
def make_requirement_from_spec(
|
||||
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)
|
||||
return self.make_requirement_from_install_req(ireq, requested_extras)
|
||||
|
||||
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 get_wheel_cache_entry(self, link, name):
|
||||
# type: (Link, Optional[str]) -> Optional[CacheEntry]
|
||||
"""Look up the link in the wheel cache.
|
||||
|
||||
If ``preparer.require_hashes`` is True, don't use the wheel cache,
|
||||
because cached wheels, always built locally, have different hashes
|
||||
than the files downloaded from the index server and thus throw false
|
||||
hash mismatches. Furthermore, cached wheels at present have
|
||||
nondeterministic contents due to file modification times.
|
||||
"""
|
||||
if self._wheel_cache is None or self.preparer.require_hashes:
|
||||
return None
|
||||
return self._wheel_cache.get_cache_entry(
|
||||
link=link,
|
||||
package_name=name,
|
||||
supported_tags=get_supported(),
|
||||
)
|
||||
|
||||
def get_dist_to_uninstall(self, candidate):
|
||||
# type: (Candidate) -> Optional[Distribution]
|
||||
# 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 None
|
||||
|
||||
# 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 dist
|
||||
|
||||
# We're installing into user site. Remove the user site installation.
|
||||
if dist_in_usersite(dist):
|
||||
return dist
|
||||
|
||||
# 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 None
|
||||
|
||||
def _report_requires_python_error(
|
||||
self,
|
||||
requirement, # type: RequiresPythonRequirement
|
||||
template, # type: Candidate
|
||||
):
|
||||
# type: (...) -> UnsupportedPythonVersion
|
||||
message_format = (
|
||||
"Package {package!r} requires a different Python: "
|
||||
"{version} not in {specifier!r}"
|
||||
)
|
||||
message = message_format.format(
|
||||
package=template.name,
|
||||
version=self._python_candidate.version,
|
||||
specifier=str(requirement.specifier),
|
||||
)
|
||||
return UnsupportedPythonVersion(message)
|
||||
|
||||
def get_installation_error(self, e):
|
||||
# type: (ResolutionImpossible) -> InstallationError
|
||||
|
||||
assert e.causes, "Installation error reported with no cause"
|
||||
|
||||
# If one of the things we can't solve is "we need Python X.Y",
|
||||
# that is what we report.
|
||||
for cause in e.causes:
|
||||
if isinstance(cause.requirement, RequiresPythonRequirement):
|
||||
return self._report_requires_python_error(
|
||||
cause.requirement,
|
||||
cause.parent,
|
||||
)
|
||||
|
||||
# Otherwise, we have a set of causes which can't all be satisfied
|
||||
# at once.
|
||||
|
||||
# The simplest case is when we have *one* cause that can't be
|
||||
# satisfied. We just report that case.
|
||||
if len(e.causes) == 1:
|
||||
req, parent = e.causes[0]
|
||||
if parent is None:
|
||||
req_disp = str(req)
|
||||
else:
|
||||
req_disp = '{} (from {})'.format(req, parent.name)
|
||||
logger.critical(
|
||||
"Could not find a version that satisfies the requirement %s",
|
||||
req_disp,
|
||||
)
|
||||
return DistributionNotFound(
|
||||
'No matching distribution found for {}'.format(req)
|
||||
)
|
||||
|
||||
# OK, we now have a list of requirements that can't all be
|
||||
# satisfied at once.
|
||||
|
||||
# A couple of formatting helpers
|
||||
def text_join(parts):
|
||||
# type: (List[str]) -> str
|
||||
if len(parts) == 1:
|
||||
return parts[0]
|
||||
|
||||
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
||||
|
||||
def readable_form(cand):
|
||||
# type: (Candidate) -> str
|
||||
return "{} {}".format(cand.name, cand.version)
|
||||
|
||||
def describe_trigger(parent):
|
||||
# type: (Candidate) -> str
|
||||
ireq = parent.get_install_requirement()
|
||||
if not ireq or not ireq.comes_from:
|
||||
return "{} {}".format(parent.name, parent.version)
|
||||
if isinstance(ireq.comes_from, InstallRequirement):
|
||||
return str(ireq.comes_from.name)
|
||||
return str(ireq.comes_from)
|
||||
|
||||
triggers = []
|
||||
for req, parent in e.causes:
|
||||
if parent is None:
|
||||
# This is a root requirement, so we can report it directly
|
||||
trigger = req.format_for_error()
|
||||
else:
|
||||
trigger = describe_trigger(parent)
|
||||
triggers.append(trigger)
|
||||
|
||||
if triggers:
|
||||
info = text_join(triggers)
|
||||
else:
|
||||
info = "the requested packages"
|
||||
|
||||
msg = "Cannot install {} because these package versions " \
|
||||
"have conflicting dependencies.".format(info)
|
||||
logger.critical(msg)
|
||||
msg = "\nThe conflict is caused by:"
|
||||
for req, parent in e.causes:
|
||||
msg = msg + "\n "
|
||||
if parent:
|
||||
msg = msg + "{} {} depends on ".format(
|
||||
parent.name,
|
||||
parent.version
|
||||
)
|
||||
else:
|
||||
msg = msg + "The user requested "
|
||||
msg = msg + req.format_for_error()
|
||||
|
||||
msg = msg + "\n\n" + \
|
||||
"To fix this you could try to:\n" + \
|
||||
"1. loosen the range of package versions you've specified\n" + \
|
||||
"2. remove package versions to allow pip attempt to solve " + \
|
||||
"the dependency conflict\n"
|
||||
|
||||
logger.info(msg)
|
||||
|
||||
return DistributionNotFound(
|
||||
"ResolutionImpossible: for help visit "
|
||||
"https://pip.pypa.io/en/latest/user_guide/"
|
||||
"#fixing-conflicting-dependencies"
|
||||
)
|
|
@ -0,0 +1,153 @@
|
|||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.resolvelib.providers import AbstractProvider
|
||||
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from .base import Requirement, Candidate
|
||||
from .factory import Factory
|
||||
|
||||
# Notes on the relationship between the provider, the factory, and the
|
||||
# candidate and requirement classes.
|
||||
#
|
||||
# The provider is a direct implementation of the resolvelib class. Its role
|
||||
# is to deliver the API that resolvelib expects.
|
||||
#
|
||||
# Rather than work with completely abstract "requirement" and "candidate"
|
||||
# concepts as resolvelib does, pip has concrete classes implementing these two
|
||||
# ideas. The API of Requirement and Candidate objects are defined in the base
|
||||
# classes, but essentially map fairly directly to the equivalent provider
|
||||
# methods. In particular, `find_matches` and `is_satisfied_by` are
|
||||
# requirement methods, and `get_dependencies` is a candidate method.
|
||||
#
|
||||
# The factory is the interface to pip's internal mechanisms. It is stateless,
|
||||
# and is created by the resolver and held as a property of the provider. It is
|
||||
# responsible for creating Requirement and Candidate objects, and provides
|
||||
# services to those objects (access to pip's finder and preparer).
|
||||
|
||||
|
||||
class PipProvider(AbstractProvider):
|
||||
def __init__(
|
||||
self,
|
||||
factory, # type: Factory
|
||||
constraints, # type: Dict[str, SpecifierSet]
|
||||
ignore_dependencies, # type: bool
|
||||
upgrade_strategy, # type: str
|
||||
user_requested, # type: Set[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self._factory = factory
|
||||
self._constraints = constraints
|
||||
self._ignore_dependencies = ignore_dependencies
|
||||
self._upgrade_strategy = upgrade_strategy
|
||||
self.user_requested = user_requested
|
||||
|
||||
def _sort_matches(self, matches):
|
||||
# type: (Iterable[Candidate]) -> Sequence[Candidate]
|
||||
|
||||
# The requirement is responsible for returning a sequence of potential
|
||||
# candidates, one per version. The provider handles the logic of
|
||||
# deciding the order in which these candidates should be passed to
|
||||
# the resolver.
|
||||
|
||||
# The `matches` argument is a sequence of candidates, one per version,
|
||||
# which are potential options to be installed. The requirement will
|
||||
# have already sorted out whether to give us an already-installed
|
||||
# candidate or a version from PyPI (i.e., it will deal with options
|
||||
# like --force-reinstall and --ignore-installed).
|
||||
|
||||
# We now work out the correct order.
|
||||
#
|
||||
# 1. If no other considerations apply, later versions take priority.
|
||||
# 2. An already installed distribution is preferred over any other,
|
||||
# unless the user has requested an upgrade.
|
||||
# Upgrades are allowed when:
|
||||
# * The --upgrade flag is set, and
|
||||
# - The project was specified on the command line, or
|
||||
# - The project is a dependency and the "eager" upgrade strategy
|
||||
# was requested.
|
||||
def _eligible_for_upgrade(name):
|
||||
# type: (str) -> bool
|
||||
"""Are upgrades allowed for this project?
|
||||
|
||||
This checks the upgrade strategy, and whether the project was one
|
||||
that the user specified in the command line, in order to decide
|
||||
whether we should upgrade if there's a newer version available.
|
||||
|
||||
(Note that we don't need access to the `--upgrade` flag, because
|
||||
an upgrade strategy of "to-satisfy-only" means that `--upgrade`
|
||||
was not specified).
|
||||
"""
|
||||
if self._upgrade_strategy == "eager":
|
||||
return True
|
||||
elif self._upgrade_strategy == "only-if-needed":
|
||||
return (name in self.user_requested)
|
||||
return False
|
||||
|
||||
def sort_key(c):
|
||||
# type: (Candidate) -> int
|
||||
"""Return a sort key for the matches.
|
||||
|
||||
The highest priority should be given to installed candidates that
|
||||
are not eligible for upgrade. We use the integer value in the first
|
||||
part of the key to sort these before other candidates.
|
||||
|
||||
We only pull the installed candidate to the bottom (i.e. most
|
||||
preferred), but otherwise keep the ordering returned by the
|
||||
requirement. The requirement is responsible for returning a list
|
||||
otherwise sorted for the resolver, taking account for versions
|
||||
and binary preferences as specified by the user.
|
||||
"""
|
||||
if c.is_installed and not _eligible_for_upgrade(c.name):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
return sorted(matches, key=sort_key)
|
||||
|
||||
def identify(self, dependency):
|
||||
# type: (Union[Requirement, Candidate]) -> str
|
||||
return dependency.name
|
||||
|
||||
def get_preference(
|
||||
self,
|
||||
resolution, # type: Optional[Candidate]
|
||||
candidates, # type: Sequence[Candidate]
|
||||
information # type: Sequence[Tuple[Requirement, Candidate]]
|
||||
):
|
||||
# type: (...) -> Any
|
||||
# Use the "usual" value for now
|
||||
return len(candidates)
|
||||
|
||||
def find_matches(self, requirements):
|
||||
# type: (Sequence[Requirement]) -> Iterable[Candidate]
|
||||
if not requirements:
|
||||
return []
|
||||
constraint = self._constraints.get(
|
||||
requirements[0].name, SpecifierSet(),
|
||||
)
|
||||
candidates = self._factory.find_candidates(requirements, constraint)
|
||||
return reversed(self._sort_matches(candidates))
|
||||
|
||||
def is_satisfied_by(self, requirement, candidate):
|
||||
# type: (Requirement, Candidate) -> bool
|
||||
return requirement.is_satisfied_by(candidate)
|
||||
|
||||
def get_dependencies(self, candidate):
|
||||
# type: (Candidate) -> Sequence[Requirement]
|
||||
with_requires = not self._ignore_dependencies
|
||||
return [
|
||||
r
|
||||
for r in candidate.iter_dependencies(with_requires)
|
||||
if r is not None
|
||||
]
|
|
@ -0,0 +1,137 @@
|
|||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from .base import Requirement, format_name
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
from .base import Candidate, CandidateLookup
|
||||
|
||||
|
||||
class ExplicitRequirement(Requirement):
|
||||
def __init__(self, candidate):
|
||||
# type: (Candidate) -> None
|
||||
self.candidate = candidate
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({candidate!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
candidate=self.candidate,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
# No need to canonicalise - the candidate did this
|
||||
return self.candidate.name
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return self.candidate.format_for_error()
|
||||
|
||||
def get_candidate_lookup(self):
|
||||
# type: () -> CandidateLookup
|
||||
return self.candidate, None
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
return candidate == self.candidate
|
||||
|
||||
|
||||
class SpecifierRequirement(Requirement):
|
||||
def __init__(self, ireq):
|
||||
# type: (InstallRequirement) -> None
|
||||
assert ireq.link is None, "This is a link, not a specifier"
|
||||
self._ireq = ireq
|
||||
self._extras = frozenset(ireq.extras)
|
||||
|
||||
def __str__(self):
|
||||
# type: () -> str
|
||||
return str(self._ireq.req)
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({requirement!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
requirement=str(self._ireq.req),
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
canonical_name = canonicalize_name(self._ireq.req.name)
|
||||
return format_name(canonical_name, self._extras)
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
|
||||
# Convert comma-separated specifiers into "A, B, ..., F and G"
|
||||
# This makes the specifier a bit more "human readable", without
|
||||
# risking a change in meaning. (Hopefully! Not all edge cases have
|
||||
# been checked)
|
||||
parts = [s.strip() for s in str(self).split(",")]
|
||||
if len(parts) == 0:
|
||||
return ""
|
||||
elif len(parts) == 1:
|
||||
return parts[0]
|
||||
|
||||
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
||||
|
||||
def get_candidate_lookup(self):
|
||||
# type: () -> CandidateLookup
|
||||
return None, self._ireq
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
assert candidate.name == self.name, \
|
||||
"Internal issue: Candidate is not for this requirement " \
|
||||
" {} vs {}".format(candidate.name, self.name)
|
||||
# We can safely always allow prereleases here since PackageFinder
|
||||
# already implements the prerelease logic, and would have filtered out
|
||||
# prerelease candidates if the user does not expect them.
|
||||
spec = self._ireq.req.specifier
|
||||
return spec.contains(candidate.version, prereleases=True)
|
||||
|
||||
|
||||
class RequiresPythonRequirement(Requirement):
|
||||
"""A requirement representing Requires-Python metadata.
|
||||
"""
|
||||
def __init__(self, specifier, match):
|
||||
# type: (SpecifierSet, Candidate) -> None
|
||||
self.specifier = specifier
|
||||
self._candidate = match
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({specifier!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
specifier=str(self.specifier),
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
return self._candidate.name
|
||||
|
||||
def format_for_error(self):
|
||||
# type: () -> str
|
||||
return "Python " + str(self.specifier)
|
||||
|
||||
def get_candidate_lookup(self):
|
||||
# type: () -> CandidateLookup
|
||||
if self.specifier.contains(self._candidate.version, prereleases=True):
|
||||
return self._candidate, None
|
||||
return None, None
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
assert candidate.name == self._candidate.name, "Not Python candidate"
|
||||
# We can safely always allow prereleases here since PackageFinder
|
||||
# already implements the prerelease logic, and would have filtered out
|
||||
# prerelease candidates if the user does not expect them.
|
||||
return self.specifier.contains(candidate.version, prereleases=True)
|
|
@ -0,0 +1,259 @@
|
|||
import functools
|
||||
import logging
|
||||
|
||||
from pip._vendor import six
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
|
||||
from pip._vendor.resolvelib import Resolver as RLResolver
|
||||
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.req.req_install import check_invalid_constraint_type
|
||||
from pip._internal.req.req_set import RequirementSet
|
||||
from pip._internal.resolution.base import BaseResolver
|
||||
from pip._internal.resolution.resolvelib.provider import PipProvider
|
||||
from pip._internal.utils.misc import dist_is_editable
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from .factory import Factory
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Dict, List, Optional, Set, Tuple
|
||||
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.resolvelib.resolvers import Result
|
||||
from pip._vendor.resolvelib.structs import Graph
|
||||
|
||||
from pip._internal.cache import WheelCache
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
from pip._internal.operations.prepare import RequirementPreparer
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.resolution.base import InstallRequirementProvider
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resolver(BaseResolver):
|
||||
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
preparer, # type: RequirementPreparer
|
||||
finder, # type: PackageFinder
|
||||
wheel_cache, # type: Optional[WheelCache]
|
||||
make_install_req, # type: InstallRequirementProvider
|
||||
use_user_site, # type: bool
|
||||
ignore_dependencies, # type: bool
|
||||
ignore_installed, # type: bool
|
||||
ignore_requires_python, # type: bool
|
||||
force_reinstall, # type: bool
|
||||
upgrade_strategy, # type: str
|
||||
py_version_info=None, # type: Optional[Tuple[int, ...]]
|
||||
lazy_wheel=False, # type: bool
|
||||
):
|
||||
super(Resolver, self).__init__()
|
||||
if lazy_wheel:
|
||||
logger.warning(
|
||||
'pip is using lazily downloaded wheels using HTTP '
|
||||
'range requests to obtain dependency information. '
|
||||
'This experimental feature is enabled through '
|
||||
'--use-feature=fast-deps and it is not ready for production.'
|
||||
)
|
||||
|
||||
assert upgrade_strategy in self._allowed_strategies
|
||||
|
||||
self.factory = Factory(
|
||||
finder=finder,
|
||||
preparer=preparer,
|
||||
make_install_req=make_install_req,
|
||||
wheel_cache=wheel_cache,
|
||||
use_user_site=use_user_site,
|
||||
force_reinstall=force_reinstall,
|
||||
ignore_installed=ignore_installed,
|
||||
ignore_requires_python=ignore_requires_python,
|
||||
py_version_info=py_version_info,
|
||||
lazy_wheel=lazy_wheel,
|
||||
)
|
||||
self.ignore_dependencies = ignore_dependencies
|
||||
self.upgrade_strategy = upgrade_strategy
|
||||
self._result = None # type: Optional[Result]
|
||||
|
||||
def resolve(self, root_reqs, check_supported_wheels):
|
||||
# type: (List[InstallRequirement], bool) -> RequirementSet
|
||||
|
||||
constraints = {} # type: Dict[str, SpecifierSet]
|
||||
user_requested = set() # type: Set[str]
|
||||
requirements = []
|
||||
for req in root_reqs:
|
||||
if req.constraint:
|
||||
# Ensure we only accept valid constraints
|
||||
problem = check_invalid_constraint_type(req)
|
||||
if problem:
|
||||
raise InstallationError(problem)
|
||||
if not req.match_markers():
|
||||
continue
|
||||
name = canonicalize_name(req.name)
|
||||
if name in constraints:
|
||||
constraints[name] = constraints[name] & req.specifier
|
||||
else:
|
||||
constraints[name] = req.specifier
|
||||
else:
|
||||
if req.user_supplied and req.name:
|
||||
user_requested.add(canonicalize_name(req.name))
|
||||
r = self.factory.make_requirement_from_install_req(
|
||||
req, requested_extras=(),
|
||||
)
|
||||
if r is not None:
|
||||
requirements.append(r)
|
||||
|
||||
provider = PipProvider(
|
||||
factory=self.factory,
|
||||
constraints=constraints,
|
||||
ignore_dependencies=self.ignore_dependencies,
|
||||
upgrade_strategy=self.upgrade_strategy,
|
||||
user_requested=user_requested,
|
||||
)
|
||||
reporter = BaseReporter()
|
||||
resolver = RLResolver(provider, reporter)
|
||||
|
||||
try:
|
||||
try_to_avoid_resolution_too_deep = 2000000
|
||||
self._result = resolver.resolve(
|
||||
requirements, max_rounds=try_to_avoid_resolution_too_deep,
|
||||
)
|
||||
|
||||
except ResolutionImpossible as e:
|
||||
error = self.factory.get_installation_error(e)
|
||||
six.raise_from(error, e)
|
||||
|
||||
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
||||
for candidate in self._result.mapping.values():
|
||||
ireq = candidate.get_install_requirement()
|
||||
if ireq is None:
|
||||
continue
|
||||
|
||||
# Check if there is already an installation under the same name,
|
||||
# and set a flag for later stages to uninstall it, if needed.
|
||||
# * There isn't, good -- no uninstalltion needed.
|
||||
# * The --force-reinstall flag is set. Always reinstall.
|
||||
# * The installation is different in version or editable-ness, so
|
||||
# we need to uninstall it to install the new distribution.
|
||||
# * The installed version is the same as the pending distribution.
|
||||
# Skip this distrubiton altogether to save work.
|
||||
installed_dist = self.factory.get_dist_to_uninstall(candidate)
|
||||
if installed_dist is None:
|
||||
ireq.should_reinstall = False
|
||||
elif self.factory.force_reinstall:
|
||||
ireq.should_reinstall = True
|
||||
elif installed_dist.parsed_version != candidate.version:
|
||||
ireq.should_reinstall = True
|
||||
elif dist_is_editable(installed_dist) != candidate.is_editable:
|
||||
ireq.should_reinstall = True
|
||||
else:
|
||||
continue
|
||||
|
||||
link = candidate.source_link
|
||||
if link and link.is_yanked:
|
||||
# The reason can contain non-ASCII characters, Unicode
|
||||
# is required for Python 2.
|
||||
msg = (
|
||||
u'The candidate selected for download or install is a '
|
||||
u'yanked version: {name!r} candidate (version {version} '
|
||||
u'at {link})\nReason for being yanked: {reason}'
|
||||
).format(
|
||||
name=candidate.name,
|
||||
version=candidate.version,
|
||||
link=link,
|
||||
reason=link.yanked_reason or u'<none given>',
|
||||
)
|
||||
logger.warning(msg)
|
||||
|
||||
req_set.add_named_requirement(ireq)
|
||||
|
||||
return req_set
|
||||
|
||||
def get_installation_order(self, req_set):
|
||||
# type: (RequirementSet) -> List[InstallRequirement]
|
||||
"""Get order for installation of requirements in RequirementSet.
|
||||
|
||||
The returned list contains a requirement before another that depends on
|
||||
it. This helps ensure that the environment is kept consistent as they
|
||||
get installed one-by-one.
|
||||
|
||||
The current implementation creates a topological ordering of the
|
||||
dependency graph, while breaking any cycles in the graph at arbitrary
|
||||
points. We make no guarantees about where the cycle would be broken,
|
||||
other than they would be broken.
|
||||
"""
|
||||
assert self._result is not None, "must call resolve() first"
|
||||
|
||||
graph = self._result.graph
|
||||
weights = get_topological_weights(graph)
|
||||
|
||||
sorted_items = sorted(
|
||||
req_set.requirements.items(),
|
||||
key=functools.partial(_req_set_item_sorter, weights=weights),
|
||||
reverse=True,
|
||||
)
|
||||
return [ireq for _, ireq in sorted_items]
|
||||
|
||||
|
||||
def get_topological_weights(graph):
|
||||
# type: (Graph) -> Dict[Optional[str], int]
|
||||
"""Assign weights to each node based on how "deep" they are.
|
||||
|
||||
This implementation may change at any point in the future without prior
|
||||
notice.
|
||||
|
||||
We take the length for the longest path to any node from root, ignoring any
|
||||
paths that contain a single node twice (i.e. cycles). This is done through
|
||||
a depth-first search through the graph, while keeping track of the path to
|
||||
the node.
|
||||
|
||||
Cycles in the graph result would result in node being revisited while also
|
||||
being it's own path. In this case, take no action. This helps ensure we
|
||||
don't get stuck in a cycle.
|
||||
|
||||
When assigning weight, the longer path (i.e. larger length) is preferred.
|
||||
"""
|
||||
path = set() # type: Set[Optional[str]]
|
||||
weights = {} # type: Dict[Optional[str], int]
|
||||
|
||||
def visit(node):
|
||||
# type: (Optional[str]) -> None
|
||||
if node in path:
|
||||
# We hit a cycle, so we'll break it here.
|
||||
return
|
||||
|
||||
# Time to visit the children!
|
||||
path.add(node)
|
||||
for child in graph.iter_children(node):
|
||||
visit(child)
|
||||
path.remove(node)
|
||||
|
||||
last_known_parent_count = weights.get(node, 0)
|
||||
weights[node] = max(last_known_parent_count, len(path))
|
||||
|
||||
# `None` is guaranteed to be the root node by resolvelib.
|
||||
visit(None)
|
||||
|
||||
# Sanity checks
|
||||
assert weights[None] == 0
|
||||
assert len(weights) == len(graph)
|
||||
|
||||
return weights
|
||||
|
||||
|
||||
def _req_set_item_sorter(
|
||||
item, # type: Tuple[str, InstallRequirement]
|
||||
weights, # type: Dict[Optional[str], int]
|
||||
):
|
||||
# type: (...) -> Tuple[int, str]
|
||||
"""Key function used to sort install requirements for installation.
|
||||
|
||||
Based on the "weight" mapping calculated in ``get_installation_order()``.
|
||||
The canonical package name is returned as the second member as a tie-
|
||||
breaker to ensure the result is predictable, which is useful in tests.
|
||||
"""
|
||||
name = canonicalize_name(item[0])
|
||||
return weights[name], name
|
Loading…
Add table
Add a link
Reference in a new issue