mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
d61b539353
A new enum class is implemented for the link evaluator to use instead of a simple boolean to better distinguish between various evaluation errors. This allows the caller to better distinguish error sources with a structured check instead of fragile error string comparison.
912 lines
31 KiB
Python
912 lines
31 KiB
Python
import logging
|
|
from typing import FrozenSet, List, Optional, Set, Tuple
|
|
|
|
import pytest
|
|
from pip._vendor.packaging.specifiers import SpecifierSet
|
|
from pip._vendor.packaging.tags import Tag
|
|
|
|
from pip._internal.index.collector import LinkCollector
|
|
from pip._internal.index.package_finder import (
|
|
CandidateEvaluator,
|
|
CandidatePreferences,
|
|
FormatControl,
|
|
LinkEvaluator,
|
|
LinkType,
|
|
PackageFinder,
|
|
_check_link_requires_python,
|
|
_extract_version_from_fragment,
|
|
_find_name_version_sep,
|
|
filter_unallowed_hashes,
|
|
)
|
|
from pip._internal.models.link import Link
|
|
from pip._internal.models.search_scope import SearchScope
|
|
from pip._internal.models.selection_prefs import SelectionPreferences
|
|
from pip._internal.models.target_python import TargetPython
|
|
from pip._internal.network.session import PipSession
|
|
from pip._internal.utils.compatibility_tags import get_supported
|
|
from pip._internal.utils.hashes import Hashes
|
|
from tests.lib import CURRENT_PY_VERSION_INFO
|
|
from tests.lib.index import make_mock_candidate
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"requires_python, expected",
|
|
[
|
|
("== 3.6.4", False),
|
|
("== 3.6.5", True),
|
|
# Test an invalid Requires-Python value.
|
|
("invalid", True),
|
|
],
|
|
)
|
|
def test_check_link_requires_python(requires_python: str, expected: bool) -> None:
|
|
version_info = (3, 6, 5)
|
|
link = Link("https://example.com", requires_python=requires_python)
|
|
actual = _check_link_requires_python(link, version_info)
|
|
assert actual == expected
|
|
|
|
|
|
def check_caplog(
|
|
caplog: pytest.LogCaptureFixture, expected_level: str, expected_message: str
|
|
) -> None:
|
|
assert len(caplog.records) == 1
|
|
record = caplog.records[0]
|
|
assert record.levelname == expected_level
|
|
assert record.message == expected_message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ignore_requires_python, expected",
|
|
[
|
|
(
|
|
False,
|
|
(
|
|
False,
|
|
"VERBOSE",
|
|
"Link requires a different Python (3.6.5 not in: '== 3.6.4'): "
|
|
"https://example.com",
|
|
),
|
|
),
|
|
(
|
|
True,
|
|
(
|
|
True,
|
|
"DEBUG",
|
|
"Ignoring failed Requires-Python check (3.6.5 not in: '== 3.6.4') "
|
|
"for link: https://example.com",
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_check_link_requires_python__incompatible_python(
|
|
caplog: pytest.LogCaptureFixture,
|
|
ignore_requires_python: bool,
|
|
expected: Tuple[bool, str, str],
|
|
) -> None:
|
|
"""
|
|
Test an incompatible Python.
|
|
"""
|
|
expected_return, expected_level, expected_message = expected
|
|
link = Link("https://example.com", requires_python="== 3.6.4")
|
|
caplog.set_level(logging.DEBUG)
|
|
actual = _check_link_requires_python(
|
|
link,
|
|
version_info=(3, 6, 5),
|
|
ignore_requires_python=ignore_requires_python,
|
|
)
|
|
assert actual == expected_return
|
|
|
|
check_caplog(caplog, expected_level, expected_message)
|
|
|
|
|
|
def test_check_link_requires_python__invalid_requires(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""
|
|
Test the log message for an invalid Requires-Python.
|
|
"""
|
|
link = Link("https://example.com", requires_python="invalid")
|
|
caplog.set_level(logging.DEBUG)
|
|
actual = _check_link_requires_python(link, version_info=(3, 6, 5))
|
|
assert actual
|
|
|
|
expected_message = (
|
|
"Ignoring invalid Requires-Python ('invalid') for link: https://example.com"
|
|
)
|
|
check_caplog(caplog, "DEBUG", expected_message)
|
|
|
|
|
|
class TestLinkEvaluator:
|
|
@pytest.mark.parametrize(
|
|
"py_version_info, ignore_requires_python, expected",
|
|
[
|
|
pytest.param(
|
|
(3, 6, 5),
|
|
False,
|
|
(LinkType.candidate, "1.12"),
|
|
id="compatible",
|
|
),
|
|
pytest.param(
|
|
(3, 6, 4),
|
|
False,
|
|
(
|
|
LinkType.requires_python_mismatch,
|
|
"1.12 Requires-Python == 3.6.5",
|
|
),
|
|
id="requires-python-mismatch",
|
|
),
|
|
pytest.param(
|
|
(3, 6, 4),
|
|
True,
|
|
(LinkType.candidate, "1.12"),
|
|
id="requires-python-mismatch-ignored",
|
|
),
|
|
],
|
|
)
|
|
def test_evaluate_link(
|
|
self,
|
|
py_version_info: Tuple[int, int, int],
|
|
ignore_requires_python: bool,
|
|
expected: Tuple[LinkType, str],
|
|
) -> None:
|
|
target_python = TargetPython(py_version_info=py_version_info)
|
|
evaluator = LinkEvaluator(
|
|
project_name="twine",
|
|
canonical_name="twine",
|
|
formats=frozenset(["source"]),
|
|
target_python=target_python,
|
|
allow_yanked=True,
|
|
ignore_requires_python=ignore_requires_python,
|
|
)
|
|
link = Link(
|
|
"https://example.com/#egg=twine-1.12",
|
|
requires_python="== 3.6.5",
|
|
)
|
|
actual = evaluator.evaluate_link(link)
|
|
assert actual == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"yanked_reason, allow_yanked, expected",
|
|
[
|
|
(None, True, (LinkType.candidate, "1.12")),
|
|
(None, False, (LinkType.candidate, "1.12")),
|
|
("", True, (LinkType.candidate, "1.12")),
|
|
(
|
|
"",
|
|
False,
|
|
(LinkType.yanked, "yanked for reason: <none given>"),
|
|
),
|
|
("bad metadata", True, (LinkType.candidate, "1.12")),
|
|
(
|
|
"bad metadata",
|
|
False,
|
|
(LinkType.yanked, "yanked for reason: bad metadata"),
|
|
),
|
|
# Test a unicode string with a non-ascii character.
|
|
("curly quote: \u2018", True, (LinkType.candidate, "1.12")),
|
|
(
|
|
"curly quote: \u2018",
|
|
False,
|
|
(
|
|
LinkType.yanked,
|
|
"yanked for reason: curly quote: \u2018",
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_evaluate_link__allow_yanked(
|
|
self,
|
|
yanked_reason: str,
|
|
allow_yanked: bool,
|
|
expected: Tuple[LinkType, str],
|
|
) -> None:
|
|
target_python = TargetPython(py_version_info=(3, 6, 4))
|
|
evaluator = LinkEvaluator(
|
|
project_name="twine",
|
|
canonical_name="twine",
|
|
formats=frozenset(["source"]),
|
|
target_python=target_python,
|
|
allow_yanked=allow_yanked,
|
|
)
|
|
link = Link(
|
|
"https://example.com/#egg=twine-1.12",
|
|
yanked_reason=yanked_reason,
|
|
)
|
|
actual = evaluator.evaluate_link(link)
|
|
assert actual == expected
|
|
|
|
def test_evaluate_link__incompatible_wheel(self) -> None:
|
|
"""
|
|
Test an incompatible wheel.
|
|
"""
|
|
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 = LinkEvaluator(
|
|
project_name="sample",
|
|
canonical_name="sample",
|
|
formats=frozenset(["binary"]),
|
|
target_python=target_python,
|
|
allow_yanked=True,
|
|
)
|
|
link = Link("https://example.com/sample-1.0-py2.py3-none-any.whl")
|
|
actual = evaluator.evaluate_link(link)
|
|
expected = (
|
|
LinkType.platform_mismatch,
|
|
"none of the wheel's tags (py2-none-any, py3-none-any) are compatible "
|
|
"(run pip debug --verbose to show compatible tags)",
|
|
)
|
|
assert actual == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"hex_digest, expected_versions",
|
|
[
|
|
(64 * "a", ["1.0", "1.1"]),
|
|
(64 * "b", ["1.0", "1.2"]),
|
|
(64 * "c", ["1.0", "1.1", "1.2"]),
|
|
],
|
|
)
|
|
def test_filter_unallowed_hashes(hex_digest: str, expected_versions: List[str]) -> None:
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate("1.1", hex_digest=(64 * "a")),
|
|
make_mock_candidate("1.2", hex_digest=(64 * "b")),
|
|
]
|
|
hashes_data = {
|
|
"sha256": [hex_digest],
|
|
}
|
|
hashes = Hashes(hashes_data)
|
|
actual = filter_unallowed_hashes(
|
|
candidates,
|
|
hashes=hashes,
|
|
project_name="my-project",
|
|
)
|
|
|
|
actual_versions = [str(candidate.version) for candidate in actual]
|
|
assert actual_versions == expected_versions
|
|
# Check that the return value is always different from the given value.
|
|
assert actual is not candidates
|
|
|
|
|
|
def test_filter_unallowed_hashes__no_hashes(caplog: pytest.LogCaptureFixture) -> None:
|
|
caplog.set_level(logging.DEBUG)
|
|
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate("1.1"),
|
|
]
|
|
actual = filter_unallowed_hashes(
|
|
candidates,
|
|
hashes=Hashes(),
|
|
project_name="my-project",
|
|
)
|
|
|
|
# Check that the return value is a copy.
|
|
assert actual == candidates
|
|
assert actual is not candidates
|
|
|
|
expected_message = (
|
|
"Given no hashes to check 2 links for project 'my-project': "
|
|
"discarding no candidates"
|
|
)
|
|
check_caplog(caplog, "DEBUG", expected_message)
|
|
|
|
|
|
def test_filter_unallowed_hashes__log_message_with_match(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
caplog.set_level(logging.DEBUG)
|
|
|
|
# Test 1 match, 2 non-matches, 3 no hashes so all 3 values will be
|
|
# different.
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate(
|
|
"1.1",
|
|
),
|
|
make_mock_candidate(
|
|
"1.2",
|
|
),
|
|
make_mock_candidate("1.3", hex_digest=(64 * "a")),
|
|
make_mock_candidate("1.4", hex_digest=(64 * "b")),
|
|
make_mock_candidate("1.5", hex_digest=(64 * "c")),
|
|
]
|
|
hashes_data = {
|
|
"sha256": [64 * "a", 64 * "d"],
|
|
}
|
|
hashes = Hashes(hashes_data)
|
|
actual = filter_unallowed_hashes(
|
|
candidates,
|
|
hashes=hashes,
|
|
project_name="my-project",
|
|
)
|
|
assert len(actual) == 4
|
|
|
|
expected_message = (
|
|
"Checked 6 links for project 'my-project' against 2 hashes "
|
|
"(1 matches, 3 no digest): discarding 2 non-matches:\n"
|
|
" https://example.com/pkg-1.4.tar.gz#sha256="
|
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n"
|
|
" https://example.com/pkg-1.5.tar.gz#sha256="
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
|
)
|
|
check_caplog(caplog, "DEBUG", expected_message)
|
|
|
|
|
|
def test_filter_unallowed_hashes__log_message_with_no_match(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
caplog.set_level(logging.DEBUG)
|
|
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate("1.1", hex_digest=(64 * "b")),
|
|
make_mock_candidate("1.2", hex_digest=(64 * "c")),
|
|
]
|
|
hashes_data = {
|
|
"sha256": [64 * "a", 64 * "d"],
|
|
}
|
|
hashes = Hashes(hashes_data)
|
|
actual = filter_unallowed_hashes(
|
|
candidates,
|
|
hashes=hashes,
|
|
project_name="my-project",
|
|
)
|
|
assert len(actual) == 3
|
|
|
|
expected_message = (
|
|
"Checked 3 links for project 'my-project' against 2 hashes "
|
|
"(0 matches, 1 no digest): discarding no candidates"
|
|
)
|
|
check_caplog(caplog, "DEBUG", expected_message)
|
|
|
|
|
|
class TestCandidateEvaluator:
|
|
@pytest.mark.parametrize(
|
|
"allow_all_prereleases, prefer_binary",
|
|
[
|
|
(False, False),
|
|
(False, True),
|
|
(True, False),
|
|
(True, True),
|
|
],
|
|
)
|
|
def test_create(self, allow_all_prereleases: bool, prefer_binary: bool) -> None:
|
|
target_python = TargetPython()
|
|
target_python._valid_tags = [Tag("py36", "none", "any")]
|
|
specifier = SpecifierSet()
|
|
evaluator = CandidateEvaluator.create(
|
|
project_name="my-project",
|
|
target_python=target_python,
|
|
allow_all_prereleases=allow_all_prereleases,
|
|
prefer_binary=prefer_binary,
|
|
specifier=specifier,
|
|
)
|
|
assert evaluator._allow_all_prereleases == allow_all_prereleases
|
|
assert evaluator._prefer_binary == prefer_binary
|
|
assert evaluator._specifier is specifier
|
|
assert evaluator._supported_tags == [Tag("py36", "none", "any")]
|
|
|
|
def test_create__target_python_none(self) -> None:
|
|
"""
|
|
Test passing target_python=None.
|
|
"""
|
|
evaluator = CandidateEvaluator.create("my-project")
|
|
expected_tags = get_supported()
|
|
assert evaluator._supported_tags == expected_tags
|
|
|
|
def test_create__specifier_none(self) -> None:
|
|
"""
|
|
Test passing specifier=None.
|
|
"""
|
|
evaluator = CandidateEvaluator.create("my-project")
|
|
expected_specifier = SpecifierSet()
|
|
assert evaluator._specifier == expected_specifier
|
|
|
|
def test_get_applicable_candidates(self) -> None:
|
|
specifier = SpecifierSet("<= 1.11")
|
|
versions = ["1.10", "1.11", "1.12"]
|
|
candidates = [make_mock_candidate(version) for version in versions]
|
|
evaluator = CandidateEvaluator.create(
|
|
"my-project",
|
|
specifier=specifier,
|
|
)
|
|
actual = evaluator.get_applicable_candidates(candidates)
|
|
expected_applicable = candidates[:2]
|
|
assert [str(c.version) for c in expected_applicable] == [
|
|
"1.10",
|
|
"1.11",
|
|
]
|
|
assert actual == expected_applicable
|
|
|
|
@pytest.mark.parametrize(
|
|
"specifier, expected_versions",
|
|
[
|
|
# Test no version constraint.
|
|
(SpecifierSet(), ["1.0", "1.2"]),
|
|
# Test a version constraint that excludes the candidate whose
|
|
# hash matches. Then the non-allowed hash is a candidate.
|
|
(SpecifierSet("<= 1.1"), ["1.0", "1.1"]),
|
|
],
|
|
)
|
|
def test_get_applicable_candidates__hashes(
|
|
self,
|
|
specifier: SpecifierSet,
|
|
expected_versions: List[str],
|
|
) -> None:
|
|
"""
|
|
Test a non-None hashes value.
|
|
"""
|
|
candidates = [
|
|
make_mock_candidate("1.0"),
|
|
make_mock_candidate("1.1", hex_digest=(64 * "a")),
|
|
make_mock_candidate("1.2", hex_digest=(64 * "b")),
|
|
]
|
|
hashes_data = {
|
|
"sha256": [64 * "b"],
|
|
}
|
|
hashes = Hashes(hashes_data)
|
|
evaluator = CandidateEvaluator.create(
|
|
"my-project",
|
|
specifier=specifier,
|
|
hashes=hashes,
|
|
)
|
|
actual = evaluator.get_applicable_candidates(candidates)
|
|
actual_versions = [str(c.version) for c in actual]
|
|
assert actual_versions == expected_versions
|
|
|
|
def test_compute_best_candidate(self) -> None:
|
|
specifier = SpecifierSet("<= 1.11")
|
|
versions = ["1.10", "1.11", "1.12"]
|
|
candidates = [make_mock_candidate(version) for version in versions]
|
|
evaluator = CandidateEvaluator.create(
|
|
"my-project",
|
|
specifier=specifier,
|
|
)
|
|
result = evaluator.compute_best_candidate(candidates)
|
|
|
|
assert result._candidates == candidates
|
|
expected_applicable = candidates[:2]
|
|
assert [str(c.version) for c in expected_applicable] == [
|
|
"1.10",
|
|
"1.11",
|
|
]
|
|
assert result._applicable_candidates == expected_applicable
|
|
|
|
assert result.best_candidate is expected_applicable[1]
|
|
|
|
def test_compute_best_candidate__none_best(self) -> None:
|
|
"""
|
|
Test returning a None best candidate.
|
|
"""
|
|
specifier = SpecifierSet("<= 1.10")
|
|
versions = ["1.11", "1.12"]
|
|
candidates = [make_mock_candidate(version) for version in versions]
|
|
evaluator = CandidateEvaluator.create(
|
|
"my-project",
|
|
specifier=specifier,
|
|
)
|
|
result = evaluator.compute_best_candidate(candidates)
|
|
|
|
assert result._candidates == candidates
|
|
assert result._applicable_candidates == []
|
|
assert result.best_candidate is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"hex_digest, expected",
|
|
[
|
|
# Test a link with no hash.
|
|
(None, 0),
|
|
# Test a link with an allowed hash.
|
|
(64 * "a", 1),
|
|
# Test a link with a hash that isn't allowed.
|
|
(64 * "b", 0),
|
|
],
|
|
)
|
|
def test_sort_key__hash(self, hex_digest: Optional[str], expected: int) -> None:
|
|
"""
|
|
Test the effect of the link's hash on _sort_key()'s return value.
|
|
"""
|
|
candidate = make_mock_candidate("1.0", hex_digest=hex_digest)
|
|
hashes_data = {
|
|
"sha256": [64 * "a"],
|
|
}
|
|
hashes = Hashes(hashes_data)
|
|
evaluator = CandidateEvaluator.create("my-project", hashes=hashes)
|
|
sort_value = evaluator._sort_key(candidate)
|
|
# The hash is reflected in the first element of the tuple.
|
|
actual = sort_value[0]
|
|
assert actual == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
"yanked_reason, expected",
|
|
[
|
|
# Test a non-yanked file.
|
|
(None, 0),
|
|
# Test a yanked file (has a lower value than non-yanked).
|
|
("bad metadata", -1),
|
|
],
|
|
)
|
|
def test_sort_key__is_yanked(
|
|
self, yanked_reason: Optional[str], expected: int
|
|
) -> None:
|
|
"""
|
|
Test the effect of is_yanked on _sort_key()'s return value.
|
|
"""
|
|
candidate = make_mock_candidate("1.0", yanked_reason=yanked_reason)
|
|
evaluator = CandidateEvaluator.create("my-project")
|
|
sort_value = evaluator._sort_key(candidate)
|
|
# Yanked / non-yanked is reflected in the second element of the tuple.
|
|
actual = sort_value[1]
|
|
assert actual == expected
|
|
|
|
def test_sort_best_candidate__no_candidates(self) -> None:
|
|
"""
|
|
Test passing an empty list.
|
|
"""
|
|
evaluator = CandidateEvaluator.create("my-project")
|
|
actual = evaluator.sort_best_candidate([])
|
|
assert actual is None
|
|
|
|
def test_sort_best_candidate__best_yanked_but_not_all(
|
|
self,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""
|
|
Test the best candidates being yanked, but not all.
|
|
"""
|
|
caplog.set_level(logging.INFO)
|
|
candidates = [
|
|
make_mock_candidate("4.0", yanked_reason="bad metadata #4"),
|
|
# Put the best candidate in the middle, to test sorting.
|
|
make_mock_candidate("2.0"),
|
|
make_mock_candidate("3.0", yanked_reason="bad metadata #3"),
|
|
make_mock_candidate("1.0"),
|
|
]
|
|
expected_best = candidates[1]
|
|
evaluator = CandidateEvaluator.create("my-project")
|
|
actual = evaluator.sort_best_candidate(candidates)
|
|
assert actual is expected_best
|
|
assert str(actual.version) == "2.0"
|
|
|
|
# Check the log messages.
|
|
assert len(caplog.records) == 0
|
|
|
|
|
|
class TestPackageFinder:
|
|
@pytest.mark.parametrize(
|
|
"allow_all_prereleases, prefer_binary",
|
|
[
|
|
(False, False),
|
|
(False, True),
|
|
(True, False),
|
|
(True, True),
|
|
],
|
|
)
|
|
def test_create__candidate_prefs(
|
|
self,
|
|
allow_all_prereleases: bool,
|
|
prefer_binary: bool,
|
|
) -> None:
|
|
"""
|
|
Test that the _candidate_prefs attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
selection_prefs = SelectionPreferences(
|
|
allow_yanked=True,
|
|
allow_all_prereleases=allow_all_prereleases,
|
|
prefer_binary=prefer_binary,
|
|
)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=selection_prefs,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
candidate_prefs = finder._candidate_prefs
|
|
assert candidate_prefs.allow_all_prereleases == allow_all_prereleases
|
|
assert candidate_prefs.prefer_binary == prefer_binary
|
|
|
|
def test_create__link_collector(self) -> None:
|
|
"""
|
|
Test that the _link_collector attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=SelectionPreferences(allow_yanked=True),
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
|
|
assert finder._link_collector is link_collector
|
|
|
|
def test_create__target_python(self) -> None:
|
|
"""
|
|
Test that the _target_python attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
target_python = TargetPython(py_version_info=(3, 7, 3))
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=SelectionPreferences(allow_yanked=True),
|
|
target_python=target_python,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
actual_target_python = finder._target_python
|
|
# The target_python attribute should be set as is.
|
|
assert actual_target_python is target_python
|
|
# Check that the attributes weren't reset.
|
|
assert actual_target_python.py_version_info == (3, 7, 3)
|
|
|
|
def test_create__target_python_none(self) -> None:
|
|
"""
|
|
Test passing target_python=None.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=SelectionPreferences(allow_yanked=True),
|
|
target_python=None,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
# Spot-check the default TargetPython object.
|
|
actual_target_python = finder._target_python
|
|
assert actual_target_python._given_py_version_info is None
|
|
assert actual_target_python.py_version_info == CURRENT_PY_VERSION_INFO
|
|
|
|
@pytest.mark.parametrize("allow_yanked", [False, True])
|
|
def test_create__allow_yanked(self, allow_yanked: bool) -> None:
|
|
"""
|
|
Test that the _allow_yanked attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
selection_prefs = SelectionPreferences(allow_yanked=allow_yanked)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=selection_prefs,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
assert finder._allow_yanked == allow_yanked
|
|
|
|
@pytest.mark.parametrize("ignore_requires_python", [False, True])
|
|
def test_create__ignore_requires_python(self, ignore_requires_python: bool) -> None:
|
|
"""
|
|
Test that the _ignore_requires_python attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
selection_prefs = SelectionPreferences(
|
|
allow_yanked=True,
|
|
ignore_requires_python=ignore_requires_python,
|
|
)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=selection_prefs,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
assert finder._ignore_requires_python == ignore_requires_python
|
|
|
|
def test_create__format_control(self) -> None:
|
|
"""
|
|
Test that the format_control attribute is set correctly.
|
|
"""
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
format_control = FormatControl(set(), {":all:"})
|
|
selection_prefs = SelectionPreferences(
|
|
allow_yanked=True,
|
|
format_control=format_control,
|
|
)
|
|
finder = PackageFinder.create(
|
|
link_collector=link_collector,
|
|
selection_prefs=selection_prefs,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
actual_format_control = finder.format_control
|
|
assert actual_format_control is format_control
|
|
# Check that the attributes weren't reset.
|
|
assert actual_format_control.only_binary == {":all:"}
|
|
|
|
@pytest.mark.parametrize(
|
|
"allow_yanked, ignore_requires_python, only_binary, expected_formats",
|
|
[
|
|
(False, False, {}, frozenset({"binary", "source"})),
|
|
# Test allow_yanked=True.
|
|
(True, False, {}, frozenset({"binary", "source"})),
|
|
# Test ignore_requires_python=True.
|
|
(False, True, {}, frozenset({"binary", "source"})),
|
|
# Test a non-trivial only_binary.
|
|
(False, False, {"twine"}, frozenset({"binary"})),
|
|
],
|
|
)
|
|
def test_make_link_evaluator(
|
|
self,
|
|
allow_yanked: bool,
|
|
ignore_requires_python: bool,
|
|
only_binary: Set[str],
|
|
expected_formats: FrozenSet[str],
|
|
) -> None:
|
|
# Create a test TargetPython that we can check for.
|
|
target_python = TargetPython(py_version_info=(3, 7))
|
|
format_control = FormatControl(set(), only_binary)
|
|
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
|
|
finder = PackageFinder(
|
|
link_collector=link_collector,
|
|
target_python=target_python,
|
|
allow_yanked=allow_yanked,
|
|
format_control=format_control,
|
|
ignore_requires_python=ignore_requires_python,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
|
|
# Pass a project_name that will be different from canonical_name.
|
|
link_evaluator = finder.make_link_evaluator("Twine")
|
|
|
|
assert link_evaluator.project_name == "Twine"
|
|
assert link_evaluator._canonical_name == "twine"
|
|
assert link_evaluator._allow_yanked == allow_yanked
|
|
assert link_evaluator._ignore_requires_python == ignore_requires_python
|
|
assert link_evaluator._formats == expected_formats
|
|
|
|
# Test the _target_python attribute.
|
|
actual_target_python = link_evaluator._target_python
|
|
# The target_python attribute should be set as is.
|
|
assert actual_target_python is target_python
|
|
# For good measure, check that the attributes weren't reset.
|
|
assert actual_target_python._given_py_version_info == (3, 7)
|
|
assert actual_target_python.py_version_info == (3, 7, 0)
|
|
|
|
@pytest.mark.parametrize(
|
|
"allow_all_prereleases, prefer_binary",
|
|
[
|
|
(False, False),
|
|
(False, True),
|
|
(True, False),
|
|
(True, True),
|
|
],
|
|
)
|
|
def test_make_candidate_evaluator(
|
|
self,
|
|
allow_all_prereleases: bool,
|
|
prefer_binary: bool,
|
|
) -> None:
|
|
target_python = TargetPython()
|
|
target_python._valid_tags = [Tag("py36", "none", "any")]
|
|
candidate_prefs = CandidatePreferences(
|
|
prefer_binary=prefer_binary,
|
|
allow_all_prereleases=allow_all_prereleases,
|
|
)
|
|
link_collector = LinkCollector(
|
|
session=PipSession(),
|
|
search_scope=SearchScope([], []),
|
|
)
|
|
finder = PackageFinder(
|
|
link_collector=link_collector,
|
|
target_python=target_python,
|
|
allow_yanked=True,
|
|
candidate_prefs=candidate_prefs,
|
|
use_deprecated_html5lib=False,
|
|
)
|
|
|
|
specifier = SpecifierSet()
|
|
# Pass hashes to check that _hashes is set.
|
|
hashes = Hashes({"sha256": [64 * "a"]})
|
|
evaluator = finder.make_candidate_evaluator(
|
|
"my-project",
|
|
specifier=specifier,
|
|
hashes=hashes,
|
|
)
|
|
assert evaluator._allow_all_prereleases == allow_all_prereleases
|
|
assert evaluator._hashes == hashes
|
|
assert evaluator._prefer_binary == prefer_binary
|
|
assert evaluator._project_name == "my-project"
|
|
assert evaluator._specifier is specifier
|
|
assert evaluator._supported_tags == [Tag("py36", "none", "any")]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("fragment", "canonical_name", "expected"),
|
|
[
|
|
# Trivial.
|
|
("pip-18.0", "pip", 3),
|
|
("zope-interface-4.5.0", "zope-interface", 14),
|
|
# Canonicalized name match non-canonicalized egg info. (pypa/pip#5870)
|
|
("Jinja2-2.10", "jinja2", 6),
|
|
("zope.interface-4.5.0", "zope-interface", 14),
|
|
("zope_interface-4.5.0", "zope-interface", 14),
|
|
# Should be smart enough to parse ambiguous names from the provided
|
|
# package name.
|
|
("foo-2-2", "foo", 3),
|
|
("foo-2-2", "foo-2", 5),
|
|
# Should be able to detect collapsed characters in the egg info.
|
|
("foo--bar-1.0", "foo-bar", 8),
|
|
("foo-_bar-1.0", "foo-bar", 8),
|
|
# The package name must not ends with a dash (PEP 508), so the first
|
|
# dash would be the separator, not the second.
|
|
("zope.interface--4.5.0", "zope-interface", 14),
|
|
("zope.interface--", "zope-interface", 14),
|
|
# The version part is missing, but the split function does not care.
|
|
("zope.interface-", "zope-interface", 14),
|
|
],
|
|
)
|
|
def test_find_name_version_sep(
|
|
fragment: str, canonical_name: str, expected: int
|
|
) -> None:
|
|
index = _find_name_version_sep(fragment, canonical_name)
|
|
assert index == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("fragment", "canonical_name"),
|
|
[
|
|
# A dash must follow the package name.
|
|
("zope.interface4.5.0", "zope-interface"),
|
|
("zope.interface.4.5.0", "zope-interface"),
|
|
("zope.interface.-4.5.0", "zope-interface"),
|
|
("zope.interface", "zope-interface"),
|
|
],
|
|
)
|
|
def test_find_name_version_sep_failure(fragment: str, canonical_name: str) -> None:
|
|
with pytest.raises(ValueError) as ctx:
|
|
_find_name_version_sep(fragment, canonical_name)
|
|
message = f"{fragment} does not match {canonical_name}"
|
|
assert str(ctx.value) == message
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("fragment", "canonical_name", "expected"),
|
|
[
|
|
# Trivial.
|
|
("pip-18.0", "pip", "18.0"),
|
|
("zope-interface-4.5.0", "zope-interface", "4.5.0"),
|
|
# Canonicalized name match non-canonicalized egg info. (pypa/pip#5870)
|
|
("Jinja2-2.10", "jinja2", "2.10"),
|
|
("zope.interface-4.5.0", "zope-interface", "4.5.0"),
|
|
("zope_interface-4.5.0", "zope-interface", "4.5.0"),
|
|
# Should be smart enough to parse ambiguous names from the provided
|
|
# package name.
|
|
("foo-2-2", "foo", "2-2"),
|
|
("foo-2-2", "foo-2", "2"),
|
|
("zope.interface--4.5.0", "zope-interface", "-4.5.0"),
|
|
("zope.interface--", "zope-interface", "-"),
|
|
# Should be able to detect collapsed characters in the egg info.
|
|
("foo--bar-1.0", "foo-bar", "1.0"),
|
|
("foo-_bar-1.0", "foo-bar", "1.0"),
|
|
# Invalid.
|
|
("the-package-name-8.19", "does-not-match", None),
|
|
("zope.interface.-4.5.0", "zope.interface", None),
|
|
("zope.interface-", "zope-interface", None),
|
|
("zope.interface4.5.0", "zope-interface", None),
|
|
("zope.interface.4.5.0", "zope-interface", None),
|
|
("zope.interface.-4.5.0", "zope-interface", None),
|
|
("zope.interface", "zope-interface", None),
|
|
],
|
|
)
|
|
def test_extract_version_from_fragment(
|
|
fragment: str, canonical_name: str, expected: Optional[str]
|
|
) -> None:
|
|
version = _extract_version_from_fragment(fragment, canonical_name)
|
|
assert version == expected
|