Skip to content

Commit a9bf041

Browse files
authored
Merge pull request #6687 from cjerdonek/add-candidate-preferences
Add PackageFinder.make_candidate_evaluator()
2 parents ddfa401 + 4a9e306 commit a9bf041

File tree

4 files changed

+187
-54
lines changed

4 files changed

+187
-54
lines changed

src/pip/_internal/commands/list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def iter_packages_latest_infos(self, packages, options):
189189
all_candidates = [candidate for candidate in all_candidates
190190
if not candidate.version.is_prerelease]
191191

192-
evaluator = finder.candidate_evaluator
192+
evaluator = finder.make_candidate_evaluator()
193193
best_candidate = evaluator.get_best_candidate(all_candidates)
194194
if best_candidate is None:
195195
continue

src/pip/_internal/index.py

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -440,53 +440,84 @@ def evaluate_link(self, link):
440440
return (True, version)
441441

442442

443+
class CandidatePreferences(object):
444+
445+
"""
446+
Encapsulates some of the preferences for filtering and sorting
447+
InstallationCandidate objects.
448+
"""
449+
450+
def __init__(
451+
self,
452+
prefer_binary=False, # type: bool
453+
allow_all_prereleases=False, # type: bool
454+
):
455+
# type: (...) -> None
456+
"""
457+
:param allow_all_prereleases: Whether to allow all pre-releases.
458+
"""
459+
self.allow_all_prereleases = allow_all_prereleases
460+
self.prefer_binary = prefer_binary
461+
462+
443463
class CandidateEvaluator(object):
444464

445465
"""
446466
Responsible for filtering and sorting candidates for installation based
447467
on what tags are valid.
448468
"""
449469

470+
@classmethod
471+
def create(
472+
cls,
473+
target_python=None, # type: Optional[TargetPython]
474+
prefer_binary=False, # type: bool
475+
allow_all_prereleases=False, # type: bool
476+
):
477+
# type: (...) -> CandidateEvaluator
478+
"""Create a CandidateEvaluator object.
479+
480+
:param target_python: The target Python interpreter to use when
481+
checking compatibility. If None (the default), a TargetPython
482+
object will be constructed from the running Python.
483+
"""
484+
if target_python is None:
485+
target_python = TargetPython()
486+
487+
supported_tags = target_python.get_tags()
488+
489+
return cls(
490+
supported_tags=supported_tags,
491+
prefer_binary=prefer_binary,
492+
allow_all_prereleases=allow_all_prereleases,
493+
)
494+
450495
def __init__(
451496
self,
452-
supported_tags=None, # type: Optional[List[Pep425Tag]]
497+
supported_tags, # type: List[Pep425Tag]
453498
prefer_binary=False, # type: bool
454499
allow_all_prereleases=False, # type: bool
455500
):
456501
# type: (...) -> None
457502
"""
458503
:param supported_tags: The PEP 425 tags supported by the target
459-
Python in order of preference (most preferred first). If None,
460-
then the list will be generated from the running Python.
461-
:param allow_all_prereleases: Whether to allow all pre-releases.
504+
Python in order of preference (most preferred first).
462505
"""
463-
if supported_tags is None:
464-
target_python = TargetPython()
465-
supported_tags = target_python.get_tags()
466-
506+
self._allow_all_prereleases = allow_all_prereleases
467507
self._prefer_binary = prefer_binary
468508
self._supported_tags = supported_tags
469509

470-
self.allow_all_prereleases = allow_all_prereleases
471-
472-
def make_found_candidates(
510+
def get_applicable_candidates(
473511
self,
474-
candidates, # type: List[InstallationCandidate]
475-
specifier=None, # type: Optional[specifiers.BaseSpecifier]
512+
candidates, # type: List[InstallationCandidate]
513+
specifier, # type: specifiers.BaseSpecifier
476514
):
477-
# type: (...) -> FoundCandidates
515+
# type: (...) -> List[InstallationCandidate]
478516
"""
479-
Create and return a `FoundCandidates` instance.
480-
481-
:param specifier: An optional object implementing `filter`
482-
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
483-
versions.
517+
Return the applicable candidates from a list of candidates.
484518
"""
485-
if specifier is None:
486-
specifier = specifiers.SpecifierSet()
487-
488519
# Using None infers from the specifier instead.
489-
allow_prereleases = self.allow_all_prereleases or None
520+
allow_prereleases = self._allow_all_prereleases or None
490521
versions = {
491522
str(v) for v in specifier.filter(
492523
# We turn the version object into a str here because otherwise
@@ -505,6 +536,28 @@ def make_found_candidates(
505536
applicable_candidates = [
506537
c for c in candidates if str(c.version) in versions
507538
]
539+
return applicable_candidates
540+
541+
def make_found_candidates(
542+
self,
543+
candidates, # type: List[InstallationCandidate]
544+
specifier=None, # type: Optional[specifiers.BaseSpecifier]
545+
):
546+
# type: (...) -> FoundCandidates
547+
"""
548+
Create and return a `FoundCandidates` instance.
549+
550+
:param specifier: An optional object implementing `filter`
551+
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
552+
versions.
553+
"""
554+
if specifier is None:
555+
specifier = specifiers.SpecifierSet()
556+
557+
applicable_candidates = self.get_applicable_candidates(
558+
candidates=candidates,
559+
specifier=specifier,
560+
)
508561

509562
return FoundCandidates(
510563
candidates,
@@ -649,36 +702,39 @@ class PackageFinder(object):
649702

650703
def __init__(
651704
self,
652-
candidate_evaluator, # type: CandidateEvaluator
653705
search_scope, # type: SearchScope
654706
session, # type: PipSession
655707
target_python, # type: TargetPython
656708
allow_yanked, # type: bool
657709
format_control=None, # type: Optional[FormatControl]
658710
trusted_hosts=None, # type: Optional[List[str]]
711+
candidate_prefs=None, # type: CandidatePreferences
659712
ignore_requires_python=None, # type: Optional[bool]
660713
):
661714
# type: (...) -> None
662715
"""
663716
This constructor is primarily meant to be used by the create() class
664717
method and from tests.
665718
666-
:param candidate_evaluator: A CandidateEvaluator object.
667719
:param session: The Session to use to make requests.
668720
:param format_control: A FormatControl object, used to control
669721
the selection of source packages / binary packages when consulting
670722
the index and links.
723+
:param candidate_prefs: Options to use when creating a
724+
CandidateEvaluator object.
671725
"""
672726
if trusted_hosts is None:
673727
trusted_hosts = []
728+
if candidate_prefs is None:
729+
candidate_prefs = CandidatePreferences()
674730

675731
format_control = format_control or FormatControl(set(), set())
676732

677733
self._allow_yanked = allow_yanked
734+
self._candidate_prefs = candidate_prefs
678735
self._ignore_requires_python = ignore_requires_python
679736
self._target_python = target_python
680737

681-
self.candidate_evaluator = candidate_evaluator
682738
self.search_scope = search_scope
683739
self.session = session
684740
self.format_control = format_control
@@ -720,15 +776,13 @@ def create(
720776
if target_python is None:
721777
target_python = TargetPython()
722778

723-
supported_tags = target_python.get_tags()
724-
candidate_evaluator = CandidateEvaluator(
725-
supported_tags=supported_tags,
779+
candidate_prefs = CandidatePreferences(
726780
prefer_binary=selection_prefs.prefer_binary,
727781
allow_all_prereleases=selection_prefs.allow_all_prereleases,
728782
)
729783

730784
return cls(
731-
candidate_evaluator=candidate_evaluator,
785+
candidate_prefs=candidate_prefs,
732786
search_scope=search_scope,
733787
session=session,
734788
target_python=target_python,
@@ -751,11 +805,11 @@ def index_urls(self):
751805
@property
752806
def allow_all_prereleases(self):
753807
# type: () -> bool
754-
return self.candidate_evaluator.allow_all_prereleases
808+
return self._candidate_prefs.allow_all_prereleases
755809

756810
def set_allow_all_prereleases(self):
757811
# type: () -> None
758-
self.candidate_evaluator.allow_all_prereleases = True
812+
self._candidate_prefs.allow_all_prereleases = True
759813

760814
def add_trusted_host(self, host, source=None):
761815
# type: (str, Optional[str]) -> None
@@ -995,6 +1049,17 @@ def find_all_candidates(self, project_name):
9951049
# This is an intentional priority ordering
9961050
return file_versions + find_links_versions + page_versions
9971051

1052+
def make_candidate_evaluator(self):
1053+
# type: (...) -> CandidateEvaluator
1054+
"""Create a CandidateEvaluator object to use.
1055+
"""
1056+
candidate_prefs = self._candidate_prefs
1057+
return CandidateEvaluator.create(
1058+
target_python=self._target_python,
1059+
prefer_binary=candidate_prefs.prefer_binary,
1060+
allow_all_prereleases=candidate_prefs.allow_all_prereleases,
1061+
)
1062+
9981063
def find_candidates(
9991064
self,
10001065
project_name, # type: str
@@ -1010,7 +1075,8 @@ def find_candidates(
10101075
:return: A `FoundCandidates` instance.
10111076
"""
10121077
candidates = self.find_all_candidates(project_name)
1013-
return self.candidate_evaluator.make_found_candidates(
1078+
candidate_evaluator = self.make_candidate_evaluator()
1079+
return candidate_evaluator.make_found_candidates(
10141080
candidates, specifier=specifier,
10151081
)
10161082

tests/unit/test_finder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ def test_link_sorting_wheels_with_build_tags(self):
242242
Link("simplewheel-1.0-py2.py3-none-any.whl"),
243243
),
244244
]
245-
finder = make_test_finder()
246-
sort_key = finder.candidate_evaluator._sort_key
245+
candidate_evaluator = CandidateEvaluator.create()
246+
sort_key = candidate_evaluator._sort_key
247247
results = sorted(links, key=sort_key, reverse=True)
248248
results2 = sorted(reversed(links), key=sort_key, reverse=True)
249249
assert links == results == results2, results2

0 commit comments

Comments
 (0)