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
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

View File

@ -440,6 +440,26 @@ class LinkEvaluator(object):
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):
"""
@ -447,28 +467,46 @@ class CandidateEvaluator(object):
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__(
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(
self,
candidates, # type: List[InstallationCandidate]
@ -486,7 +524,7 @@ class CandidateEvaluator(object):
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
@ -649,13 +687,13 @@ 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
@ -663,22 +701,25 @@ class PackageFinder(object):
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
@ -720,15 +761,13 @@ class PackageFinder(object):
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,
@ -751,11 +790,11 @@ class PackageFinder(object):
@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
@ -995,6 +1034,17 @@ class PackageFinder(object):
# 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
@ -1010,7 +1060,8 @@ class PackageFinder(object):
: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,
)

View File

@ -242,8 +242,8 @@ class TestWheel:
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

View File

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