mirror of https://github.com/pypa/pip
Merge pull request #6656 from cjerdonek/issue-6654-filter-yanked-files
Fix #6654: filter yanked files before calling get_best_candidate()
This commit is contained in:
commit
15d6182d4c
|
@ -347,6 +347,7 @@ class RequirementCommand(Command):
|
|||
|
||||
return PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=True,
|
||||
format_control=options.format_control,
|
||||
trusted_hosts=options.trusted_hosts,
|
||||
allow_all_prereleases=options.pre,
|
||||
|
|
|
@ -116,8 +116,10 @@ class ListCommand(Command):
|
|||
"""
|
||||
search_scope = make_search_scope(options)
|
||||
|
||||
# Pass allow_yanked=False to ignore yanked versions.
|
||||
return PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=False,
|
||||
allow_all_prereleases=options.pre,
|
||||
trusted_hosts=options.trusted_hosts,
|
||||
session=session,
|
||||
|
@ -183,10 +185,7 @@ class ListCommand(Command):
|
|||
if not candidate.version.is_prerelease]
|
||||
|
||||
evaluator = finder.candidate_evaluator
|
||||
# Pass allow_yanked=False to ignore yanked versions.
|
||||
best_candidate = evaluator.get_best_candidate(
|
||||
all_candidates, allow_yanked=False,
|
||||
)
|
||||
best_candidate = evaluator.get_best_candidate(all_candidates)
|
||||
if best_candidate is None:
|
||||
continue
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ if MYPY_CHECK_RUNNING:
|
|||
from logging import Logger
|
||||
from typing import (
|
||||
Any, Callable, Iterable, Iterator, List, MutableMapping, Optional,
|
||||
Sequence, Set, Tuple, Union,
|
||||
Sequence, Set, Text, Tuple, Union,
|
||||
)
|
||||
import xml.etree.ElementTree
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
|
@ -307,8 +307,13 @@ class CandidateEvaluator(object):
|
|||
on what tags are valid.
|
||||
"""
|
||||
|
||||
# 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
|
||||
|
@ -316,6 +321,8 @@ class CandidateEvaluator(object):
|
|||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
: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
|
||||
|
@ -329,6 +336,7 @@ class CandidateEvaluator(object):
|
|||
if ignore_requires_python is None:
|
||||
ignore_requires_python = False
|
||||
|
||||
self._allow_yanked = allow_yanked
|
||||
self._ignore_requires_python = ignore_requires_python
|
||||
self._prefer_binary = prefer_binary
|
||||
self._target_python = target_python
|
||||
|
@ -347,7 +355,7 @@ class CandidateEvaluator(object):
|
|||
return wheel.supported(valid_tags)
|
||||
|
||||
def evaluate_link(self, link, search):
|
||||
# type: (Link, Search) -> Tuple[bool, Optional[str]]
|
||||
# type: (Link, Search) -> Tuple[bool, Optional[Text]]
|
||||
"""
|
||||
Determine whether a link is a candidate for installation.
|
||||
|
||||
|
@ -357,6 +365,13 @@ class CandidateEvaluator(object):
|
|||
the link fails to qualify.
|
||||
"""
|
||||
version = None
|
||||
if link.is_yanked and not self._allow_yanked:
|
||||
reason = link.yanked_reason or '<none given>'
|
||||
# Mark this as a unicode string to prevent "UnicodeEncodeError:
|
||||
# 'ascii' codec can't encode character" in Python 2 when
|
||||
# the reason contains non-ascii characters.
|
||||
return (False, u'yanked for reason: {}'.format(reason))
|
||||
|
||||
if link.egg_fragment:
|
||||
egg_info = link.egg_fragment
|
||||
ext = link.ext
|
||||
|
@ -507,23 +522,14 @@ class CandidateEvaluator(object):
|
|||
yank_value, binary_preference, candidate.version, build_tag, pri,
|
||||
)
|
||||
|
||||
# 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 get_best_candidate(
|
||||
self,
|
||||
candidates, # type: List[InstallationCandidate]
|
||||
allow_yanked, # type: bool
|
||||
):
|
||||
# type: (...) -> Optional[InstallationCandidate]
|
||||
"""
|
||||
Return the best candidate per the instance's sort order, or None if
|
||||
no candidate is acceptable.
|
||||
|
||||
:param allow_yanked: Whether to permit returning a yanked candidate
|
||||
in the sense of PEP 592. If true, a yanked candidate will be
|
||||
returned only if all candidates have been yanked.
|
||||
"""
|
||||
if not candidates:
|
||||
return None
|
||||
|
@ -532,20 +538,17 @@ class CandidateEvaluator(object):
|
|||
|
||||
# Log a warning per PEP 592 if necessary before returning.
|
||||
link = best_candidate.location
|
||||
if not link.is_yanked:
|
||||
return best_candidate
|
||||
|
||||
# Otherwise, all the candidates were yanked.
|
||||
if not allow_yanked:
|
||||
return None
|
||||
|
||||
reason = link.yanked_reason or '<none given>'
|
||||
msg = (
|
||||
'The candidate selected for download or install is a '
|
||||
'yanked version: {candidate}\n'
|
||||
'Reason for being yanked: {reason}'
|
||||
).format(candidate=best_candidate, reason=reason)
|
||||
logger.warning(msg)
|
||||
if link.is_yanked:
|
||||
reason = link.yanked_reason or '<none given>'
|
||||
msg = (
|
||||
# Mark this as a unicode string to prevent
|
||||
# "UnicodeEncodeError: 'ascii' codec can't encode character"
|
||||
# in Python 2 when the reason contains non-ascii characters.
|
||||
u'The candidate selected for download or install is a '
|
||||
'yanked version: {candidate}\n'
|
||||
'Reason for being yanked: {reason}'
|
||||
).format(candidate=best_candidate, reason=reason)
|
||||
logger.warning(msg)
|
||||
|
||||
return best_candidate
|
||||
|
||||
|
@ -589,23 +592,13 @@ class FoundCandidates(object):
|
|||
# Again, converting version to str to deal with debundling.
|
||||
return (c for c in self.iter_all() if str(c.version) in self._versions)
|
||||
|
||||
# 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 get_best(self, allow_yanked):
|
||||
# type: (bool) -> Optional[InstallationCandidate]
|
||||
def get_best(self):
|
||||
# type: () -> Optional[InstallationCandidate]
|
||||
"""Return the best candidate available, or None if no applicable
|
||||
candidates are found.
|
||||
|
||||
:param allow_yanked: Whether to permit returning a yanked candidate
|
||||
in the sense of PEP 592. If true, a yanked candidate will be
|
||||
returned only if all candidates have been yanked.
|
||||
"""
|
||||
candidates = list(self.iter_applicable())
|
||||
return self._evaluator.get_best_candidate(
|
||||
candidates, allow_yanked=allow_yanked,
|
||||
)
|
||||
return self._evaluator.get_best_candidate(candidates)
|
||||
|
||||
|
||||
class PackageFinder(object):
|
||||
|
@ -648,10 +641,15 @@ class PackageFinder(object):
|
|||
# These are boring links that have already been logged somehow.
|
||||
self._logged_links = set() # type: Set[Link]
|
||||
|
||||
# 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.
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
search_scope, # type: SearchScope
|
||||
allow_yanked, # type: bool
|
||||
allow_all_prereleases=False, # type: bool
|
||||
trusted_hosts=None, # type: Optional[List[str]]
|
||||
session=None, # type: Optional[PipSession]
|
||||
|
@ -663,6 +661,8 @@ class PackageFinder(object):
|
|||
# type: (...) -> PackageFinder
|
||||
"""Create a PackageFinder.
|
||||
|
||||
:param allow_yanked: Whether files marked as yanked (in the sense
|
||||
of PEP 592) are permitted to be candidates for install.
|
||||
:param trusted_hosts: Domains not to emit warnings for when not using
|
||||
HTTPS.
|
||||
:param session: The Session to use to make requests.
|
||||
|
@ -682,6 +682,7 @@ class PackageFinder(object):
|
|||
)
|
||||
|
||||
candidate_evaluator = CandidateEvaluator(
|
||||
allow_yanked=allow_yanked,
|
||||
target_python=target_python,
|
||||
prefer_binary=prefer_binary,
|
||||
allow_all_prereleases=allow_all_prereleases,
|
||||
|
@ -969,7 +970,7 @@ class PackageFinder(object):
|
|||
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
|
||||
"""
|
||||
candidates = self.find_candidates(req.name, req.specifier)
|
||||
best_candidate = candidates.get_best(allow_yanked=True)
|
||||
best_candidate = candidates.get_best()
|
||||
|
||||
installed_version = None # type: Optional[_BaseVersion]
|
||||
if req.satisfied_by is not None:
|
||||
|
@ -1072,11 +1073,14 @@ class PackageFinder(object):
|
|||
return no_eggs + eggs
|
||||
|
||||
def _log_skipped_link(self, link, reason):
|
||||
# type: (Link, str) -> None
|
||||
# type: (Link, Text) -> None
|
||||
if link not in self._logged_links:
|
||||
# Put the link at the end so the reason is more visible and
|
||||
# because the link string is usually very long.
|
||||
logger.debug('Skipping link: %s: %s', reason, link)
|
||||
# Mark this as a unicode string to prevent "UnicodeEncodeError:
|
||||
# 'ascii' codec can't encode character" in Python 2 when
|
||||
# the reason contains non-ascii characters.
|
||||
# Also, put the link at the end so the reason is more visible
|
||||
# and because the link string is usually very long.
|
||||
logger.debug(u'Skipping link: %s: %s', reason, link)
|
||||
self._logged_links.add(link)
|
||||
|
||||
def get_install_candidate(self, link, search):
|
||||
|
@ -1094,7 +1098,9 @@ class PackageFinder(object):
|
|||
return None
|
||||
|
||||
return InstallationCandidate(
|
||||
search.supplied, location=link, version=result,
|
||||
# Convert the Text result to str since InstallationCandidate
|
||||
# accepts str.
|
||||
search.supplied, location=link, version=str(result),
|
||||
)
|
||||
|
||||
def _package_versions(
|
||||
|
@ -1229,6 +1235,7 @@ def _create_link_from_element(
|
|||
|
||||
yanked_reason = anchor.get('data-yanked')
|
||||
if yanked_reason:
|
||||
# This is a unicode string in Python 2 (and 3).
|
||||
yanked_reason = unescape(yanked_reason)
|
||||
|
||||
link = Link(
|
||||
|
|
|
@ -11,7 +11,7 @@ from pip._internal.utils.models import KeyBasedCompareMixin
|
|||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Optional, Tuple, Union
|
||||
from typing import Optional, Text, Tuple, Union
|
||||
from pip._internal.index import HTMLPage
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ class Link(KeyBasedCompareMixin):
|
|||
url, # type: str
|
||||
comes_from=None, # type: Optional[Union[str, HTMLPage]]
|
||||
requires_python=None, # type: Optional[str]
|
||||
yanked_reason=None, # type: Optional[str]
|
||||
yanked_reason=None, # type: Optional[Text]
|
||||
):
|
||||
# type: (...) -> None
|
||||
"""
|
||||
|
|
|
@ -125,17 +125,16 @@ def pip_version_check(session, options):
|
|||
# Lets use PackageFinder to see what the latest pip version is
|
||||
search_scope = make_search_scope(options, suppress_no_index=True)
|
||||
|
||||
# Pass allow_yanked=False so we don't suggest upgrading to a
|
||||
# yanked version.
|
||||
finder = PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=False,
|
||||
allow_all_prereleases=False, # Explicitly set to False
|
||||
trusted_hosts=options.trusted_hosts,
|
||||
session=session,
|
||||
)
|
||||
# Pass allow_yanked=False so we don't suggest upgrading to a
|
||||
# yanked version.
|
||||
candidate = finder.find_candidates("pip").get_best(
|
||||
allow_yanked=False,
|
||||
)
|
||||
candidate = finder.find_candidates("pip").get_best()
|
||||
if candidate is None:
|
||||
return
|
||||
pypi_version = str(candidate.version)
|
||||
|
|
|
@ -104,6 +104,7 @@ def make_test_finder(
|
|||
|
||||
return PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=True,
|
||||
allow_all_prereleases=allow_all_prereleases,
|
||||
trusted_hosts=trusted_hosts,
|
||||
session=session,
|
||||
|
|
|
@ -27,7 +27,11 @@ def run_with_build_env(script, setup_script_contents,
|
|||
from pip._internal.models.search_scope import SearchScope
|
||||
|
||||
search_scope = SearchScope.create([%r], [])
|
||||
finder = PackageFinder.create(search_scope, session=PipSession())
|
||||
finder = PackageFinder.create(
|
||||
search_scope,
|
||||
allow_yanked=True,
|
||||
session=PipSession(),
|
||||
)
|
||||
build_env = BuildEnvironment()
|
||||
|
||||
try:
|
||||
|
|
|
@ -218,7 +218,10 @@ class TestWheel:
|
|||
]
|
||||
target_python = TargetPython()
|
||||
target_python._valid_tags = valid_tags
|
||||
evaluator = CandidateEvaluator(target_python=target_python)
|
||||
evaluator = CandidateEvaluator(
|
||||
allow_yanked=True,
|
||||
target_python=target_python,
|
||||
)
|
||||
sort_key = evaluator._sort_key
|
||||
results = sorted(links, key=sort_key, reverse=True)
|
||||
results2 = sorted(reversed(links), key=sort_key, reverse=True)
|
||||
|
@ -426,7 +429,7 @@ class TestCandidateEvaluator(object):
|
|||
def setup(self):
|
||||
self.search_name = 'pytest'
|
||||
self.canonical_name = 'pytest'
|
||||
self.evaluator = CandidateEvaluator()
|
||||
self.evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
|
||||
@pytest.mark.parametrize('url, expected_version', [
|
||||
('http:/yo/pytest-1.0.tar.gz', '1.0'),
|
||||
|
|
|
@ -90,7 +90,10 @@ class TestCandidateEvaluator:
|
|||
Test the target_python argument.
|
||||
"""
|
||||
target_python = TargetPython(py_version_info=(3, 7, 3))
|
||||
evaluator = CandidateEvaluator(target_python=target_python)
|
||||
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
|
||||
|
||||
|
@ -98,7 +101,10 @@ class TestCandidateEvaluator:
|
|||
"""
|
||||
Test passing None for the target_python argument.
|
||||
"""
|
||||
evaluator = CandidateEvaluator(target_python=None)
|
||||
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
|
||||
|
@ -118,6 +124,7 @@ class TestCandidateEvaluator:
|
|||
):
|
||||
target_python = TargetPython(py_version_info=py_version_info)
|
||||
evaluator = CandidateEvaluator(
|
||||
allow_yanked=True,
|
||||
target_python=target_python,
|
||||
ignore_requires_python=ignore_requires_python,
|
||||
)
|
||||
|
@ -131,6 +138,33 @@ class TestCandidateEvaluator:
|
|||
actual = evaluator.evaluate_link(link, search=search)
|
||||
assert actual == expected
|
||||
|
||||
@pytest.mark.parametrize('yanked_reason, allow_yanked, expected', [
|
||||
(None, True, (True, '1.12')),
|
||||
(None, False, (True, '1.12')),
|
||||
('', True, (True, '1.12')),
|
||||
('', False, (False, 'yanked for reason: <none given>')),
|
||||
('bad metadata', True, (True, '1.12')),
|
||||
('bad metadata', False,
|
||||
(False, 'yanked for reason: bad metadata')),
|
||||
# Test a unicode string with a non-ascii character.
|
||||
(u'curly quote: \u2018', True, (True, '1.12')),
|
||||
(u'curly quote: \u2018', False,
|
||||
(False, u'yanked for reason: curly quote: \u2018')),
|
||||
])
|
||||
def test_evaluate_link__allow_yanked(
|
||||
self, yanked_reason, allow_yanked, expected,
|
||||
):
|
||||
evaluator = CandidateEvaluator(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)
|
||||
assert actual == expected
|
||||
|
||||
def test_evaluate_link__incompatible_wheel(self):
|
||||
"""
|
||||
Test an incompatible wheel.
|
||||
|
@ -138,7 +172,10 @@ 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(target_python=target_python)
|
||||
evaluator = CandidateEvaluator(
|
||||
allow_yanked=True,
|
||||
target_python=target_python,
|
||||
)
|
||||
link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl')
|
||||
search = Search(
|
||||
supplied='sample', canonical='sample', formats=['binary'],
|
||||
|
@ -163,7 +200,7 @@ class TestCandidateEvaluator:
|
|||
link = Link(url, yanked_reason=yanked_reason)
|
||||
candidate = InstallationCandidate('mypackage', '1.0', link)
|
||||
|
||||
evaluator = CandidateEvaluator()
|
||||
evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
sort_value = evaluator._sort_key(candidate)
|
||||
# Yanked / non-yanked is reflected in the first element of the tuple.
|
||||
actual = sort_value[0]
|
||||
|
@ -176,30 +213,17 @@ class TestCandidateEvaluator:
|
|||
|
||||
return candidate
|
||||
|
||||
@pytest.mark.parametrize('allow_yanked', [True, False])
|
||||
def test_get_best_candidate__no_candidates(self, allow_yanked):
|
||||
def test_get_best_candidate__no_candidates(self):
|
||||
"""
|
||||
Test passing an empty list.
|
||||
"""
|
||||
evaluator = CandidateEvaluator()
|
||||
actual = evaluator.get_best_candidate([], allow_yanked=allow_yanked)
|
||||
evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
actual = evaluator.get_best_candidate([])
|
||||
assert actual is None
|
||||
|
||||
def test_get_best_candidate__all_yanked__allow_yanked_false(self):
|
||||
def test_get_best_candidate__all_yanked(self, caplog):
|
||||
"""
|
||||
Test all candidates yanked with allow_yanked=False.
|
||||
"""
|
||||
candidates = [
|
||||
self.make_mock_candidate('1.0', yanked_reason=''),
|
||||
self.make_mock_candidate('2.0', yanked_reason=''),
|
||||
]
|
||||
evaluator = CandidateEvaluator()
|
||||
actual = evaluator.get_best_candidate(candidates, allow_yanked=False)
|
||||
assert actual is None
|
||||
|
||||
def test_get_best_candidate__all_yanked__allow_yanked_true(self, caplog):
|
||||
"""
|
||||
Test all candidates yanked with allow_yanked=True.
|
||||
Test all candidates yanked.
|
||||
"""
|
||||
candidates = [
|
||||
self.make_mock_candidate('1.0', yanked_reason='bad metadata #1'),
|
||||
|
@ -208,8 +232,8 @@ class TestCandidateEvaluator:
|
|||
self.make_mock_candidate('2.0', yanked_reason='bad metadata #2'),
|
||||
]
|
||||
expected_best = candidates[1]
|
||||
evaluator = CandidateEvaluator()
|
||||
actual = evaluator.get_best_candidate(candidates, allow_yanked=True)
|
||||
evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
actual = evaluator.get_best_candidate(candidates)
|
||||
assert actual is expected_best
|
||||
assert str(actual.version) == '3.0'
|
||||
|
||||
|
@ -224,26 +248,35 @@ class TestCandidateEvaluator:
|
|||
'Reason for being yanked: bad metadata #3'
|
||||
)
|
||||
|
||||
def test_get_best_candidate__yanked_no_reason_given(self, caplog):
|
||||
@pytest.mark.parametrize('yanked_reason, expected_reason', [
|
||||
# Test no reason given.
|
||||
('', '<none given>'),
|
||||
# Test a unicode string with a non-ascii character.
|
||||
(u'curly quote: \u2018', u'curly quote: \u2018'),
|
||||
])
|
||||
def test_get_best_candidate__yanked_reason(
|
||||
self, caplog, yanked_reason, expected_reason,
|
||||
):
|
||||
"""
|
||||
Test the log message when no reason is given.
|
||||
Test the log message with various reason strings.
|
||||
"""
|
||||
candidates = [
|
||||
self.make_mock_candidate('1.0', yanked_reason=''),
|
||||
self.make_mock_candidate('1.0', yanked_reason=yanked_reason),
|
||||
]
|
||||
evaluator = CandidateEvaluator()
|
||||
actual = evaluator.get_best_candidate(candidates, allow_yanked=True)
|
||||
evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
actual = evaluator.get_best_candidate(candidates)
|
||||
assert str(actual.version) == '1.0'
|
||||
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.levelname == 'WARNING'
|
||||
assert record.message == (
|
||||
expected_message = (
|
||||
'The candidate selected for download or install is a yanked '
|
||||
"version: 'mypackage' candidate "
|
||||
'(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n'
|
||||
'Reason for being yanked: <none given>'
|
||||
)
|
||||
'Reason for being yanked: '
|
||||
) + expected_reason
|
||||
assert record.message == expected_message
|
||||
|
||||
def test_get_best_candidate__best_yanked_but_not_all(self, caplog):
|
||||
"""
|
||||
|
@ -257,8 +290,8 @@ class TestCandidateEvaluator:
|
|||
self.make_mock_candidate('1.0'),
|
||||
]
|
||||
expected_best = candidates[1]
|
||||
evaluator = CandidateEvaluator()
|
||||
actual = evaluator.get_best_candidate(candidates, allow_yanked=True)
|
||||
evaluator = CandidateEvaluator(allow_yanked=True)
|
||||
actual = evaluator.get_best_candidate(candidates)
|
||||
assert actual is expected_best
|
||||
assert str(actual.version) == '2.0'
|
||||
|
||||
|
@ -268,6 +301,20 @@ class TestCandidateEvaluator:
|
|||
|
||||
class TestPackageFinder:
|
||||
|
||||
@pytest.mark.parametrize('allow_yanked', [False, True])
|
||||
def test_create__allow_yanked(self, allow_yanked):
|
||||
"""
|
||||
Test that allow_yanked is passed to CandidateEvaluator.
|
||||
"""
|
||||
search_scope = SearchScope([], [])
|
||||
finder = PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=allow_yanked,
|
||||
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.
|
||||
|
@ -276,6 +323,7 @@ class TestPackageFinder:
|
|||
target_python = TargetPython(py_version_info=(3, 7, 3))
|
||||
finder = PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=True,
|
||||
session=object(),
|
||||
target_python=target_python,
|
||||
)
|
||||
|
@ -361,6 +409,7 @@ class TestPackageFinder:
|
|||
search_scope = SearchScope([], [])
|
||||
finder = PackageFinder.create(
|
||||
search_scope=search_scope,
|
||||
allow_yanked=True,
|
||||
trusted_hosts=None,
|
||||
session=object(),
|
||||
)
|
||||
|
@ -656,10 +705,18 @@ class TestHTMLPage:
|
|||
# Test a value with an escaped character.
|
||||
('<a href="/pkg4-1.0.tar.gz" data-yanked="version < 1"></a>',
|
||||
'version < 1'),
|
||||
# Test a yanked reason with a non-ascii character.
|
||||
(u'<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>',
|
||||
u'curlyquote \u2018'),
|
||||
]
|
||||
)
|
||||
def test_iter_links__yanked_reason(self, anchor_html, expected):
|
||||
html = '<html><body>{}</body></html>'.format(anchor_html)
|
||||
html = (
|
||||
# Mark this as a unicode string for Python 2 since anchor_html
|
||||
# can contain non-ascii.
|
||||
u'<html><head><meta charset="utf-8"><head>'
|
||||
'<body>{}</body></html>'
|
||||
).format(anchor_html)
|
||||
html_bytes = html.encode('utf-8')
|
||||
page = HTMLPage(html_bytes, url='https://example.com/simple/')
|
||||
links = list(page.iter_links())
|
||||
|
|
|
@ -16,7 +16,7 @@ class MockFoundCandidates(object):
|
|||
def __init__(self, best):
|
||||
self._best = best
|
||||
|
||||
def get_best(self, allow_yanked):
|
||||
def get_best(self):
|
||||
return self._best
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue