test_resolution_legacy_resolver.py 9.7 KB
Newer Older
1
import logging
2
from unittest import mock
A
Lint  
Andrey Bienkowski 已提交
3

4
import pytest
5
from pip._vendor import pkg_resources
6

P
Pradyun Gedam 已提交
7
from pip._internal.exceptions import NoneMetadataError, UnsupportedPythonVersion
8
from pip._internal.req.constructors import install_req_from_line
9
from pip._internal.resolution.legacy.resolver import (
10
    Resolver,
11 12
    _check_dist_requires_python,
)
13
from pip._internal.utils.packaging import get_requires_python
14 15
from tests.lib import make_test_finder
from tests.lib.index import make_mock_candidate
16 17


18 19 20
# We need to inherit from DistInfoDistribution for the `isinstance()`
# check inside `packaging.get_metadata()` to work.
class FakeDist(pkg_resources.DistInfoDistribution):
21

22 23 24 25 26 27 28 29 30
    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'
31

32 33 34
        self.project_name = 'my-project'
        self.metadata_name = metadata_name
        self.metadata = metadata
35

36
    def __str__(self):
37
        return f'<distribution {self.project_name!r}>'
38 39 40 41 42 43 44 45 46 47 48 49

    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:
50
        metadata += f'Requires-Python:{requires_python}'
51 52

    return FakeDist(metadata, metadata_name=metadata_name)
53 54


55
class TestCheckDistRequiresPython:
56 57 58 59 60

    """
    Test _check_dist_requires_python().
    """

61 62 63 64
    def test_compatible(self, caplog):
        """
        Test a Python version compatible with the dist's Requires-Python.
        """
65
        caplog.set_level(logging.DEBUG)
66
        dist = make_fake_dist('== 3.6.5')
67 68 69 70 71 72

        _check_dist_requires_python(
            dist,
            version_info=(3, 6, 5),
            ignore_requires_python=False,
        )
73
        assert not len(caplog.records)
74

75 76 77 78 79
    def test_incompatible(self):
        """
        Test a Python version incompatible with the dist's Requires-Python.
        """
        dist = make_fake_dist('== 3.6.4')
80 81 82 83 84 85 86 87 88 89 90
        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'"
        )

91 92 93 94 95
    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.
        """
96
        caplog.set_level(logging.DEBUG)
97
        dist = make_fake_dist('== 3.6.4')
98 99 100 101 102 103 104 105 106 107 108 109
        _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'"
        )
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173

    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: "
            "<distribution 'my-project'>".format(metadata_name)
        )
174 175


176
class TestYankedWarning:
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    """
    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.
        """
204 205 206 207
        # Ignore spurious DEBUG level messages
        # TODO: Probably better to work out why they are occurring, but IMO the
        #       tests are at fault here for being to dependent on exact output.
        caplog.set_level(logging.WARNING)
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
        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.
        """
224 225 226 227
        # Ignore spurious DEBUG level messages
        # TODO: Probably better to work out why they are occurring, but IMO the
        #       tests are at fault here for being to dependent on exact output.
        caplog.set_level(logging.WARNING)
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
        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.
        ('', '<none given>'),
        # Test a unicode string with a non-ascii character.
J
Jon Dufresne 已提交
256
        ('curly quote: \u2018', 'curly quote: \u2018'),
257 258 259 260 261 262 263
    ])
    def test_sort_best_candidate__yanked_reason(
        self, caplog, monkeypatch, yanked_reason, expected_reason,
    ):
        """
        Test the log message with various reason strings.
        """
264 265 266 267
        # Ignore spurious DEBUG level messages
        # TODO: Probably better to work out why they are occurring, but IMO the
        #       tests are at fault here for being to dependent on exact output.
        caplog.set_level(logging.WARNING)
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
        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