Add LinkEvaluator class.

This commit is contained in:
Chris Jerdonek 2019-06-30 12:14:53 -07:00
parent a8510bc5e6
commit 979c8405d2
4 changed files with 316 additions and 185 deletions

View File

@ -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.
"""

View File

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

View File

@ -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<something> 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)

View File

@ -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):
"""