From b2389bf8c72dc3b646ba6fdcde513240c889e63e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 6 Jul 2019 20:47:17 -0700 Subject: [PATCH] Add CandidatePreferences and PackageFinder.make_candidate_evaluator(). --- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/index.py | 93 +++++++++++++++++++++++------- tests/unit/test_finder.py | 4 +- tests/unit/test_index.py | 86 +++++++++++++++++++++------ 4 files changed, 143 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 8a373ef08..801b4ea31 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -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 diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index e0e78678c..c7b2d0b0a 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -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, ) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 59c7b5ac7..54fb1897b 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -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 diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 2d32867bd..f9018324c 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -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): """