import logging import mock import pytest from pip._vendor import pkg_resources from pip._internal.exceptions import NoneMetadataError, UnsupportedPythonVersion from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.legacy.resolver import ( Resolver, _check_dist_requires_python, ) from pip._internal.utils.packaging import get_requires_python from tests.lib import make_test_finder from tests.lib.index import make_mock_candidate # We need to inherit from DistInfoDistribution for the `isinstance()` # check inside `packaging.get_metadata()` to work. class FakeDist(pkg_resources.DistInfoDistribution): def __init__(self, metadata, metadata_name=None): """ :param metadata: The value that dist.get_metadata() should return for the `metadata_name` metadata. :param metadata_name: The name of the metadata to store (can be "METADATA" or "PKG-INFO"). Defaults to "METADATA". """ if metadata_name is None: metadata_name = 'METADATA' self.project_name = 'my-project' self.metadata_name = metadata_name self.metadata = metadata def __str__(self): return ''.format(self.project_name) def has_metadata(self, name): return (name == self.metadata_name) def get_metadata(self, name): assert name == self.metadata_name return self.metadata def make_fake_dist(requires_python=None, metadata_name=None): metadata = 'Name: test\n' if requires_python is not None: metadata += 'Requires-Python:{}'.format(requires_python) return FakeDist(metadata, metadata_name=metadata_name) class TestCheckDistRequiresPython(object): """ Test _check_dist_requires_python(). """ def test_compatible(self, caplog): """ Test a Python version compatible with the dist's Requires-Python. """ caplog.set_level(logging.DEBUG) dist = make_fake_dist('== 3.6.5') _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=False, ) assert not len(caplog.records) def test_incompatible(self): """ Test a Python version incompatible with the dist's Requires-Python. """ dist = make_fake_dist('== 3.6.4') with pytest.raises(UnsupportedPythonVersion) as exc: _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=False, ) assert str(exc.value) == ( "Package 'my-project' requires a different Python: " "3.6.5 not in '== 3.6.4'" ) def test_incompatible_with_ignore_requires(self, caplog): """ Test a Python version incompatible with the dist's Requires-Python while passing ignore_requires_python=True. """ caplog.set_level(logging.DEBUG) dist = make_fake_dist('== 3.6.4') _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=True, ) assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == 'DEBUG' assert record.message == ( "Ignoring failed Requires-Python check for package 'my-project': " "3.6.5 not in '== 3.6.4'" ) def test_none_requires_python(self, caplog): """ Test a dist with Requires-Python None. """ caplog.set_level(logging.DEBUG) dist = make_fake_dist() # Make sure our test setup is correct. assert get_requires_python(dist) is None assert len(caplog.records) == 0 # Then there is no exception and no log message. _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=False, ) assert len(caplog.records) == 0 def test_invalid_requires_python(self, caplog): """ Test a dist with an invalid Requires-Python. """ caplog.set_level(logging.DEBUG) dist = make_fake_dist('invalid') _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=False, ) assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == 'WARNING' assert record.message == ( "Package 'my-project' has an invalid Requires-Python: " "Invalid specifier: 'invalid'" ) @pytest.mark.parametrize('metadata_name', [ 'METADATA', 'PKG-INFO', ]) def test_empty_metadata_error(self, caplog, metadata_name): """ Test dist.has_metadata() returning True and dist.get_metadata() returning None. """ dist = make_fake_dist(metadata_name=metadata_name) dist.metadata = None # Make sure our test setup is correct. assert dist.has_metadata(metadata_name) assert dist.get_metadata(metadata_name) is None with pytest.raises(NoneMetadataError) as exc: _check_dist_requires_python( dist, version_info=(3, 6, 5), ignore_requires_python=False, ) assert str(exc.value) == ( "None {} metadata found for distribution: " "".format(metadata_name) ) class TestYankedWarning(object): """ Test _populate_link() emits warning if one or more candidates are yanked. """ def _make_test_resolver(self, monkeypatch, mock_candidates): def _find_candidates(project_name): return mock_candidates finder = make_test_finder() monkeypatch.setattr(finder, "find_all_candidates", _find_candidates) return Resolver( finder=finder, preparer=mock.Mock(), # Not used. make_install_req=install_req_from_line, wheel_cache=None, use_user_site=False, force_reinstall=False, ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, upgrade_strategy="to-satisfy-only", ) def test_sort_best_candidate__has_non_yanked(self, caplog, monkeypatch): """ Test unyanked candidate preferred over yanked. """ candidates = [ make_mock_candidate('1.0'), make_mock_candidate('2.0', yanked_reason='bad metadata #2'), ] ireq = install_req_from_line("pkg") resolver = self._make_test_resolver(monkeypatch, candidates) resolver._populate_link(ireq) assert ireq.link == candidates[0].link assert len(caplog.records) == 0 def test_sort_best_candidate__all_yanked(self, caplog, monkeypatch): """ Test all candidates yanked. """ candidates = [ make_mock_candidate('1.0', yanked_reason='bad metadata #1'), # Put the best candidate in the middle, to test sorting. make_mock_candidate('3.0', yanked_reason='bad metadata #3'), make_mock_candidate('2.0', yanked_reason='bad metadata #2'), ] ireq = install_req_from_line("pkg") resolver = self._make_test_resolver(monkeypatch, candidates) resolver._populate_link(ireq) assert ireq.link == candidates[1].link # Check the log messages. assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == 'WARNING' assert record.message == ( 'The candidate selected for download or install is a yanked ' "version: 'mypackage' candidate " '(version 3.0 at https://example.com/pkg-3.0.tar.gz)\n' 'Reason for being yanked: bad metadata #3' ) @pytest.mark.parametrize('yanked_reason, expected_reason', [ # Test no reason given. ('', ''), # Test a unicode string with a non-ascii character. (u'curly quote: \u2018', u'curly quote: \u2018'), ]) def test_sort_best_candidate__yanked_reason( self, caplog, monkeypatch, yanked_reason, expected_reason, ): """ Test the log message with various reason strings. """ candidates = [ make_mock_candidate('1.0', yanked_reason=yanked_reason), ] ireq = install_req_from_line("pkg") resolver = self._make_test_resolver(monkeypatch, candidates) resolver._populate_link(ireq) assert ireq.link == candidates[0].link assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == 'WARNING' 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: ' ) + expected_reason assert record.message == expected_message