mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
42359a9605
The pip-specific Path implementation has been removed, and all its usages replaced by pathlib.Path. The tmpdir and tmpdir_factory fixtures are also removed, and all usages are replaced by tmp_path and tmp_path_factory, which use pathlib.Path. The pip() function now also accepts pathlib.Path so we don't need to put str() everywhere. Path arguments are coerced with os.fspath() into str.
372 lines
12 KiB
Python
372 lines
12 KiB
Python
import email.message
|
|
import logging
|
|
import os
|
|
from typing import List, Optional, Type, TypeVar, cast
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from pip._vendor.packaging.specifiers import SpecifierSet
|
|
from pip._vendor.packaging.utils import NormalizedName
|
|
|
|
from pip._internal.exceptions import (
|
|
InstallationError,
|
|
NoneMetadataError,
|
|
UnsupportedPythonVersion,
|
|
)
|
|
from pip._internal.metadata import BaseDistribution
|
|
from pip._internal.models.candidate import InstallationCandidate
|
|
from pip._internal.req.constructors import install_req_from_line
|
|
from pip._internal.req.req_set import RequirementSet
|
|
from pip._internal.resolution.legacy.resolver import (
|
|
Resolver,
|
|
_check_dist_requires_python,
|
|
)
|
|
from tests.lib import TestData, make_test_finder
|
|
from tests.lib.index import make_mock_candidate
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class FakeDist(BaseDistribution):
|
|
def __init__(self, metadata: email.message.Message) -> None:
|
|
self._canonical_name = cast(NormalizedName, "my-project")
|
|
self._metadata = metadata
|
|
|
|
def __str__(self) -> str:
|
|
return f"<distribution {self.canonical_name!r}>"
|
|
|
|
@property
|
|
def canonical_name(self) -> NormalizedName:
|
|
return self._canonical_name
|
|
|
|
@property
|
|
def metadata(self) -> email.message.Message:
|
|
return self._metadata
|
|
|
|
|
|
def make_fake_dist(
|
|
*, klass: Type[BaseDistribution] = FakeDist, requires_python: Optional[str] = None
|
|
) -> BaseDistribution:
|
|
metadata = email.message.Message()
|
|
metadata["Name"] = "my-project"
|
|
if requires_python is not None:
|
|
metadata["Requires-Python"] = requires_python
|
|
|
|
# Too many arguments for "BaseDistribution"
|
|
return klass(metadata) # type: ignore[call-arg]
|
|
|
|
|
|
def make_test_resolver(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
mock_candidates: List[InstallationCandidate],
|
|
) -> Resolver:
|
|
def _find_candidates(project_name: str) -> List[InstallationCandidate]:
|
|
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",
|
|
)
|
|
|
|
|
|
class TestAddRequirement:
|
|
"""
|
|
Test _add_requirement_to_set().
|
|
"""
|
|
|
|
def test_unsupported_wheel_link_requirement_raises(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# GIVEN
|
|
resolver = make_test_resolver(monkeypatch, [])
|
|
requirement_set = RequirementSet(check_supported_wheels=True)
|
|
|
|
install_req = install_req_from_line(
|
|
"https://whatever.com/peppercorn-0.4-py2.py3-bogus-any.whl",
|
|
)
|
|
assert install_req.link is not None
|
|
assert install_req.link.is_wheel
|
|
assert install_req.link.scheme == "https"
|
|
|
|
# WHEN / THEN
|
|
with pytest.raises(InstallationError):
|
|
resolver._add_requirement_to_set(requirement_set, install_req)
|
|
|
|
def test_unsupported_wheel_local_file_requirement_raises(
|
|
self, data: TestData, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# GIVEN
|
|
resolver = make_test_resolver(monkeypatch, [])
|
|
requirement_set = RequirementSet(check_supported_wheels=True)
|
|
|
|
install_req = install_req_from_line(
|
|
os.fspath(data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl")),
|
|
)
|
|
assert install_req.link is not None
|
|
assert install_req.link.is_wheel
|
|
assert install_req.link.scheme == "file"
|
|
|
|
# WHEN / THEN
|
|
with pytest.raises(InstallationError):
|
|
resolver._add_requirement_to_set(requirement_set, install_req)
|
|
|
|
def test_exclusive_environment_markers(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Make sure excluding environment markers are handled correctly."""
|
|
# GIVEN
|
|
resolver = make_test_resolver(monkeypatch, [])
|
|
requirement_set = RequirementSet(check_supported_wheels=True)
|
|
|
|
eq36 = install_req_from_line("Django>=1.6.10,<1.7 ; python_version == '3.6'")
|
|
eq36.user_supplied = True
|
|
ne36 = install_req_from_line("Django>=1.6.10,<1.8 ; python_version != '3.6'")
|
|
ne36.user_supplied = True
|
|
|
|
# WHEN
|
|
resolver._add_requirement_to_set(requirement_set, eq36)
|
|
resolver._add_requirement_to_set(requirement_set, ne36)
|
|
|
|
# THEN
|
|
assert requirement_set.has_requirement("Django")
|
|
assert len(requirement_set.all_requirements) == 1
|
|
|
|
|
|
class TestCheckDistRequiresPython:
|
|
"""
|
|
Test _check_dist_requires_python().
|
|
"""
|
|
|
|
def test_compatible(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
"""
|
|
Test a Python version compatible with the dist's Requires-Python.
|
|
"""
|
|
caplog.set_level(logging.DEBUG)
|
|
dist = make_fake_dist(requires_python="== 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) -> None:
|
|
"""
|
|
Test a Python version incompatible with the dist's Requires-Python.
|
|
"""
|
|
dist = make_fake_dist(requires_python="== 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: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""
|
|
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(requires_python="== 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: pytest.LogCaptureFixture) -> None:
|
|
"""
|
|
Test a dist with Requires-Python None.
|
|
"""
|
|
caplog.set_level(logging.DEBUG)
|
|
dist = make_fake_dist()
|
|
# Make sure our test setup is correct.
|
|
assert dist.requires_python == SpecifierSet()
|
|
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: pytest.LogCaptureFixture) -> None:
|
|
"""
|
|
Test a dist with an invalid Requires-Python.
|
|
"""
|
|
caplog.set_level(logging.DEBUG)
|
|
dist = make_fake_dist(requires_python="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, metadata_name: str) -> None:
|
|
"""Test dist.metadata raises FileNotFoundError."""
|
|
|
|
class NotWorkingFakeDist(FakeDist):
|
|
@property
|
|
def metadata(self) -> email.message.Message:
|
|
raise FileNotFoundError(metadata_name)
|
|
|
|
dist = make_fake_dist(klass=NotWorkingFakeDist)
|
|
|
|
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)
|
|
)
|
|
|
|
|
|
class TestYankedWarning:
|
|
"""
|
|
Test _populate_link() emits warning if one or more candidates are yanked.
|
|
"""
|
|
|
|
def test_sort_best_candidate__has_non_yanked(
|
|
self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""
|
|
Test unyanked candidate preferred over yanked.
|
|
"""
|
|
# 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)
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate("2.0", yanked_reason="bad metadata #2"),
|
|
]
|
|
ireq = install_req_from_line("pkg")
|
|
|
|
resolver = 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: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""
|
|
Test all candidates yanked.
|
|
"""
|
|
# 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)
|
|
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 = 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.
|
|
("curly quote: \u2018", "curly quote: \u2018"),
|
|
],
|
|
)
|
|
def test_sort_best_candidate__yanked_reason(
|
|
self,
|
|
caplog: pytest.LogCaptureFixture,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
yanked_reason: str,
|
|
expected_reason: str,
|
|
) -> None:
|
|
"""
|
|
Test the log message with various reason strings.
|
|
"""
|
|
# 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)
|
|
candidates = [
|
|
make_mock_candidate("1.0", yanked_reason=yanked_reason),
|
|
]
|
|
ireq = install_req_from_line("pkg")
|
|
|
|
resolver = 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
|