Skip to content

Commit

Permalink
Implement new Provider.find_matches()
Browse files Browse the repository at this point in the history
  • Loading branch information
uranusjr committed May 19, 2020
1 parent 9e8828c commit 0a675ba
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 82 deletions.
21 changes: 13 additions & 8 deletions src/pip/_internal/resolution/resolvelib/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
from typing import Optional, Sequence, Set
from typing import AbstractSet, Optional, Sequence, Tuple

from pip._internal.req.req_install import InstallRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.version import _BaseVersion

from pip._internal.req.req_install import InstallRequirement

CandidateLookup = Tuple[
Optional["Candidate"],
Optional[InstallRequirement],
]


def format_name(project, extras):
# type: (str, Set[str]) -> str
# type: (str, AbstractSet[str]) -> str
if not extras:
return project
canonical_extras = sorted(canonicalize_name(e) for e in extras)
Expand All @@ -24,14 +29,14 @@ def name(self):
# type: () -> str
raise NotImplementedError("Subclass should override")

def find_matches(self, constraint):
# type: (SpecifierSet) -> Sequence[Candidate]
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")


class Candidate(object):
@property
Expand Down
16 changes: 14 additions & 2 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .base import Candidate, format_name

if MYPY_CHECK_RUNNING:
from typing import Any, Optional, Sequence, Set, Tuple, Union
from typing import Any, FrozenSet, Optional, Sequence, Tuple, Union

from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.pkg_resources import Distribution
Expand Down Expand Up @@ -132,6 +132,10 @@ def __repr__(self):
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__):
Expand Down Expand Up @@ -305,6 +309,10 @@ def __repr__(self):
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__):
Expand Down Expand Up @@ -365,7 +373,7 @@ class ExtrasCandidate(Candidate):
def __init__(
self,
base, # type: BaseCandidate
extras, # type: Set[str]
extras, # type: FrozenSet[str]
):
# type: (...) -> None
self.base = base
Expand All @@ -379,6 +387,10 @@ def __repr__(self):
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__):
Expand Down
93 changes: 74 additions & 19 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
InstallationError,
UnsupportedPythonVersion,
)
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
dist_in_site_packages,
dist_in_usersite,
Expand All @@ -29,7 +30,17 @@
)

if MYPY_CHECK_RUNNING:
from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar
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
Expand Down Expand Up @@ -64,7 +75,7 @@ def __init__(
):
# type: (...) -> None

self.finder = finder
self._finder = finder
self.preparer = preparer
self._python_candidate = RequiresPythonCandidate(py_version_info)
self._make_install_req_from_spec = make_install_req
Expand All @@ -86,7 +97,7 @@ def __init__(
def _make_candidate_from_dist(
self,
dist, # type: Distribution
extras, # type: Set[str]
extras, # type: FrozenSet[str]
parent, # type: InstallRequirement
):
# type: (...) -> Candidate
Expand All @@ -98,7 +109,7 @@ def _make_candidate_from_dist(
def _make_candidate_from_link(
self,
link, # type: Link
extras, # type: Set[str]
extras, # type: FrozenSet[str]
parent, # type: InstallRequirement
name=None, # type: Optional[str]
version=None, # type: Optional[_BaseVersion]
Expand All @@ -122,9 +133,28 @@ def _make_candidate_from_link(
return ExtrasCandidate(base, extras)
return base

def iter_found_candidates(self, ireq, extras):
# type: (InstallRequirement, Set[str]) -> Iterator[Candidate]
name = canonicalize_name(ireq.req.name)
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
# "parent", which doesn't really fit with graph-based resolution.
# Here we just choose the first requirement to represent all of them.
# Hopefully the Project model can correct this mismatch in the future.
parent = ireqs[0]
name = canonicalize_name(parent.req.name)

hashes = Hashes()
extras = frozenset() # type: FrozenSet[str]
for ireq in ireqs:
specifier &= ireq.req.specifier
hashes |= ireq.hashes(trust_internet=False)
extras |= ireq.req.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
Expand All @@ -140,47 +170,72 @@ def iter_found_candidates(self, ireq, extras):
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
):
if specifier.contains(installed_version, prereleases=True):
candidate = self._make_candidate_from_dist(
dist=installed_dist,
extras=extras,
parent=ireq,
parent=parent,
)
candidates[installed_version] = candidate

found = self.finder.find_best_candidate(
project_name=ireq.req.name,
specifier=ireq.req.specifier,
hashes=ireq.hashes(trust_internet=False),
found = self._finder.find_best_candidate(
project_name=name,
specifier=specifier,
hashes=hashes,
)
for ican in found.iter_applicable():
if ican.version == installed_version:
continue
candidate = self._make_candidate_from_link(
link=ican.link,
extras=extras,
parent=ireq,
parent=parent,
name=name,
version=ican.version,
)
candidates[ican.version] = 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):
# type: (InstallRequirement) -> Requirement
if ireq.link:
# TODO: Get name and version from ireq, if possible?
# Specifically, this might be needed in "name @ URL"
# syntax - need to check where that syntax is handled.
cand = self._make_candidate_from_link(
ireq.link, extras=set(ireq.extras), parent=ireq,
ireq.link, extras=frozenset(ireq.extras), parent=ireq,
)
return ExplicitRequirement(cand)
return SpecifierRequirement(ireq, factory=self)
return SpecifierRequirement(ireq)

def make_requirement_from_spec(self, specifier, comes_from):
# type: (str, InstallRequirement) -> Requirement
Expand Down
31 changes: 21 additions & 10 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union
from typing import (
Any,
Dict,
Iterable,
Optional,
Sequence,
Set,
Tuple,
Union,
)

from pip._vendor.packaging.version import _BaseVersion

Expand Down Expand Up @@ -47,7 +56,7 @@ def __init__(
self.user_requested = user_requested

def _sort_matches(self, matches):
# type: (Sequence[Candidate]) -> Sequence[Candidate]
# 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
Expand All @@ -70,7 +79,6 @@ def _sort_matches(self, matches):
# - 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?
Expand Down Expand Up @@ -99,10 +107,9 @@ def sort_key(c):
"""
if c.is_installed and not _eligible_for_upgrade(c.name):
return (1, c.version)

return (0, c.version)

return sorted(matches, key=sort_key)
return sorted(matches, key=sort_key, reverse=True)

def identify(self, dependency):
# type: (Union[Requirement, Candidate]) -> str
Expand All @@ -118,11 +125,15 @@ def get_preference(
# Use the "usual" value for now
return len(candidates)

def find_matches(self, requirement):
# type: (Requirement) -> Sequence[Candidate]
constraint = self._constraints.get(requirement.name, SpecifierSet())
matches = requirement.find_matches(constraint)
return self._sort_matches(matches)
def find_matches(self, requirements):
# type: (Sequence[Requirement]) -> Sequence[Candidate]
if not requirements:
return []
constraint = self._constraints.get(
requirements[0].name, SpecifierSet(),
)
candidates = self._factory.find_candidates(requirements, constraint)
return self._sort_matches(candidates)

def is_satisfied_by(self, requirement, candidate):
# type: (Requirement, Candidate) -> bool
Expand Down
Loading

0 comments on commit 0a675ba

Please sign in to comment.