Add CandidatePreferences and PackageFinder.make_candidate_evaluator().

This commit is contained in:
Chris Jerdonek 2019-07-06 20:47:17 -07:00
parent 9311049de2
commit b2389bf8c7
4 changed files with 143 additions and 42 deletions

View File

@ -189,7 +189,7 @@ class ListCommand(Command):
all_candidates = [candidate for candidate in all_candidates all_candidates = [candidate for candidate in all_candidates
if not candidate.version.is_prerelease] if not candidate.version.is_prerelease]
evaluator = finder.candidate_evaluator evaluator = finder.make_candidate_evaluator()
best_candidate = evaluator.get_best_candidate(all_candidates) best_candidate = evaluator.get_best_candidate(all_candidates)
if best_candidate is None: if best_candidate is None:
continue continue

View File

@ -440,6 +440,26 @@ class LinkEvaluator(object):
return (True, version) 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): class CandidateEvaluator(object):
""" """
@ -447,28 +467,46 @@ class CandidateEvaluator(object):
on what tags are valid. on what tags are valid.
""" """
@classmethod
def create(
cls,
target_python=None, # type: Optional[TargetPython]
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__( def __init__(
self, self,
supported_tags=None, # type: Optional[List[Pep425Tag]] supported_tags, # type: List[Pep425Tag]
prefer_binary=False, # type: bool prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool allow_all_prereleases=False, # type: bool
): ):
# type: (...) -> None # type: (...) -> None
""" """
:param supported_tags: The PEP 425 tags supported by the target :param supported_tags: The PEP 425 tags supported by the target
Python in order of preference (most preferred first). If None, Python in order of preference (most preferred first).
then the list will be generated from the running Python.
:param allow_all_prereleases: Whether to allow all pre-releases.
""" """
if supported_tags is None: self._allow_all_prereleases = allow_all_prereleases
target_python = TargetPython()
supported_tags = target_python.get_tags()
self._prefer_binary = prefer_binary self._prefer_binary = prefer_binary
self._supported_tags = supported_tags self._supported_tags = supported_tags
self.allow_all_prereleases = allow_all_prereleases
def make_found_candidates( def make_found_candidates(
self, self,
candidates, # type: List[InstallationCandidate] candidates, # type: List[InstallationCandidate]
@ -486,7 +524,7 @@ class CandidateEvaluator(object):
specifier = specifiers.SpecifierSet() specifier = specifiers.SpecifierSet()
# Using None infers from the specifier instead. # Using None infers from the specifier instead.
allow_prereleases = self.allow_all_prereleases or None allow_prereleases = self._allow_all_prereleases or None
versions = { versions = {
str(v) for v in specifier.filter( str(v) for v in specifier.filter(
# We turn the version object into a str here because otherwise # We turn the version object into a str here because otherwise
@ -649,13 +687,13 @@ class PackageFinder(object):
def __init__( def __init__(
self, self,
candidate_evaluator, # type: CandidateEvaluator
search_scope, # type: SearchScope search_scope, # type: SearchScope
session, # type: PipSession session, # type: PipSession
target_python, # type: TargetPython target_python, # type: TargetPython
allow_yanked, # type: bool allow_yanked, # type: bool
format_control=None, # type: Optional[FormatControl] format_control=None, # type: Optional[FormatControl]
trusted_hosts=None, # type: Optional[List[str]] trusted_hosts=None, # type: Optional[List[str]]
candidate_prefs=None, # type: CandidatePreferences
ignore_requires_python=None, # type: Optional[bool] ignore_requires_python=None, # type: Optional[bool]
): ):
# type: (...) -> None # type: (...) -> None
@ -663,22 +701,25 @@ class PackageFinder(object):
This constructor is primarily meant to be used by the create() class This constructor is primarily meant to be used by the create() class
method and from tests. method and from tests.
:param candidate_evaluator: A CandidateEvaluator object.
:param session: The Session to use to make requests. :param session: The Session to use to make requests.
:param format_control: A FormatControl object, used to control :param format_control: A FormatControl object, used to control
the selection of source packages / binary packages when consulting the selection of source packages / binary packages when consulting
the index and links. the index and links.
:param candidate_prefs: Options to use when creating a
CandidateEvaluator object.
""" """
if trusted_hosts is None: if trusted_hosts is None:
trusted_hosts = [] trusted_hosts = []
if candidate_prefs is None:
candidate_prefs = CandidatePreferences()
format_control = format_control or FormatControl(set(), set()) format_control = format_control or FormatControl(set(), set())
self._allow_yanked = allow_yanked self._allow_yanked = allow_yanked
self._candidate_prefs = candidate_prefs
self._ignore_requires_python = ignore_requires_python self._ignore_requires_python = ignore_requires_python
self._target_python = target_python self._target_python = target_python
self.candidate_evaluator = candidate_evaluator
self.search_scope = search_scope self.search_scope = search_scope
self.session = session self.session = session
self.format_control = format_control self.format_control = format_control
@ -720,15 +761,13 @@ class PackageFinder(object):
if target_python is None: if target_python is None:
target_python = TargetPython() target_python = TargetPython()
supported_tags = target_python.get_tags() candidate_prefs = CandidatePreferences(
candidate_evaluator = CandidateEvaluator(
supported_tags=supported_tags,
prefer_binary=selection_prefs.prefer_binary, prefer_binary=selection_prefs.prefer_binary,
allow_all_prereleases=selection_prefs.allow_all_prereleases, allow_all_prereleases=selection_prefs.allow_all_prereleases,
) )
return cls( return cls(
candidate_evaluator=candidate_evaluator, candidate_prefs=candidate_prefs,
search_scope=search_scope, search_scope=search_scope,
session=session, session=session,
target_python=target_python, target_python=target_python,
@ -751,11 +790,11 @@ class PackageFinder(object):
@property @property
def allow_all_prereleases(self): def allow_all_prereleases(self):
# type: () -> bool # type: () -> bool
return self.candidate_evaluator.allow_all_prereleases return self._candidate_prefs.allow_all_prereleases
def set_allow_all_prereleases(self): def set_allow_all_prereleases(self):
# type: () -> None # type: () -> None
self.candidate_evaluator.allow_all_prereleases = True self._candidate_prefs.allow_all_prereleases = True
def add_trusted_host(self, host, source=None): def add_trusted_host(self, host, source=None):
# type: (str, Optional[str]) -> None # type: (str, Optional[str]) -> None
@ -995,6 +1034,17 @@ class PackageFinder(object):
# This is an intentional priority ordering # This is an intentional priority ordering
return file_versions + find_links_versions + page_versions 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( def find_candidates(
self, self,
project_name, # type: str project_name, # type: str
@ -1010,7 +1060,8 @@ class PackageFinder(object):
:return: A `FoundCandidates` instance. :return: A `FoundCandidates` instance.
""" """
candidates = self.find_all_candidates(project_name) 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, candidates, specifier=specifier,
) )

View File

@ -242,8 +242,8 @@ class TestWheel:
Link("simplewheel-1.0-py2.py3-none-any.whl"), Link("simplewheel-1.0-py2.py3-none-any.whl"),
), ),
] ]
finder = make_test_finder() candidate_evaluator = CandidateEvaluator.create()
sort_key = finder.candidate_evaluator._sort_key sort_key = candidate_evaluator._sort_key
results = sorted(links, key=sort_key, reverse=True) results = sorted(links, key=sort_key, reverse=True)
results2 = sorted(reversed(links), key=sort_key, reverse=True) results2 = sorted(reversed(links), key=sort_key, reverse=True)
assert links == results == results2, results2 assert links == results == results2, results2

View File

@ -8,8 +8,8 @@ from pip._vendor.packaging.specifiers import SpecifierSet
from pip._internal.download import PipSession from pip._internal.download import PipSession
from pip._internal.index import ( from pip._internal.index import (
CandidateEvaluator, FormatControl, HTMLPage, Link, LinkEvaluator, CandidateEvaluator, CandidatePreferences, FormatControl, HTMLPage, Link,
PackageFinder, _check_link_requires_python, _clean_link, LinkEvaluator, PackageFinder, _check_link_requires_python, _clean_link,
_determine_base_url, _extract_version_from_fragment, _determine_base_url, _extract_version_from_fragment,
_find_name_version_sep, _get_html_page, _find_name_version_sep, _get_html_page,
) )
@ -17,6 +17,7 @@ from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.search_scope import SearchScope from pip._internal.models.search_scope import SearchScope
from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython from pip._internal.models.target_python import TargetPython
from pip._internal.pep425tags import get_supported
from tests.lib import CURRENT_PY_VERSION_INFO, make_test_finder from tests.lib import CURRENT_PY_VERSION_INFO, make_test_finder
@ -171,6 +172,32 @@ class TestLinkEvaluator:
class TestCandidateEvaluator: class TestCandidateEvaluator:
@pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
(False, False),
(False, True),
(True, False),
(True, True),
])
def test_create(self, allow_all_prereleases, prefer_binary):
target_python = TargetPython()
target_python._valid_tags = [('py36', 'none', 'any')]
evaluator = CandidateEvaluator.create(
target_python=target_python,
allow_all_prereleases=allow_all_prereleases,
prefer_binary=prefer_binary,
)
assert evaluator._allow_all_prereleases == allow_all_prereleases
assert evaluator._prefer_binary == prefer_binary
assert evaluator._supported_tags == [('py36', 'none', 'any')]
def test_create__target_python_none(self):
"""
Test passing target_python=None.
"""
evaluator = CandidateEvaluator.create()
expected_tags = get_supported()
assert evaluator._supported_tags == expected_tags
def make_mock_candidate(self, version, yanked_reason=None): def make_mock_candidate(self, version, yanked_reason=None):
url = 'https://example.com/pkg-{}.tar.gz'.format(version) url = 'https://example.com/pkg-{}.tar.gz'.format(version)
link = Link(url, yanked_reason=yanked_reason) link = Link(url, yanked_reason=yanked_reason)
@ -184,7 +211,7 @@ class TestCandidateEvaluator:
candidates = [ candidates = [
self.make_mock_candidate(version) for version in versions self.make_mock_candidate(version) for version in versions
] ]
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
found_candidates = evaluator.make_found_candidates( found_candidates = evaluator.make_found_candidates(
candidates, specifier=specifier, candidates, specifier=specifier,
) )
@ -212,7 +239,7 @@ class TestCandidateEvaluator:
link = Link(url, yanked_reason=yanked_reason) link = Link(url, yanked_reason=yanked_reason)
candidate = InstallationCandidate('mypackage', '1.0', link) candidate = InstallationCandidate('mypackage', '1.0', link)
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
sort_value = evaluator._sort_key(candidate) sort_value = evaluator._sort_key(candidate)
# Yanked / non-yanked is reflected in the first element of the tuple. # Yanked / non-yanked is reflected in the first element of the tuple.
actual = sort_value[0] actual = sort_value[0]
@ -222,7 +249,7 @@ class TestCandidateEvaluator:
""" """
Test passing an empty list. Test passing an empty list.
""" """
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
actual = evaluator.get_best_candidate([]) actual = evaluator.get_best_candidate([])
assert actual is None assert actual is None
@ -237,7 +264,7 @@ class TestCandidateEvaluator:
self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'), self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
] ]
expected_best = candidates[1] expected_best = candidates[1]
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
actual = evaluator.get_best_candidate(candidates) actual = evaluator.get_best_candidate(candidates)
assert actual is expected_best assert actual is expected_best
assert str(actual.version) == '3.0' assert str(actual.version) == '3.0'
@ -268,7 +295,7 @@ class TestCandidateEvaluator:
candidates = [ candidates = [
self.make_mock_candidate('1.0', yanked_reason=yanked_reason), self.make_mock_candidate('1.0', yanked_reason=yanked_reason),
] ]
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
actual = evaluator.get_best_candidate(candidates) actual = evaluator.get_best_candidate(candidates)
assert str(actual.version) == '1.0' assert str(actual.version) == '1.0'
@ -295,7 +322,7 @@ class TestCandidateEvaluator:
self.make_mock_candidate('1.0'), self.make_mock_candidate('1.0'),
] ]
expected_best = candidates[1] expected_best = candidates[1]
evaluator = CandidateEvaluator() evaluator = CandidateEvaluator.create()
actual = evaluator.get_best_candidate(candidates) actual = evaluator.get_best_candidate(candidates)
assert actual is expected_best assert actual is expected_best
assert str(actual.version) == '2.0' assert str(actual.version) == '2.0'
@ -312,29 +339,25 @@ class TestPackageFinder:
(True, False), (True, False),
(True, True), (True, True),
]) ])
def test_create__candidate_evaluator( def test_create__candidate_prefs(
self, allow_all_prereleases, prefer_binary, self, allow_all_prereleases, prefer_binary,
): ):
""" """
Test that the candidate_evaluator attribute is set correctly. Test that the _candidate_prefs attribute is set correctly.
""" """
selection_prefs = SelectionPreferences( selection_prefs = SelectionPreferences(
allow_yanked=True, allow_yanked=True,
allow_all_prereleases=allow_all_prereleases, allow_all_prereleases=allow_all_prereleases,
prefer_binary=prefer_binary, prefer_binary=prefer_binary,
) )
target_python = TargetPython(py_version_info=(3, 7, 3))
target_python._valid_tags = ['tag1', 'tag2']
finder = PackageFinder.create( finder = PackageFinder.create(
search_scope=SearchScope([], []), search_scope=SearchScope([], []),
selection_prefs=selection_prefs, selection_prefs=selection_prefs,
session=PipSession(), session=PipSession(),
target_python=target_python,
) )
evaluator = finder.candidate_evaluator candidate_prefs = finder._candidate_prefs
assert evaluator.allow_all_prereleases == allow_all_prereleases assert candidate_prefs.allow_all_prereleases == allow_all_prereleases
assert evaluator._prefer_binary == prefer_binary assert candidate_prefs.prefer_binary == prefer_binary
assert evaluator._supported_tags == ['tag1', 'tag2']
def test_create__target_python(self): def test_create__target_python(self):
""" """
@ -526,7 +549,6 @@ class TestPackageFinder:
target_python = TargetPython(py_version_info=(3, 7)) target_python = TargetPython(py_version_info=(3, 7))
format_control = FormatControl(set(), only_binary) format_control = FormatControl(set(), only_binary)
finder = PackageFinder( finder = PackageFinder(
candidate_evaluator=CandidateEvaluator(),
search_scope=SearchScope([], []), search_scope=SearchScope([], []),
session=PipSession(), session=PipSession(),
target_python=target_python, target_python=target_python,
@ -552,6 +574,34 @@ class TestPackageFinder:
assert actual_target_python._given_py_version_info == (3, 7) assert actual_target_python._given_py_version_info == (3, 7)
assert actual_target_python.py_version_info == (3, 7, 0) assert actual_target_python.py_version_info == (3, 7, 0)
@pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [
(False, False),
(False, True),
(True, False),
(True, True),
])
def test_make_candidate_evaluator(
self, allow_all_prereleases, prefer_binary,
):
target_python = TargetPython()
target_python._valid_tags = [('py36', 'none', 'any')]
candidate_prefs = CandidatePreferences(
prefer_binary=prefer_binary,
allow_all_prereleases=allow_all_prereleases,
)
finder = PackageFinder(
search_scope=SearchScope([], []),
session=PipSession(),
target_python=target_python,
allow_yanked=True,
candidate_prefs=candidate_prefs,
)
evaluator = finder.make_candidate_evaluator()
assert evaluator._allow_all_prereleases == allow_all_prereleases
assert evaluator._prefer_binary == prefer_binary
assert evaluator._supported_tags == [('py36', 'none', 'any')]
def test_sort_locations_file_expand_dir(data): def test_sort_locations_file_expand_dir(data):
""" """