Skip to content

Commit dddd28b

Browse files
authored
Add CandidateEvaluator class to encapsulate sorting. (#6424)
1 parent 7130b1d commit dddd28b

File tree

3 files changed

+96
-66
lines changed

3 files changed

+96
-66
lines changed

src/pip/_internal/commands/list.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,11 @@ def iter_packages_latest_infos(self, packages, options):
182182
all_candidates = [candidate for candidate in all_candidates
183183
if not candidate.version.is_prerelease]
184184

185-
if not all_candidates:
185+
evaluator = finder.candidate_evaluator
186+
best_candidate = evaluator.get_best_candidate(all_candidates)
187+
if best_candidate is None:
186188
continue
187-
best_candidate = max(all_candidates,
188-
key=finder._candidate_sort_key)
189+
189190
remote_version = best_candidate.version
190191
if best_candidate.location.is_wheel:
191192
typ = 'wheel'

src/pip/_internal/index.py

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
)
4949
from pip._vendor.packaging.version import _BaseVersion
5050
from pip._vendor.requests import Response
51+
from pip._internal.pep425tags import Pep425Tag
5152
from pip._internal.req import InstallRequirement
5253
from pip._internal.download import PipSession
5354

@@ -255,6 +256,71 @@ def _get_html_page(link, session=None):
255256
return None
256257

257258

259+
class CandidateEvaluator(object):
260+
261+
def __init__(
262+
self,
263+
valid_tags, # type: List[Pep425Tag]
264+
prefer_binary=False # type: bool
265+
266+
):
267+
# type: (...) -> None
268+
self._prefer_binary = prefer_binary
269+
self._valid_tags = valid_tags
270+
271+
def is_wheel_supported(self, wheel):
272+
# type: (Wheel) -> bool
273+
return wheel.supported(self._valid_tags)
274+
275+
def _sort_key(self, candidate):
276+
# type: (InstallationCandidate) -> CandidateSortingKey
277+
"""
278+
Function used to generate link sort key for link tuples.
279+
The greater the return value, the more preferred it is.
280+
If not finding wheels, then sorted by version only.
281+
If finding wheels, then the sort order is by version, then:
282+
1. existing installs
283+
2. wheels ordered via Wheel.support_index_min(self._valid_tags)
284+
3. source archives
285+
If prefer_binary was set, then all wheels are sorted above sources.
286+
Note: it was considered to embed this logic into the Link
287+
comparison operators, but then different sdist links
288+
with the same version, would have to be considered equal
289+
"""
290+
support_num = len(self._valid_tags)
291+
build_tag = tuple() # type: BuildTag
292+
binary_preference = 0
293+
if candidate.location.is_wheel:
294+
# can raise InvalidWheelFilename
295+
wheel = Wheel(candidate.location.filename)
296+
if not wheel.supported(self._valid_tags):
297+
raise UnsupportedWheel(
298+
"%s is not a supported wheel for this platform. It "
299+
"can't be sorted." % wheel.filename
300+
)
301+
if self._prefer_binary:
302+
binary_preference = 1
303+
pri = -(wheel.support_index_min(self._valid_tags))
304+
if wheel.build_tag is not None:
305+
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
306+
build_tag_groups = match.groups()
307+
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
308+
else: # sdist
309+
pri = -(support_num)
310+
return (binary_preference, candidate.version, build_tag, pri)
311+
312+
def get_best_candidate(self, candidates):
313+
# type: (List[InstallationCandidate]) -> InstallationCandidate
314+
"""
315+
Return the best candidate per the instance's sort order, or None if
316+
no candidates are given.
317+
"""
318+
if not candidates:
319+
return None
320+
321+
return max(candidates, key=self._sort_key)
322+
323+
258324
class FoundCandidates(object):
259325
"""A collection of candidates, returned by `PackageFinder.find_candidates`.
260326
@@ -267,18 +333,18 @@ class FoundCandidates(object):
267333
* `specifier`: Specifier to filter applicable versions.
268334
* `prereleases`: Whether prereleases should be accounted. Pass None to
269335
infer from the specifier.
270-
* `sort_key`: A callable used as the key function when choosing the best
271-
candidate.
336+
* `evaluator`: A CandidateEvaluator object to sort applicable candidates
337+
by order of preference.
272338
"""
273339
def __init__(
274340
self,
275341
candidates, # type: List[InstallationCandidate]
276342
versions, # type: Set[str]
277-
sort_key, # type: Callable[[InstallationCandidate], Any]
343+
evaluator, # type: CandidateEvaluator
278344
):
279345
# type: (...) -> None
280346
self._candidates = candidates
281-
self._sort_key = sort_key
347+
self._evaluator = evaluator
282348
self._versions = versions
283349

284350
@classmethod
@@ -287,7 +353,7 @@ def from_specifier(
287353
candidates, # type: List[InstallationCandidate]
288354
specifier, # type: specifiers.BaseSpecifier
289355
prereleases, # type: Optional[bool]
290-
sort_key, # type: Callable[[InstallationCandidate], Any]
356+
evaluator, # type: CandidateEvaluator
291357
):
292358
# type: (...) -> FoundCandidates
293359
versions = {
@@ -303,7 +369,7 @@ def from_specifier(
303369
prereleases=prereleases,
304370
)
305371
}
306-
return cls(candidates, versions, sort_key)
372+
return cls(candidates, versions, evaluator)
307373

308374
def iter_all(self):
309375
# type: () -> Iterable[InstallationCandidate]
@@ -325,9 +391,7 @@ def get_best(self):
325391
candidates are found.
326392
"""
327393
candidates = list(self.iter_applicable())
328-
if not candidates:
329-
return None
330-
return max(candidates, key=self._sort_key)
394+
return self._evaluator.get_best_candidate(candidates)
331395

332396

333397
class PackageFinder(object):
@@ -368,6 +432,8 @@ def __init__(
368432
to pep425tags.py in the get_supported() method.
369433
:param implementation: A string or None. This is passed directly
370434
to pep425tags.py in the get_supported() method.
435+
:param prefer_binary: Whether to prefer an old, but valid, binary
436+
dist over a new source dist.
371437
"""
372438
if session is None:
373439
raise TypeError(
@@ -408,15 +474,15 @@ def __init__(
408474
self.session = session
409475

410476
# The valid tags to check potential found wheel candidates against
411-
self.valid_tags = get_supported(
477+
valid_tags = get_supported(
412478
versions=versions,
413479
platform=platform,
414480
abi=abi,
415481
impl=implementation,
416482
)
417-
418-
# Do we prefer old, but valid, binary dist over new source dist
419-
self.prefer_binary = prefer_binary
483+
self.candidate_evaluator = CandidateEvaluator(
484+
valid_tags=valid_tags, prefer_binary=prefer_binary,
485+
)
420486

421487
# If we don't have TLS enabled, then WARN if anyplace we're looking
422488
# relies on TLS.
@@ -503,43 +569,6 @@ def sort_path(path):
503569

504570
return files, urls
505571

506-
def _candidate_sort_key(self, candidate):
507-
# type: (InstallationCandidate) -> CandidateSortingKey
508-
"""
509-
Function used to generate link sort key for link tuples.
510-
The greater the return value, the more preferred it is.
511-
If not finding wheels, then sorted by version only.
512-
If finding wheels, then the sort order is by version, then:
513-
1. existing installs
514-
2. wheels ordered via Wheel.support_index_min(self.valid_tags)
515-
3. source archives
516-
If prefer_binary was set, then all wheels are sorted above sources.
517-
Note: it was considered to embed this logic into the Link
518-
comparison operators, but then different sdist links
519-
with the same version, would have to be considered equal
520-
"""
521-
support_num = len(self.valid_tags)
522-
build_tag = tuple() # type: BuildTag
523-
binary_preference = 0
524-
if candidate.location.is_wheel:
525-
# can raise InvalidWheelFilename
526-
wheel = Wheel(candidate.location.filename)
527-
if not wheel.supported(self.valid_tags):
528-
raise UnsupportedWheel(
529-
"%s is not a supported wheel for this platform. It "
530-
"can't be sorted." % wheel.filename
531-
)
532-
if self.prefer_binary:
533-
binary_preference = 1
534-
pri = -(wheel.support_index_min(self.valid_tags))
535-
if wheel.build_tag is not None:
536-
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
537-
build_tag_groups = match.groups()
538-
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
539-
else: # sdist
540-
pri = -(support_num)
541-
return (binary_preference, candidate.version, build_tag, pri)
542-
543572
def _validate_secure_origin(self, logger, location):
544573
# type: (Logger, Link) -> bool
545574
# Determine if this url used a secure transport mechanism
@@ -722,7 +751,7 @@ def find_candidates(
722751
self.find_all_candidates(project_name),
723752
specifier=specifier,
724753
prereleases=(self.allow_all_prereleases or None),
725-
sort_key=self._candidate_sort_key,
754+
evaluator=self.candidate_evaluator,
726755
)
727756

728757
def find_requirement(self, req, upgrade):
@@ -893,7 +922,7 @@ def _link_package_versions(self, link, search):
893922
link, 'wrong project name (not %s)' % search.supplied)
894923
return None
895924

896-
if not wheel.supported(self.valid_tags):
925+
if not self.candidate_evaluator.is_wheel_supported(wheel):
897926
self._log_skipped_link(
898927
link, 'it is not compatible with this Python')
899928
return None

tests/unit/test_finder.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
BestVersionAlreadyInstalled, DistributionNotFound,
1313
)
1414
from pip._internal.index import (
15-
InstallationCandidate, Link, PackageFinder, Search,
15+
CandidateEvaluator, InstallationCandidate, Link, PackageFinder, Search,
1616
)
1717
from pip._internal.req.constructors import install_req_from_line
1818

@@ -154,7 +154,8 @@ def test_not_find_wheel_not_supported(self, data, monkeypatch):
154154
[],
155155
session=PipSession(),
156156
)
157-
finder.valid_tags = pip._internal.pep425tags.get_supported()
157+
valid_tags = pip._internal.pep425tags.get_supported()
158+
finder.candidate_evaluator = CandidateEvaluator(valid_tags=valid_tags)
158159

159160
with pytest.raises(DistributionNotFound):
160161
finder.find_requirement(req, True)
@@ -243,16 +244,15 @@ def test_link_sorting(self):
243244
Link('simple-1.0.tar.gz'),
244245
),
245246
]
246-
finder = PackageFinder([], [], session=PipSession())
247-
finder.valid_tags = [
247+
valid_tags = [
248248
('pyT', 'none', 'TEST'),
249249
('pyT', 'TEST', 'any'),
250250
('pyT', 'none', 'any'),
251251
]
252-
results = sorted(links,
253-
key=finder._candidate_sort_key, reverse=True)
254-
results2 = sorted(reversed(links),
255-
key=finder._candidate_sort_key, reverse=True)
252+
evaluator = CandidateEvaluator(valid_tags=valid_tags)
253+
sort_key = evaluator._sort_key
254+
results = sorted(links, key=sort_key, reverse=True)
255+
results2 = sorted(reversed(links), key=sort_key, reverse=True)
256256

257257
assert links == results == results2, results2
258258

@@ -276,9 +276,9 @@ def test_link_sorting_wheels_with_build_tags(self):
276276
),
277277
]
278278
finder = PackageFinder([], [], session=PipSession())
279-
results = sorted(links, key=finder._candidate_sort_key, reverse=True)
280-
results2 = sorted(reversed(links), key=finder._candidate_sort_key,
281-
reverse=True)
279+
sort_key = finder.candidate_evaluator._sort_key
280+
results = sorted(links, key=sort_key, reverse=True)
281+
results2 = sorted(reversed(links), key=sort_key, reverse=True)
282282
assert links == results == results2, results2
283283

284284

0 commit comments

Comments
 (0)