summaryrefslogtreecommitdiff
path: root/tests/unit/test_resolution_legacy_resolver.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/test_resolution_legacy_resolver.py')
-rw-r--r--tests/unit/test_resolution_legacy_resolver.py297
1 files changed, 191 insertions, 106 deletions
diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py
index 236a4c624..8b9d1a58a 100644
--- a/tests/unit/test_resolution_legacy_resolver.py
+++ b/tests/unit/test_resolution_legacy_resolver.py
@@ -1,69 +1,159 @@
+import email.message
import logging
+import os
+from typing import List, Optional, Type, TypeVar, cast
from unittest import mock
import pytest
-from pip._vendor import pkg_resources
+from pip._vendor.packaging.specifiers import SpecifierSet
+from pip._vendor.packaging.utils import NormalizedName
-from pip._internal.exceptions import NoneMetadataError, UnsupportedPythonVersion
+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 pip._internal.utils.packaging import get_requires_python
-from tests.lib import make_test_finder
+from tests.lib import TestData, make_test_finder
from tests.lib.index import make_mock_candidate
+T = TypeVar("T")
-# 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
+class FakeDist(BaseDistribution):
+ def __init__(self, metadata: email.message.Message) -> None:
+ self._canonical_name = cast(NormalizedName, "my-project")
+ self._metadata = metadata
- def __str__(self):
- return f'<distribution {self.project_name!r}>'
+ def __str__(self) -> str:
+ return f"<distribution {self.canonical_name!r}>"
- def has_metadata(self, name):
- return (name == self.metadata_name)
+ @property
+ def canonical_name(self) -> NormalizedName:
+ return self._canonical_name
- def get_metadata(self, name):
- assert name == self.metadata_name
- return self.metadata
+ @property
+ def metadata(self) -> email.message.Message:
+ return self._metadata
-def make_fake_dist(requires_python=None, metadata_name=None):
- metadata = 'Name: test\n'
+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 += f'Requires-Python:{requires_python}'
+ metadata["Requires-Python"] = requires_python
- return FakeDist(metadata, metadata_name=metadata_name)
+ # Too many arguments for "BaseDistribution"
+ return klass(metadata) # type: ignore[call-arg]
-class TestCheckDistRequiresPython:
+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):
+ 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('== 3.6.5')
+ dist = make_fake_dist(requires_python="== 3.6.5")
_check_dist_requires_python(
dist,
@@ -72,11 +162,11 @@ class TestCheckDistRequiresPython:
)
assert not len(caplog.records)
- def test_incompatible(self):
+ def test_incompatible(self) -> None:
"""
Test a Python version incompatible with the dist's Requires-Python.
"""
- dist = make_fake_dist('== 3.6.4')
+ dist = make_fake_dist(requires_python="== 3.6.4")
with pytest.raises(UnsupportedPythonVersion) as exc:
_check_dist_requires_python(
dist,
@@ -85,16 +175,18 @@ class TestCheckDistRequiresPython:
)
assert str(exc.value) == (
"Package 'my-project' requires a different Python: "
- "3.6.5 not in '== 3.6.4'"
+ "3.6.5 not in '==3.6.4'"
)
- def test_incompatible_with_ignore_requires(self, caplog):
+ 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('== 3.6.4')
+ dist = make_fake_dist(requires_python="== 3.6.4")
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
@@ -102,20 +194,20 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'DEBUG'
+ assert record.levelname == "DEBUG"
assert record.message == (
"Ignoring failed Requires-Python check for package 'my-project': "
- "3.6.5 not in '== 3.6.4'"
+ "3.6.5 not in '==3.6.4'"
)
- def test_none_requires_python(self, caplog):
+ 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 get_requires_python(dist) is None
+ assert dist.requires_python == SpecifierSet()
assert len(caplog.records) == 0
# Then there is no exception and no log message.
@@ -126,12 +218,12 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 0
- def test_invalid_requires_python(self, caplog):
+ 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('invalid')
+ dist = make_fake_dist(requires_python="invalid")
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
@@ -139,27 +231,28 @@ class TestCheckDistRequiresPython:
)
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ 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
+ @pytest.mark.parametrize(
+ "metadata_name",
+ [
+ "METADATA",
+ "PKG-INFO",
+ ],
+ )
+ def test_empty_metadata_error(self, metadata_name: str) -> None:
+ """Test dist.metadata raises FileNotFoundError."""
- # Make sure our test setup is correct.
- assert dist.has_metadata(metadata_name)
- assert dist.get_metadata(metadata_name) is None
+ 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(
@@ -177,27 +270,10 @@ class TestYankedWarning:
"""
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):
+ def test_sort_best_candidate__has_non_yanked(
+ self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test unyanked candidate preferred over yanked.
"""
@@ -206,18 +282,20 @@ class TestYankedWarning:
# 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'),
+ 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 = 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):
+ def test_sort_best_candidate__all_yanked(
+ self, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
"""
Test all candidates yanked.
"""
@@ -226,14 +304,14 @@ class TestYankedWarning:
# 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'),
+ 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'),
+ 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 = make_test_resolver(monkeypatch, candidates)
resolver._populate_link(ireq)
assert ireq.link == candidates[1].link
@@ -241,23 +319,30 @@ class TestYankedWarning:
# Check the log messages.
assert len(caplog.records) == 1
record = caplog.records[0]
- assert record.levelname == 'WARNING'
+ assert record.levelname == "WARNING"
assert record.message == (
- 'The candidate selected for download or install is a yanked '
+ "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'
+ "(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'),
- ])
+ @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, monkeypatch, yanked_reason, expected_reason,
- ):
+ self,
+ caplog: pytest.LogCaptureFixture,
+ monkeypatch: pytest.MonkeyPatch,
+ yanked_reason: str,
+ expected_reason: str,
+ ) -> None:
"""
Test the log message with various reason strings.
"""
@@ -266,22 +351,22 @@ class TestYankedWarning:
# 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),
+ make_mock_candidate("1.0", yanked_reason=yanked_reason),
]
ireq = install_req_from_line("pkg")
- resolver = self._make_test_resolver(monkeypatch, candidates)
+ 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'
+ assert record.levelname == "WARNING"
expected_message = (
- 'The candidate selected for download or install is a yanked '
+ "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: '
+ "(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n"
+ "Reason for being yanked: "
) + expected_reason
assert record.message == expected_message