pip/tests/functional/test_install_metadata.py

237 lines
7.4 KiB
Python

import json
import re
from pathlib import Path
from typing import Any, Callable, Dict, Iterator, List, Tuple
import pytest
from pip._vendor.packaging.requirements import Requirement
from pip._internal.models.direct_url import DirectUrl
from pip._internal.utils.urls import path_to_url
from tests.lib import (
PipTestEnvironment,
TestPipResult,
)
@pytest.fixture(scope="function")
def install_with_generated_html_index(
script: PipTestEnvironment,
html_index_for_packages: Path,
tmpdir: Path,
) -> Callable[..., Tuple[TestPipResult, Dict[str, Any]]]:
"""Execute `pip download` against a generated PyPI index."""
output_file = tmpdir / "output_file.json"
def run_for_generated_index(
args: List[str],
*,
dry_run: bool = True,
allow_error: bool = False,
) -> Tuple[TestPipResult, Dict[str, Any]]:
"""
Produce a PyPI directory structure pointing to the specified packages, then
execute `pip install --report ... -i ...` pointing to our generated index.
"""
pip_args = [
"install",
*(("--dry-run",) if dry_run else ()),
"--ignore-installed",
"--report",
str(output_file),
"-i",
path_to_url(str(html_index_for_packages)),
*args,
]
result = script.pip(*pip_args, allow_error=allow_error)
try:
with open(output_file, "rb") as f:
report = json.load(f)
except FileNotFoundError:
if allow_error:
report = {}
else:
raise
return (result, report)
return run_for_generated_index
def iter_dists(report: Dict[str, Any]) -> Iterator[Tuple[Requirement, DirectUrl]]:
"""Parse a (req,url) tuple from each installed dist in the --report json."""
for inst in report["install"]:
metadata = inst["metadata"]
name = metadata["name"]
version = metadata["version"]
req = Requirement(f"{name}=={version}")
direct_url = DirectUrl.from_dict(inst["download_info"])
yield (req, direct_url)
@pytest.mark.parametrize(
"requirement_to_install, expected_outputs",
[
("simple2==1.0", ["simple2==1.0", "simple==1.0"]),
("simple==2.0", ["simple==2.0"]),
(
"colander",
["colander==0.9.9", "translationstring==1.1"],
),
(
"compilewheel",
["compilewheel==1.0", "simple==1.0"],
),
],
)
def test_install_with_metadata(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
requirement_to_install: str,
expected_outputs: List[str],
) -> None:
"""Verify that if a data-dist-info-metadata attribute is present, then it is used
instead of the actual dist's METADATA."""
_, report = install_with_generated_html_index(
[requirement_to_install],
)
installed = sorted(str(r) for r, _ in iter_dists(report))
assert installed == expected_outputs
@pytest.mark.parametrize(
"requirement_to_install, real_hash",
[
(
"simple==3.0",
"95e0f200b6302989bcf2cead9465cf229168295ea330ca30d1ffeab5c0fed996",
),
(
"has-script",
"16ba92d7f6f992f6de5ecb7d58c914675cf21f57f8e674fb29dcb4f4c9507e5b",
),
],
)
def test_incorrect_metadata_hash(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
requirement_to_install: str,
real_hash: str,
) -> None:
"""Verify that if a hash for data-dist-info-metadata is provided, it must match the
actual hash of the metadata file."""
result, _ = install_with_generated_html_index(
[requirement_to_install],
allow_error=True,
)
assert result.returncode != 0
expected_msg = f"""\
Expected sha256 WRONG-HASH
Got {real_hash}"""
assert expected_msg in result.stderr
@pytest.mark.parametrize(
"requirement_to_install, expected_url",
[
("simple2==2.0", "simple2-2.0.tar.gz.metadata"),
("priority", "priority-1.0-py2.py3-none-any.whl.metadata"),
],
)
def test_metadata_not_found(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
requirement_to_install: str,
expected_url: str,
) -> None:
"""Verify that if a data-dist-info-metadata attribute is provided, that pip will
fetch the .metadata file at the location specified by PEP 658, and error
if unavailable."""
result, _ = install_with_generated_html_index(
[requirement_to_install],
allow_error=True,
)
assert result.returncode != 0
expected_re = re.escape(expected_url)
pattern = re.compile(
f"ERROR: 404 Client Error: FileNotFoundError for url:.*{expected_re}"
)
assert pattern.search(result.stderr), (pattern, result.stderr)
def test_produces_error_for_mismatched_package_name_in_metadata(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
) -> None:
"""Verify that the package name from the metadata matches the requested package."""
result, _ = install_with_generated_html_index(
["simple2==3.0"],
allow_error=True,
)
assert result.returncode != 0
assert (
"simple2-3.0.tar.gz has inconsistent Name: expected 'simple2', but metadata "
"has 'not-simple2'"
) in result.stdout
@pytest.mark.parametrize(
"requirement",
(
"requires-simple-extra==0.1",
"REQUIRES_SIMPLE-EXTRA==0.1",
"REQUIRES....simple-_-EXTRA==0.1",
),
)
def test_canonicalizes_package_name_before_verifying_metadata(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
requirement: str,
) -> None:
"""Verify that the package name from the command line and the package's
METADATA are both canonicalized before comparison, while the name from the METADATA
is always used verbatim to represent the installed candidate in --report.
Regression test for https://github.com/pypa/pip/issues/12038
"""
_, report = install_with_generated_html_index(
[requirement],
)
reqs = [str(r) for r, _ in iter_dists(report)]
assert reqs == ["Requires_Simple.Extra==0.1"]
@pytest.mark.parametrize(
"requirement,err_string",
(
# It's important that we verify pip won't even attempt to fetch the file, so we
# construct an input that will cause it to error if it tries at all.
("complex-dist==0.1", "404 Client Error: FileNotFoundError"),
("corruptwheel==1.0", ".whl is invalid."),
),
)
def test_dry_run_avoids_downloading_metadata_only_dists(
install_with_generated_html_index: Callable[
..., Tuple[TestPipResult, Dict[str, Any]]
],
requirement: str,
err_string: str,
) -> None:
"""Verify that the underlying dist files are not downloaded at all when
`install --dry-run` is used to resolve dists with PEP 658 metadata."""
_, report = install_with_generated_html_index(
[requirement],
)
assert [requirement] == list(str(r) for r, _ in iter_dists(report))
result, _ = install_with_generated_html_index(
[requirement],
dry_run=False,
allow_error=True,
)
assert result.returncode != 0
assert err_string in result.stderr