diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 247de0ce4..6fad83e10 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -7,7 +7,6 @@ import logging import mimetypes import os import re -from collections import namedtuple from pip._vendor import html5lib, requests, six from pip._vendor.distlib.compat import unescape @@ -41,8 +40,8 @@ from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: from logging import Logger from typing import ( - Any, Callable, Iterable, Iterator, List, MutableMapping, Optional, - Sequence, Set, Text, Tuple, Union, + Any, Callable, FrozenSet, Iterable, Iterator, List, MutableMapping, + Optional, Sequence, Set, Text, Tuple, Union, ) import xml.etree.ElementTree from pip._vendor.packaging.version import _BaseVersion @@ -50,6 +49,7 @@ if MYPY_CHECK_RUNNING: from pip._internal.models.search_scope import SearchScope from pip._internal.req import InstallRequirement from pip._internal.download import PipSession + from pip._internal.pep425tags import Pep425Tag BuildTag = Tuple[Any, ...] # either empty tuple or Tuple[int, str] CandidateSortingKey = ( @@ -301,62 +301,58 @@ def _check_link_requires_python( return True -class CandidateEvaluator(object): +class LinkEvaluator(object): """ - Responsible for filtering and sorting candidates for installation based - on what tags are valid. + Responsible for evaluating links for a particular project. """ + _py_version_re = re.compile(r'-py([123]\.?[0-9]?)$') + # Don't include an allow_yanked default value to make sure each call # site considers whether yanked releases are allowed. This also causes # that decision to be made explicit in the calling code, which helps # people when reading the code. def __init__( self, - allow_yanked, # type: bool - target_python=None, # type: Optional[TargetPython] - prefer_binary=False, # type: bool - allow_all_prereleases=False, # type: bool + project_name, # type: str + canonical_name, # type: str + formats, # type: FrozenSet + target_python, # type: TargetPython + allow_yanked, # type: bool ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> None """ + :param project_name: The user supplied package name. + :param canonical_name: The canonical package name. + :param formats: The formats allowed for this package. Should be a set + with 'binary' or 'source' or both in it. + :param target_python: The target Python interpreter to use when + evaluating link compatibility. This is used, for example, to + check wheel compatibility, as well as when checking the Python + version, e.g. the Python version embedded in a link filename + (or egg fragment) and against an HTML link's optional PEP 503 + "data-requires-python" attribute. :param allow_yanked: Whether files marked as yanked (in the sense of PEP 592) are permitted to be candidates for install. - :param target_python: The target Python interpreter to use to check - both the Python version embedded in the filename and the package's - "Requires-Python" metadata. If None (the default), then a - TargetPython object will be constructed from the running Python. - :param allow_all_prereleases: Whether to allow all pre-releases. :param ignore_requires_python: Whether to ignore incompatible - "Requires-Python" values in links. Defaults to False. + PEP 503 "data-requires-python" values in HTML links. Defaults + to False. """ - if target_python is None: - target_python = TargetPython() if ignore_requires_python is None: ignore_requires_python = False self._allow_yanked = allow_yanked + self._canonical_name = canonical_name self._ignore_requires_python = ignore_requires_python - self._prefer_binary = prefer_binary + self._formats = formats self._target_python = target_python - # We compile the regex here instead of as a class attribute so as - # not to impact pip start-up time. This is also okay because - # CandidateEvaluator is generally instantiated only once per pip - # invocation (when PackageFinder is instantiated). - self._py_version_re = re.compile(r'-py([123]\.?[0-9]?)$') + self.project_name = project_name - self.allow_all_prereleases = allow_all_prereleases - - def _is_wheel_supported(self, wheel): - # type: (Wheel) -> bool - valid_tags = self._target_python.get_tags() - return wheel.supported(valid_tags) - - def evaluate_link(self, link, search): - # type: (Link, Search) -> Tuple[bool, Optional[Text]] + def evaluate_link(self, link): + # type: (Link) -> Tuple[bool, Optional[Text]] """ Determine whether a link is a candidate for installation. @@ -382,8 +378,8 @@ class CandidateEvaluator(object): return (False, 'not a file') if ext not in SUPPORTED_EXTENSIONS: return (False, 'unsupported archive format: %s' % ext) - if "binary" not in search.formats and ext == WHEEL_EXTENSION: - reason = 'No binaries permitted for %s' % search.supplied + if "binary" not in self._formats and ext == WHEEL_EXTENSION: + reason = 'No binaries permitted for %s' % self.project_name return (False, reason) if "macosx10" in link.path and ext == '.zip': return (False, 'macosx10 one') @@ -392,11 +388,12 @@ class CandidateEvaluator(object): wheel = Wheel(link.filename) except InvalidWheelFilename: return (False, 'invalid wheel filename') - if canonicalize_name(wheel.name) != search.canonical: - reason = 'wrong project name (not %s)' % search.supplied + if canonicalize_name(wheel.name) != self._canonical_name: + reason = 'wrong project name (not %s)' % self.project_name return (False, reason) - if not self._is_wheel_supported(wheel): + supported_tags = self._target_python.get_tags() + if not wheel.supported(supported_tags): # Include the wheel's tags in the reason string to # simplify troubleshooting compatibility issues. file_tags = wheel.get_formatted_file_tags() @@ -409,16 +406,18 @@ class CandidateEvaluator(object): version = wheel.version - # This should be up by the search.ok_binary check, but see issue 2700. - if "source" not in search.formats and ext != WHEEL_EXTENSION: - return (False, 'No sources permitted for %s' % search.supplied) + # This should be up by the self.ok_binary check, but see issue 2700. + if "source" not in self._formats and ext != WHEEL_EXTENSION: + return (False, 'No sources permitted for %s' % self.project_name) if not version: version = _extract_version_from_fragment( - egg_info, search.canonical, + egg_info, self._canonical_name, ) if not version: - return (False, 'Missing project version for %s' % search.supplied) + return ( + False, 'Missing project version for %s' % self.project_name, + ) match = self._py_version_re.search(version) if match: @@ -440,6 +439,36 @@ class CandidateEvaluator(object): return (True, version) + +class CandidateEvaluator(object): + + """ + Responsible for filtering and sorting candidates for installation based + on what tags are valid. + """ + + def __init__( + self, + supported_tags=None, # type: Optional[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. + """ + if supported_tags is None: + target_python = TargetPython() + supported_tags = target_python.get_tags() + + 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] @@ -490,7 +519,7 @@ class CandidateEvaluator(object): If not finding wheels, they are sorted by version only. If finding wheels, then the sort order is by version, then: 1. existing installs - 2. wheels ordered via Wheel.support_index_min(self._valid_tags) + 2. wheels ordered via Wheel.support_index_min(self._supported_tags) 3. source archives If prefer_binary was set, then all wheels are sorted above sources. @@ -498,7 +527,7 @@ class CandidateEvaluator(object): comparison operators, but then different sdist links with the same version, would have to be considered equal """ - valid_tags = self._target_python.get_tags() + valid_tags = self._supported_tags support_num = len(valid_tags) build_tag = tuple() # type: BuildTag binary_preference = 0 @@ -506,7 +535,7 @@ class CandidateEvaluator(object): if link.is_wheel: # can raise InvalidWheelFilename wheel = Wheel(link.filename) - if not self._is_wheel_supported(wheel): + if not wheel.supported(valid_tags): raise UnsupportedWheel( "%s is not a supported wheel for this platform. It " "can't be sorted." % wheel.filename @@ -616,8 +645,11 @@ class PackageFinder(object): 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]] + ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> None """ @@ -635,6 +667,10 @@ class PackageFinder(object): format_control = format_control or FormatControl(set(), set()) + self._allow_yanked = allow_yanked + 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 @@ -665,28 +701,34 @@ class PackageFinder(object): :param trusted_hosts: Domains not to emit warnings for when not using HTTPS. :param session: The Session to use to make requests. - :param target_python: The target Python interpreter. + :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 session is None: raise TypeError( "PackageFinder.create() missing 1 required keyword argument: " "'session'" ) + if target_python is None: + target_python = TargetPython() + supported_tags = target_python.get_tags() candidate_evaluator = CandidateEvaluator( - allow_yanked=selection_prefs.allow_yanked, - target_python=target_python, + supported_tags=supported_tags, prefer_binary=selection_prefs.prefer_binary, allow_all_prereleases=selection_prefs.allow_all_prereleases, - ignore_requires_python=selection_prefs.ignore_requires_python, ) return cls( candidate_evaluator=candidate_evaluator, search_scope=search_scope, session=session, + target_python=target_python, + allow_yanked=selection_prefs.allow_yanked, format_control=selection_prefs.format_control, trusted_hosts=trusted_hosts, + ignore_requires_python=selection_prefs.ignore_requires_python, ) @property @@ -865,6 +907,20 @@ class PackageFinder(object): return False + def make_link_evaluator(self, project_name): + # type: (str) -> LinkEvaluator + canonical_name = canonicalize_name(project_name) + formats = self.format_control.get_allowed_formats(canonical_name) + + return LinkEvaluator( + project_name=project_name, + canonical_name=canonical_name, + formats=formats, + target_python=self._target_python, + allow_yanked=self._allow_yanked, + ignore_requires_python=self._ignore_requires_python, + ) + def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name @@ -872,7 +928,7 @@ class PackageFinder(object): This checks index_urls and find_links. All versions found are returned as an InstallationCandidate list. - See CandidateEvaluator.evaluate_link() for details on which files + See LinkEvaluator.evaluate_link() for details on which files are accepted. """ search_scope = self.search_scope @@ -903,13 +959,11 @@ class PackageFinder(object): for location in url_locations: logger.debug('* %s', location) - canonical_name = canonicalize_name(project_name) - formats = self.format_control.get_allowed_formats(canonical_name) - search = Search(project_name, canonical_name, formats) + link_evaluator = self.make_link_evaluator(project_name) find_links_versions = self._package_versions( + link_evaluator, # We trust every directly linked archive in find_links (Link(url, '-f') for url in self.find_links), - search ) page_versions = [] @@ -917,10 +971,10 @@ class PackageFinder(object): logger.debug('Analyzing links from page %s', page.url) with indent_log(): page_versions.extend( - self._package_versions(page.iter_links(), search) + self._package_versions(link_evaluator, page.iter_links()) ) - file_versions = self._package_versions(file_locations, search) + file_versions = self._package_versions(link_evaluator, file_locations) if file_versions: file_versions.sort(reverse=True) logger.debug( @@ -1075,35 +1129,31 @@ class PackageFinder(object): logger.debug(u'Skipping link: %s: %s', reason, link) self._logged_links.add(link) - def get_install_candidate(self, link, search): - # type: (Link, Search) -> Optional[InstallationCandidate] + def get_install_candidate(self, link_evaluator, link): + # type: (LinkEvaluator, Link) -> Optional[InstallationCandidate] """ If the link is a candidate for install, convert it to an InstallationCandidate and return it. Otherwise, return None. """ - is_candidate, result = ( - self.candidate_evaluator.evaluate_link(link, search=search) - ) + is_candidate, result = link_evaluator.evaluate_link(link) if not is_candidate: if result: self._log_skipped_link(link, reason=result) return None return InstallationCandidate( + project=link_evaluator.project_name, + location=link, # Convert the Text result to str since InstallationCandidate # accepts str. - search.supplied, location=link, version=str(result), + version=str(result), ) - def _package_versions( - self, - links, # type: Iterable[Link] - search # type: Search - ): - # type: (...) -> List[InstallationCandidate] + def _package_versions(self, link_evaluator, links): + # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate] result = [] for link in self._sort_links(links): - candidate = self.get_install_candidate(link, search=search) + candidate = self.get_install_candidate(link_evaluator, link) if candidate is not None: result.append(candidate) return result @@ -1272,13 +1322,3 @@ class HTMLPage(object): if link is None: continue yield link - - -Search = namedtuple('Search', 'supplied canonical formats') -"""Capture key aspects of a search. - -:attribute supplied: The user supplied package. -:attribute canonical: The canonical package name. -:attribute formats: The formats allowed for this package. Should be a set - with 'binary' or 'source' or both in it. -""" diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 7ad5786c4..a23b79c4e 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -82,7 +82,9 @@ class TargetPython(object): def get_tags(self): # type: () -> List[Pep425Tag] """ - Return the supported tags to check wheel candidates against. + Return the supported PEP 425 tags to check wheel candidates against. + + The tags are returned in order of preference (most preferred first). """ if self._valid_tags is None: # Pass versions=None if no py_version_info was given since diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 7a79fb51f..59c7b5ac7 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -3,7 +3,7 @@ import sys import pytest from mock import Mock, patch -from pkg_resources import Distribution, parse_version +from pkg_resources import parse_version import pip._internal.pep425tags import pip._internal.wheel @@ -11,7 +11,7 @@ from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, ) from pip._internal.index import ( - CandidateEvaluator, InstallationCandidate, Link, Search, + CandidateEvaluator, InstallationCandidate, Link, LinkEvaluator, ) from pip._internal.models.target_python import TargetPython from pip._internal.req.constructors import install_req_from_line @@ -216,12 +216,7 @@ class TestWheel: ('pyT', 'TEST', 'any'), ('pyT', 'none', 'any'), ] - target_python = TargetPython() - target_python._valid_tags = valid_tags - evaluator = CandidateEvaluator( - allow_yanked=True, - target_python=target_python, - ) + evaluator = CandidateEvaluator(supported_tags=valid_tags) sort_key = evaluator._sort_key results = sorted(links, key=sort_key, reverse=True) results2 = sorted(reversed(links), key=sort_key, reverse=True) @@ -419,17 +414,17 @@ def test_finder_installs_pre_releases_with_version_spec(): assert link.url == "https://foo/bar-2.0b1.tar.gz" -class TestCandidateEvaluator(object): +class TestLinkEvaluator(object): - # patch this for travis which has distribute in its base env for now - @patch( - 'pip._internal.wheel.pkg_resources.get_distribution', - lambda x: Distribution(project_name='setuptools', version='0.9') - ) - def setup(self): - self.search_name = 'pytest' - self.canonical_name = 'pytest' - self.evaluator = CandidateEvaluator(allow_yanked=True) + def make_test_link_evaluator(self, formats): + target_python = TargetPython() + return LinkEvaluator( + project_name='pytest', + canonical_name='pytest', + formats=formats, + target_python=target_python, + allow_yanked=True, + ) @pytest.mark.parametrize('url, expected_version', [ ('http:/yo/pytest-1.0.tar.gz', '1.0'), @@ -438,12 +433,8 @@ class TestCandidateEvaluator(object): def test_evaluate_link__match(self, url, expected_version): """Test that 'pytest' archives match for 'pytest'""" link = Link(url) - search = Search( - supplied=self.search_name, - canonical=self.canonical_name, - formats=['source', 'binary'], - ) - actual = self.evaluator.evaluate_link(link, search) + evaluator = self.make_test_link_evaluator(formats=['source', 'binary']) + actual = evaluator.evaluate_link(link) assert actual == (True, expected_version) @pytest.mark.parametrize('url, expected_msg', [ @@ -457,12 +448,8 @@ class TestCandidateEvaluator(object): def test_evaluate_link__substring_fails(self, url, expected_msg): """Test that 'pytest archives won't match for 'pytest'.""" link = Link(url) - search = Search( - supplied=self.search_name, - canonical=self.canonical_name, - formats=['source', 'binary'], - ) - actual = self.evaluator.evaluate_link(link, search) + evaluator = self.make_test_link_evaluator(formats=['source', 'binary']) + actual = evaluator.evaluate_link(link) assert actual == (False, expected_msg) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index b653660d2..dc4f74d9f 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -7,9 +7,10 @@ from pip._vendor import html5lib, requests from pip._internal.download import PipSession from pip._internal.index import ( - CandidateEvaluator, HTMLPage, Link, PackageFinder, Search, - _check_link_requires_python, _clean_link, _determine_base_url, - _extract_version_from_fragment, _find_name_version_sep, _get_html_page, + CandidateEvaluator, 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, ) from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.search_scope import SearchScope @@ -84,32 +85,7 @@ def test_check_link_requires_python__invalid_requires(caplog): check_caplog(caplog, 'DEBUG', expected_message) -class TestCandidateEvaluator: - - def test_init__target_python(self): - """ - Test the target_python argument. - """ - target_python = TargetPython(py_version_info=(3, 7, 3)) - evaluator = CandidateEvaluator( - allow_yanked=True, - target_python=target_python, - ) - # The target_python attribute should be set as is. - assert evaluator._target_python is target_python - - def test_init__target_python_none(self): - """ - Test passing None for the target_python argument. - """ - evaluator = CandidateEvaluator( - allow_yanked=True, - target_python=None, - ) - # Spot-check the default TargetPython object. - actual_target_python = evaluator._target_python - assert actual_target_python._given_py_version_info is None - assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO +class TestLinkEvaluator: @pytest.mark.parametrize( 'py_version_info,ignore_requires_python,expected', [ @@ -124,19 +100,19 @@ class TestCandidateEvaluator: self, py_version_info, ignore_requires_python, expected, ): target_python = TargetPython(py_version_info=py_version_info) - evaluator = CandidateEvaluator( - allow_yanked=True, + evaluator = LinkEvaluator( + project_name='twine', + canonical_name='twine', + formats={'source'}, target_python=target_python, + allow_yanked=True, ignore_requires_python=ignore_requires_python, ) link = Link( 'https://example.com/#egg=twine-1.12', requires_python='== 3.6.5', ) - search = Search( - supplied='twine', canonical='twine', formats=['source'], - ) - actual = evaluator.evaluate_link(link, search=search) + actual = evaluator.evaluate_link(link) assert actual == expected @pytest.mark.parametrize('yanked_reason, allow_yanked, expected', [ @@ -155,15 +131,19 @@ class TestCandidateEvaluator: def test_evaluate_link__allow_yanked( self, yanked_reason, allow_yanked, expected, ): - evaluator = CandidateEvaluator(allow_yanked=allow_yanked) + target_python = TargetPython(py_version_info=(3, 6, 4)) + evaluator = LinkEvaluator( + project_name='twine', + canonical_name='twine', + formats={'source'}, + target_python=target_python, + allow_yanked=allow_yanked, + ) link = Link( 'https://example.com/#egg=twine-1.12', yanked_reason=yanked_reason, ) - search = Search( - supplied='twine', canonical='twine', formats=['source'], - ) - actual = evaluator.evaluate_link(link, search=search) + actual = evaluator.evaluate_link(link) assert actual == expected def test_evaluate_link__incompatible_wheel(self): @@ -173,20 +153,23 @@ class TestCandidateEvaluator: target_python = TargetPython(py_version_info=(3, 6, 4)) # Set the valid tags to an empty list to make sure nothing matches. target_python._valid_tags = [] - evaluator = CandidateEvaluator( - allow_yanked=True, + evaluator = LinkEvaluator( + project_name='sample', + canonical_name='sample', + formats={'binary'}, target_python=target_python, + allow_yanked=True, ) link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl') - search = Search( - supplied='sample', canonical='sample', formats=['binary'], - ) - actual = evaluator.evaluate_link(link, search=search) + actual = evaluator.evaluate_link(link) expected = ( False, "none of the wheel's tags match: py2-none-any, py3-none-any" ) assert actual == expected + +class TestCandidateEvaluator: + @pytest.mark.parametrize('yanked_reason, expected', [ # Test a non-yanked file. (None, 0), @@ -201,7 +184,7 @@ class TestCandidateEvaluator: link = Link(url, yanked_reason=yanked_reason) candidate = InstallationCandidate('mypackage', '1.0', link) - evaluator = CandidateEvaluator(allow_yanked=True) + evaluator = CandidateEvaluator() sort_value = evaluator._sort_key(candidate) # Yanked / non-yanked is reflected in the first element of the tuple. actual = sort_value[0] @@ -218,7 +201,7 @@ class TestCandidateEvaluator: """ Test passing an empty list. """ - evaluator = CandidateEvaluator(allow_yanked=True) + evaluator = CandidateEvaluator() actual = evaluator.get_best_candidate([]) assert actual is None @@ -233,7 +216,7 @@ class TestCandidateEvaluator: self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'), ] expected_best = candidates[1] - evaluator = CandidateEvaluator(allow_yanked=True) + evaluator = CandidateEvaluator() actual = evaluator.get_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '3.0' @@ -264,7 +247,7 @@ class TestCandidateEvaluator: candidates = [ self.make_mock_candidate('1.0', yanked_reason=yanked_reason), ] - evaluator = CandidateEvaluator(allow_yanked=True) + evaluator = CandidateEvaluator() actual = evaluator.get_best_candidate(candidates) assert str(actual.version) == '1.0' @@ -291,7 +274,7 @@ class TestCandidateEvaluator: self.make_mock_candidate('1.0'), ] expected_best = candidates[1] - evaluator = CandidateEvaluator(allow_yanked=True) + evaluator = CandidateEvaluator() actual = evaluator.get_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '2.0' @@ -302,43 +285,116 @@ class TestCandidateEvaluator: class TestPackageFinder: - @pytest.mark.parametrize('allow_yanked', [False, True]) - def test_create__allow_yanked(self, allow_yanked): + @pytest.mark.parametrize('allow_all_prereleases, prefer_binary', [ + (False, False), + (False, True), + (True, False), + (True, True), + ]) + def test_create__candidate_evaluator( + self, allow_all_prereleases, prefer_binary, + ): """ - Test that allow_yanked is passed to CandidateEvaluator. + Test that the candidate_evaluator attribute is set correctly. """ - search_scope = SearchScope([], []) - selection_prefs = SelectionPreferences( - allow_yanked=allow_yanked, - ) - finder = PackageFinder.create( - search_scope=search_scope, - selection_prefs=selection_prefs, - session=object(), - ) - evaluator = finder.candidate_evaluator - assert evaluator._allow_yanked == allow_yanked - - def test_create__target_python(self): - """ - Test that target_python is passed to CandidateEvaluator as is. - """ - search_scope = SearchScope([], []) 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=search_scope, + search_scope=SearchScope([], []), selection_prefs=selection_prefs, - session=object(), + session=PipSession(), target_python=target_python, ) evaluator = finder.candidate_evaluator - actual_target_python = evaluator._target_python + assert evaluator.allow_all_prereleases == allow_all_prereleases + assert evaluator._prefer_binary == prefer_binary + assert evaluator._supported_tags == ['tag1', 'tag2'] + + def test_create__target_python(self): + """ + Test that the _target_python attribute is set correctly. + """ + target_python = TargetPython(py_version_info=(3, 7, 3)) + finder = PackageFinder.create( + search_scope=SearchScope([], []), + selection_prefs=SelectionPreferences(allow_yanked=True), + session=PipSession(), + target_python=target_python, + ) + actual_target_python = finder._target_python + # The target_python attribute should be set as is. assert actual_target_python is target_python + # Check that the attributes weren't reset. assert actual_target_python.py_version_info == (3, 7, 3) + def test_create__target_python_none(self): + """ + Test passing target_python=None. + """ + finder = PackageFinder.create( + search_scope=SearchScope([], []), + selection_prefs=SelectionPreferences(allow_yanked=True), + session=PipSession(), + target_python=None, + ) + # Spot-check the default TargetPython object. + actual_target_python = finder._target_python + assert actual_target_python._given_py_version_info is None + assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO + + @pytest.mark.parametrize('allow_yanked', [False, True]) + def test_create__allow_yanked(self, allow_yanked): + """ + Test that the _allow_yanked attribute is set correctly. + """ + selection_prefs = SelectionPreferences(allow_yanked=allow_yanked) + finder = PackageFinder.create( + search_scope=SearchScope([], []), + selection_prefs=selection_prefs, + session=PipSession(), + ) + assert finder._allow_yanked == allow_yanked + + @pytest.mark.parametrize('ignore_requires_python', [False, True]) + def test_create__ignore_requires_python(self, ignore_requires_python): + """ + Test that the _ignore_requires_python attribute is set correctly. + """ + selection_prefs = SelectionPreferences( + allow_yanked=True, + ignore_requires_python=ignore_requires_python, + ) + finder = PackageFinder.create( + search_scope=SearchScope([], []), + selection_prefs=selection_prefs, + session=PipSession(), + ) + assert finder._ignore_requires_python == ignore_requires_python + + def test_create__format_control(self): + """ + Test that the format_control attribute is set correctly. + """ + format_control = FormatControl(set(), {':all:'}) + selection_prefs = SelectionPreferences( + allow_yanked=True, + format_control=format_control, + ) + finder = PackageFinder.create( + search_scope=SearchScope([], []), + selection_prefs=selection_prefs, + session=PipSession(), + ) + actual_format_control = finder.format_control + assert actual_format_control is format_control + # Check that the attributes weren't reset. + assert actual_format_control.only_binary == {':all:'} + def test_add_trusted_host(self): # Leave a gap to test how the ordering is affected. trusted_hosts = ['host1', 'host3'] @@ -429,6 +485,52 @@ class TestPackageFinder: # Spot-check that SECURE_ORIGINS is included. assert actual[0] == ('https', '*', '*') + @pytest.mark.parametrize( + 'allow_yanked, ignore_requires_python, only_binary, expected_formats', + [ + (False, False, {}, frozenset({'binary', 'source'})), + # Test allow_yanked=True. + (True, False, {}, frozenset({'binary', 'source'})), + # Test ignore_requires_python=True. + (False, True, {}, frozenset({'binary', 'source'})), + # Test a non-trivial only_binary. + (False, False, {'twine'}, frozenset({'binary'})), + ] + ) + def test_make_link_evaluator( + self, allow_yanked, ignore_requires_python, only_binary, + expected_formats, + ): + # Create a test TargetPython that we can check for. + 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, + allow_yanked=allow_yanked, + format_control=format_control, + ignore_requires_python=ignore_requires_python, + ) + + # Pass a project_name that will be different from canonical_name. + link_evaluator = finder.make_link_evaluator('Twine') + + assert link_evaluator.project_name == 'Twine' + assert link_evaluator._canonical_name == 'twine' + assert link_evaluator._allow_yanked == allow_yanked + assert link_evaluator._ignore_requires_python == ignore_requires_python + assert link_evaluator._formats == expected_formats + + # Test the _target_python attribute. + actual_target_python = link_evaluator._target_python + # The target_python attribute should be set as is. + assert actual_target_python is target_python + # For good measure, check that the attributes weren't reset. + assert actual_target_python._given_py_version_info == (3, 7) + assert actual_target_python.py_version_info == (3, 7, 0) + def test_sort_locations_file_expand_dir(data): """