Skip to content

Add PackageFinder.make_candidate_evaluator() #6687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pip/_internal/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def iter_packages_latest_infos(self, packages, options):
all_candidates = [candidate for candidate in all_candidates
if not candidate.version.is_prerelease]

evaluator = finder.candidate_evaluator
evaluator = finder.make_candidate_evaluator()
best_candidate = evaluator.get_best_candidate(all_candidates)
if best_candidate is None:
continue
Expand Down
132 changes: 99 additions & 33 deletions src/pip/_internal/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,53 +440,84 @@ def evaluate_link(self, link):
return (True, version)


class CandidatePreferences(object):

"""
Encapsulates some of the preferences for filtering and sorting
InstallationCandidate objects.
"""

def __init__(
self,
prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool
):
# type: (...) -> None
"""
:param allow_all_prereleases: Whether to allow all pre-releases.
"""
self.allow_all_prereleases = allow_all_prereleases
self.prefer_binary = prefer_binary


class CandidateEvaluator(object):

"""
Responsible for filtering and sorting candidates for installation based
on what tags are valid.
"""

@classmethod
def create(
cls,
target_python=None, # type: Optional[TargetPython]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what is the style convention for the offset of these type declaration comments? In some places it is aligned with the others in a declaration, in others there are chunks of aligned comments, and in others they are all just 2 spaces from their corresponding parameters.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know of one. I've always been accustomed to two spaces for inline comments, but some people started aligning them. I think it's overboard to always align (e.g. having to change every argument line just because of a change in one line), so I sometimes align to adjacent lines when convenient and it improves the readability / how it looks.

prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool
):
# type: (...) -> CandidateEvaluator
"""Create a CandidateEvaluator object.

:param target_python: The target Python interpreter to use when
checking compatibility. If None (the default), a TargetPython
object will be constructed from the running Python.
"""
if target_python is None:
target_python = TargetPython()

supported_tags = target_python.get_tags()

return cls(
supported_tags=supported_tags,
prefer_binary=prefer_binary,
allow_all_prereleases=allow_all_prereleases,
)

def __init__(
self,
supported_tags=None, # type: Optional[List[Pep425Tag]]
supported_tags, # type: List[Pep425Tag]
prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool
):
# type: (...) -> None
"""
:param supported_tags: The PEP 425 tags supported by the target
Python in order of preference (most preferred first). If None,
then the list will be generated from the running Python.
:param allow_all_prereleases: Whether to allow all pre-releases.
Python in order of preference (most preferred first).
"""
if supported_tags is None:
target_python = TargetPython()
supported_tags = target_python.get_tags()

self._allow_all_prereleases = allow_all_prereleases
self._prefer_binary = prefer_binary
self._supported_tags = supported_tags

self.allow_all_prereleases = allow_all_prereleases

def make_found_candidates(
def get_applicable_candidates(
self,
candidates, # type: List[InstallationCandidate]
specifier=None, # type: Optional[specifiers.BaseSpecifier]
candidates, # type: List[InstallationCandidate]
specifier, # type: specifiers.BaseSpecifier
):
# type: (...) -> FoundCandidates
# type: (...) -> List[InstallationCandidate]
"""
Create and return a `FoundCandidates` instance.

:param specifier: An optional object implementing `filter`
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
versions.
Return the applicable candidates from a list of candidates.
"""
if specifier is None:
specifier = specifiers.SpecifierSet()

# Using None infers from the specifier instead.
allow_prereleases = self.allow_all_prereleases or None
allow_prereleases = self._allow_all_prereleases or None
versions = {
str(v) for v in specifier.filter(
# We turn the version object into a str here because otherwise
Expand All @@ -505,6 +536,28 @@ def make_found_candidates(
applicable_candidates = [
c for c in candidates if str(c.version) in versions
]
return applicable_candidates

def make_found_candidates(
self,
candidates, # type: List[InstallationCandidate]
specifier=None, # type: Optional[specifiers.BaseSpecifier]
):
# type: (...) -> FoundCandidates
"""
Create and return a `FoundCandidates` instance.

:param specifier: An optional object implementing `filter`
(e.g. `packaging.specifiers.SpecifierSet`) to filter applicable
versions.
"""
if specifier is None:
specifier = specifiers.SpecifierSet()

applicable_candidates = self.get_applicable_candidates(
candidates=candidates,
specifier=specifier,
)

return FoundCandidates(
candidates,
Expand Down Expand Up @@ -649,36 +702,39 @@ class PackageFinder(object):

def __init__(
self,
candidate_evaluator, # type: CandidateEvaluator
search_scope, # type: SearchScope
session, # type: PipSession
target_python, # type: TargetPython
allow_yanked, # type: bool
format_control=None, # type: Optional[FormatControl]
trusted_hosts=None, # type: Optional[List[str]]
candidate_prefs=None, # type: CandidatePreferences
ignore_requires_python=None, # type: Optional[bool]
):
# type: (...) -> None
"""
This constructor is primarily meant to be used by the create() class
method and from tests.

:param candidate_evaluator: A CandidateEvaluator object.
:param session: The Session to use to make requests.
:param format_control: A FormatControl object, used to control
the selection of source packages / binary packages when consulting
the index and links.
:param candidate_prefs: Options to use when creating a
CandidateEvaluator object.
"""
if trusted_hosts is None:
trusted_hosts = []
if candidate_prefs is None:
candidate_prefs = CandidatePreferences()

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

self._allow_yanked = allow_yanked
self._candidate_prefs = candidate_prefs
self._ignore_requires_python = ignore_requires_python
self._target_python = target_python

self.candidate_evaluator = candidate_evaluator
self.search_scope = search_scope
self.session = session
self.format_control = format_control
Expand Down Expand Up @@ -720,15 +776,13 @@ def create(
if target_python is None:
target_python = TargetPython()

supported_tags = target_python.get_tags()
candidate_evaluator = CandidateEvaluator(
supported_tags=supported_tags,
candidate_prefs = CandidatePreferences(
prefer_binary=selection_prefs.prefer_binary,
allow_all_prereleases=selection_prefs.allow_all_prereleases,
)

return cls(
candidate_evaluator=candidate_evaluator,
candidate_prefs=candidate_prefs,
search_scope=search_scope,
session=session,
target_python=target_python,
Expand All @@ -751,11 +805,11 @@ def index_urls(self):
@property
def allow_all_prereleases(self):
# type: () -> bool
return self.candidate_evaluator.allow_all_prereleases
return self._candidate_prefs.allow_all_prereleases

def set_allow_all_prereleases(self):
# type: () -> None
self.candidate_evaluator.allow_all_prereleases = True
self._candidate_prefs.allow_all_prereleases = True

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

def make_candidate_evaluator(self):
# type: (...) -> CandidateEvaluator
"""Create a CandidateEvaluator object to use.
"""
candidate_prefs = self._candidate_prefs
return CandidateEvaluator.create(
target_python=self._target_python,
prefer_binary=candidate_prefs.prefer_binary,
allow_all_prereleases=candidate_prefs.allow_all_prereleases,
)

def find_candidates(
self,
project_name, # type: str
Expand All @@ -1010,7 +1075,8 @@ def find_candidates(
:return: A `FoundCandidates` instance.
"""
candidates = self.find_all_candidates(project_name)
return self.candidate_evaluator.make_found_candidates(
candidate_evaluator = self.make_candidate_evaluator()
return candidate_evaluator.make_found_candidates(
candidates, specifier=specifier,
)

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ def test_link_sorting_wheels_with_build_tags(self):
Link("simplewheel-1.0-py2.py3-none-any.whl"),
),
]
finder = make_test_finder()
sort_key = finder.candidate_evaluator._sort_key
candidate_evaluator = CandidateEvaluator.create()
sort_key = candidate_evaluator._sort_key
results = sorted(links, key=sort_key, reverse=True)
results2 = sorted(reversed(links), key=sort_key, reverse=True)
assert links == results == results2, results2
Expand Down
Loading