Skip to content

Commit 03b11ee

Browse files
authored
Merge pull request #8267 from uranusjr/new-resolver-candidate-order
Upgrade ResolveLib to 0.4.0 and implement the new Provider.find_matches() interface
2 parents 7c10d3e + 9ee19a1 commit 03b11ee

File tree

14 files changed

+231
-142
lines changed

14 files changed

+231
-142
lines changed

src/pip/_internal/resolution/resolvelib/base.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,20 @@
33
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
44

55
if MYPY_CHECK_RUNNING:
6-
from typing import Iterable, Optional, Sequence, Set
6+
from typing import FrozenSet, Iterable, Optional, Tuple
77

8-
from pip._internal.req.req_install import InstallRequirement
9-
from pip._vendor.packaging.specifiers import SpecifierSet
108
from pip._vendor.packaging.version import _BaseVersion
119

10+
from pip._internal.req.req_install import InstallRequirement
11+
12+
CandidateLookup = Tuple[
13+
Optional["Candidate"],
14+
Optional[InstallRequirement],
15+
]
16+
1217

1318
def format_name(project, extras):
14-
# type: (str, Set[str]) -> str
19+
# type: (str, FrozenSet[str]) -> str
1520
if not extras:
1621
return project
1722
canonical_extras = sorted(canonicalize_name(e) for e in extras)
@@ -24,14 +29,14 @@ def name(self):
2429
# type: () -> str
2530
raise NotImplementedError("Subclass should override")
2631

27-
def find_matches(self, constraint):
28-
# type: (SpecifierSet) -> Sequence[Candidate]
29-
raise NotImplementedError("Subclass should override")
30-
3132
def is_satisfied_by(self, candidate):
3233
# type: (Candidate) -> bool
3334
return False
3435

36+
def get_candidate_lookup(self):
37+
# type: () -> CandidateLookup
38+
raise NotImplementedError("Subclass should override")
39+
3540

3641
class Candidate(object):
3742
@property

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from .base import Candidate, format_name
1818

1919
if MYPY_CHECK_RUNNING:
20-
from typing import Any, Iterable, Optional, Set, Tuple, Union
20+
from typing import Any, FrozenSet, Iterable, Optional, Tuple, Union
2121

2222
from pip._vendor.packaging.version import _BaseVersion
2323
from pip._vendor.pkg_resources import Distribution
@@ -132,6 +132,10 @@ def __repr__(self):
132132
link=str(self.link),
133133
)
134134

135+
def __hash__(self):
136+
# type: () -> int
137+
return hash((self.__class__, self.link))
138+
135139
def __eq__(self, other):
136140
# type: (Any) -> bool
137141
if isinstance(other, self.__class__):
@@ -313,6 +317,10 @@ def __repr__(self):
313317
distribution=self.dist,
314318
)
315319

320+
def __hash__(self):
321+
# type: () -> int
322+
return hash((self.__class__, self.name, self.version))
323+
316324
def __eq__(self, other):
317325
# type: (Any) -> bool
318326
if isinstance(other, self.__class__):
@@ -371,7 +379,7 @@ class ExtrasCandidate(Candidate):
371379
def __init__(
372380
self,
373381
base, # type: BaseCandidate
374-
extras, # type: Set[str]
382+
extras, # type: FrozenSet[str]
375383
):
376384
# type: (...) -> None
377385
self.base = base
@@ -385,6 +393,10 @@ def __repr__(self):
385393
extras=self.extras,
386394
)
387395

396+
def __hash__(self):
397+
# type: () -> int
398+
return hash((self.base, self.extras))
399+
388400
def __eq__(self, other):
389401
# type: (Any) -> bool
390402
if isinstance(other, self.__class__):

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
UnsupportedPythonVersion,
1010
)
1111
from pip._internal.utils.compatibility_tags import get_supported
12+
from pip._internal.utils.hashes import Hashes
1213
from pip._internal.utils.misc import (
1314
dist_in_site_packages,
1415
dist_in_usersite,
@@ -31,7 +32,17 @@
3132
)
3233

3334
if MYPY_CHECK_RUNNING:
34-
from typing import Dict, Iterable, Iterator, Optional, Set, Tuple, TypeVar
35+
from typing import (
36+
FrozenSet,
37+
Dict,
38+
Iterable,
39+
List,
40+
Optional,
41+
Sequence,
42+
Set,
43+
Tuple,
44+
TypeVar,
45+
)
3546

3647
from pip._vendor.packaging.specifiers import SpecifierSet
3748
from pip._vendor.packaging.version import _BaseVersion
@@ -71,7 +82,7 @@ def __init__(
7182
):
7283
# type: (...) -> None
7384

74-
self.finder = finder
85+
self._finder = finder
7586
self.preparer = preparer
7687
self._wheel_cache = wheel_cache
7788
self._python_candidate = RequiresPythonCandidate(py_version_info)
@@ -94,7 +105,7 @@ def __init__(
94105
def _make_candidate_from_dist(
95106
self,
96107
dist, # type: Distribution
97-
extras, # type: Set[str]
108+
extras, # type: FrozenSet[str]
98109
parent, # type: InstallRequirement
99110
):
100111
# type: (...) -> Candidate
@@ -106,7 +117,7 @@ def _make_candidate_from_dist(
106117
def _make_candidate_from_link(
107118
self,
108119
link, # type: Link
109-
extras, # type: Set[str]
120+
extras, # type: FrozenSet[str]
110121
parent, # type: InstallRequirement
111122
name, # type: Optional[str]
112123
version, # type: Optional[_BaseVersion]
@@ -130,9 +141,28 @@ def _make_candidate_from_link(
130141
return ExtrasCandidate(base, extras)
131142
return base
132143

133-
def iter_found_candidates(self, ireq, extras):
134-
# type: (InstallRequirement, Set[str]) -> Iterator[Candidate]
135-
name = canonicalize_name(ireq.req.name)
144+
def _iter_found_candidates(
145+
self,
146+
ireqs, # type: Sequence[InstallRequirement]
147+
specifier, # type: SpecifierSet
148+
):
149+
# type: (...) -> Iterable[Candidate]
150+
if not ireqs:
151+
return ()
152+
153+
# The InstallRequirement implementation requires us to give it a
154+
# "parent", which doesn't really fit with graph-based resolution.
155+
# Here we just choose the first requirement to represent all of them.
156+
# Hopefully the Project model can correct this mismatch in the future.
157+
parent = ireqs[0]
158+
name = canonicalize_name(parent.req.name)
159+
160+
hashes = Hashes()
161+
extras = frozenset() # type: FrozenSet[str]
162+
for ireq in ireqs:
163+
specifier &= ireq.req.specifier
164+
hashes |= ireq.hashes(trust_internet=False)
165+
extras |= frozenset(ireq.extras)
136166

137167
# We use this to ensure that we only yield a single candidate for
138168
# each version (the finder's preferred one for that version). The
@@ -148,43 +178,68 @@ def iter_found_candidates(self, ireq, extras):
148178
if not self._force_reinstall and name in self._installed_dists:
149179
installed_dist = self._installed_dists[name]
150180
installed_version = installed_dist.parsed_version
151-
if ireq.req.specifier.contains(
152-
installed_version,
153-
prereleases=True
154-
):
181+
if specifier.contains(installed_version, prereleases=True):
155182
candidate = self._make_candidate_from_dist(
156183
dist=installed_dist,
157184
extras=extras,
158-
parent=ireq,
185+
parent=parent,
159186
)
160187
candidates[installed_version] = candidate
161188

162-
found = self.finder.find_best_candidate(
163-
project_name=ireq.req.name,
164-
specifier=ireq.req.specifier,
165-
hashes=ireq.hashes(trust_internet=False),
189+
found = self._finder.find_best_candidate(
190+
project_name=name,
191+
specifier=specifier,
192+
hashes=hashes,
166193
)
167194
for ican in found.iter_applicable():
168195
if ican.version == installed_version:
169196
continue
170197
candidate = self._make_candidate_from_link(
171198
link=ican.link,
172199
extras=extras,
173-
parent=ireq,
200+
parent=parent,
174201
name=name,
175202
version=ican.version,
176203
)
177204
candidates[ican.version] = candidate
178205

179206
return six.itervalues(candidates)
180207

208+
def find_candidates(self, requirements, constraint):
209+
# type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate]
210+
explicit_candidates = set() # type: Set[Candidate]
211+
ireqs = [] # type: List[InstallRequirement]
212+
for req in requirements:
213+
cand, ireq = req.get_candidate_lookup()
214+
if cand is not None:
215+
explicit_candidates.add(cand)
216+
if ireq is not None:
217+
ireqs.append(ireq)
218+
219+
# If none of the requirements want an explicit candidate, we can ask
220+
# the finder for candidates.
221+
if not explicit_candidates:
222+
return self._iter_found_candidates(ireqs, constraint)
223+
224+
if constraint:
225+
name = explicit_candidates.pop().name
226+
raise InstallationError(
227+
"Could not satisfy constraints for {!r}: installation from "
228+
"path or url cannot be constrained to a version".format(name)
229+
)
230+
231+
return (
232+
c for c in explicit_candidates
233+
if all(req.is_satisfied_by(c) for req in requirements)
234+
)
235+
181236
def make_requirement_from_install_req(self, ireq):
182237
# type: (InstallRequirement) -> Requirement
183238
if not ireq.link:
184-
return SpecifierRequirement(ireq, factory=self)
239+
return SpecifierRequirement(ireq)
185240
cand = self._make_candidate_from_link(
186241
ireq.link,
187-
extras=set(ireq.extras),
242+
extras=frozenset(ireq.extras),
188243
parent=ireq,
189244
name=canonicalize_name(ireq.name) if ireq.name else None,
190245
version=None,

src/pip/_internal/resolution/resolvelib/provider.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
55

66
if MYPY_CHECK_RUNNING:
7-
from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union
7+
from typing import (
8+
Any,
9+
Dict,
10+
Iterable,
11+
Optional,
12+
Sequence,
13+
Set,
14+
Tuple,
15+
Union,
16+
)
817

918
from .base import Requirement, Candidate
1019
from .factory import Factory
@@ -45,7 +54,7 @@ def __init__(
4554
self.user_requested = user_requested
4655

4756
def _sort_matches(self, matches):
48-
# type: (Sequence[Candidate]) -> Sequence[Candidate]
57+
# type: (Iterable[Candidate]) -> Sequence[Candidate]
4958

5059
# The requirement is responsible for returning a sequence of potential
5160
# candidates, one per version. The provider handles the logic of
@@ -68,7 +77,6 @@ def _sort_matches(self, matches):
6877
# - The project was specified on the command line, or
6978
# - The project is a dependency and the "eager" upgrade strategy
7079
# was requested.
71-
7280
def _eligible_for_upgrade(name):
7381
# type: (str) -> bool
7482
"""Are upgrades allowed for this project?
@@ -121,11 +129,15 @@ def get_preference(
121129
# Use the "usual" value for now
122130
return len(candidates)
123131

124-
def find_matches(self, requirement):
125-
# type: (Requirement) -> Sequence[Candidate]
126-
constraint = self._constraints.get(requirement.name, SpecifierSet())
127-
matches = requirement.find_matches(constraint)
128-
return self._sort_matches(matches)
132+
def find_matches(self, requirements):
133+
# type: (Sequence[Requirement]) -> Iterable[Candidate]
134+
if not requirements:
135+
return []
136+
constraint = self._constraints.get(
137+
requirements[0].name, SpecifierSet(),
138+
)
139+
candidates = self._factory.find_candidates(requirements, constraint)
140+
return reversed(self._sort_matches(candidates))
129141

130142
def is_satisfied_by(self, requirement, candidate):
131143
# type: (Requirement, Candidate) -> bool

0 commit comments

Comments
 (0)