Skip to content

Commit d6fdfc0

Browse files
move test_download_metadata mock pypi index utilities to conftest.py
1 parent 3c0147a commit d6fdfc0

File tree

2 files changed

+282
-263
lines changed

2 files changed

+282
-263
lines changed

tests/conftest.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import compileall
22
import fnmatch
3+
import http.server
34
import io
45
import os
56
import re
67
import shutil
78
import subprocess
89
import sys
10+
import threading
911
from contextlib import ExitStack, contextmanager
12+
from dataclasses import dataclass
13+
from enum import Enum
14+
from hashlib import sha256
1015
from pathlib import Path
16+
from textwrap import dedent
1117
from typing import (
1218
TYPE_CHECKING,
19+
Any,
1320
AnyStr,
1421
Callable,
22+
ClassVar,
1523
Dict,
1624
Iterable,
1725
Iterator,
1826
List,
1927
Optional,
28+
Set,
29+
Tuple,
2030
Union,
2131
)
2232
from unittest.mock import patch
@@ -750,3 +760,235 @@ def proxy(request: pytest.FixtureRequest) -> str:
750760
@pytest.fixture
751761
def enable_user_site(virtualenv: VirtualEnvironment) -> None:
752762
virtualenv.user_site_packages = True
763+
764+
765+
class MetadataKind(Enum):
766+
"""All the types of values we might be provided for the data-dist-info-metadata
767+
attribute from PEP 658."""
768+
769+
# Valid: will read metadata from the dist instead.
770+
No = "none"
771+
# Valid: will read the .metadata file, but won't check its hash.
772+
Unhashed = "unhashed"
773+
# Valid: will read the .metadata file and check its hash matches.
774+
Sha256 = "sha256"
775+
# Invalid: will error out after checking the hash.
776+
WrongHash = "wrong-hash"
777+
# Invalid: will error out after failing to fetch the .metadata file.
778+
NoFile = "no-file"
779+
780+
781+
@dataclass(frozen=True)
782+
class FakePackage:
783+
"""Mock package structure used to generate a PyPI repository.
784+
785+
FakePackage name and version should correspond to sdists (.tar.gz files) in our test
786+
data."""
787+
788+
name: str
789+
version: str
790+
filename: str
791+
metadata: MetadataKind
792+
# This will override any dependencies specified in the actual dist's METADATA.
793+
requires_dist: Tuple[str, ...] = ()
794+
# This will override the Name specified in the actual dist's METADATA.
795+
metadata_name: Optional[str] = None
796+
797+
def metadata_filename(self) -> str:
798+
"""This is specified by PEP 658."""
799+
return f"{self.filename}.metadata"
800+
801+
def generate_additional_tag(self) -> str:
802+
"""This gets injected into the <a> tag in the generated PyPI index page for this
803+
package."""
804+
if self.metadata == MetadataKind.No:
805+
return ""
806+
if self.metadata in [MetadataKind.Unhashed, MetadataKind.NoFile]:
807+
return 'data-dist-info-metadata="true"'
808+
if self.metadata == MetadataKind.WrongHash:
809+
return 'data-dist-info-metadata="sha256=WRONG-HASH"'
810+
assert self.metadata == MetadataKind.Sha256
811+
checksum = sha256(self.generate_metadata()).hexdigest()
812+
return f'data-dist-info-metadata="sha256={checksum}"'
813+
814+
def requires_str(self) -> str:
815+
if not self.requires_dist:
816+
return ""
817+
joined = " and ".join(self.requires_dist)
818+
return f"Requires-Dist: {joined}"
819+
820+
def generate_metadata(self) -> bytes:
821+
"""This is written to `self.metadata_filename()` and will override the actual
822+
dist's METADATA, unless `self.metadata == MetadataKind.NoFile`."""
823+
return dedent(
824+
f"""\
825+
Metadata-Version: 2.1
826+
Name: {self.metadata_name or self.name}
827+
Version: {self.version}
828+
{self.requires_str()}
829+
"""
830+
).encode("utf-8")
831+
832+
833+
@pytest.fixture(scope="session")
834+
def fake_packages() -> Dict[str, List[FakePackage]]:
835+
"""The package database we generate for testing PEP 658 support."""
836+
return {
837+
"simple": [
838+
FakePackage("simple", "1.0", "simple-1.0.tar.gz", MetadataKind.Sha256),
839+
FakePackage("simple", "2.0", "simple-2.0.tar.gz", MetadataKind.No),
840+
# This will raise a hashing error.
841+
FakePackage("simple", "3.0", "simple-3.0.tar.gz", MetadataKind.WrongHash),
842+
],
843+
"simple2": [
844+
# Override the dependencies here in order to force pip to download
845+
# simple-1.0.tar.gz as well.
846+
FakePackage(
847+
"simple2",
848+
"1.0",
849+
"simple2-1.0.tar.gz",
850+
MetadataKind.Unhashed,
851+
("simple==1.0",),
852+
),
853+
# This will raise an error when pip attempts to fetch the metadata file.
854+
FakePackage("simple2", "2.0", "simple2-2.0.tar.gz", MetadataKind.NoFile),
855+
# This has a METADATA file with a mismatched name.
856+
FakePackage(
857+
"simple2",
858+
"3.0",
859+
"simple2-3.0.tar.gz",
860+
MetadataKind.Sha256,
861+
metadata_name="not-simple2",
862+
),
863+
],
864+
"colander": [
865+
# Ensure we can read the dependencies from a metadata file within a wheel
866+
# *without* PEP 658 metadata.
867+
FakePackage(
868+
"colander",
869+
"0.9.9",
870+
"colander-0.9.9-py2.py3-none-any.whl",
871+
MetadataKind.No,
872+
),
873+
],
874+
"compilewheel": [
875+
# Ensure we can override the dependencies of a wheel file by injecting PEP
876+
# 658 metadata.
877+
FakePackage(
878+
"compilewheel",
879+
"1.0",
880+
"compilewheel-1.0-py2.py3-none-any.whl",
881+
MetadataKind.Unhashed,
882+
("simple==1.0",),
883+
),
884+
],
885+
"has-script": [
886+
# Ensure we check PEP 658 metadata hashing errors for wheel files.
887+
FakePackage(
888+
"has-script",
889+
"1.0",
890+
"has.script-1.0-py2.py3-none-any.whl",
891+
MetadataKind.WrongHash,
892+
),
893+
],
894+
"translationstring": [
895+
FakePackage(
896+
"translationstring",
897+
"1.1",
898+
"translationstring-1.1.tar.gz",
899+
MetadataKind.No,
900+
),
901+
],
902+
"priority": [
903+
# Ensure we check for a missing metadata file for wheels.
904+
FakePackage(
905+
"priority",
906+
"1.0",
907+
"priority-1.0-py2.py3-none-any.whl",
908+
MetadataKind.NoFile,
909+
),
910+
],
911+
"requires-simple-extra": [
912+
# Metadata name is not canonicalized.
913+
FakePackage(
914+
"requires-simple-extra",
915+
"0.1",
916+
"requires_simple_extra-0.1-py2.py3-none-any.whl",
917+
MetadataKind.Sha256,
918+
metadata_name="Requires_Simple.Extra",
919+
),
920+
],
921+
}
922+
923+
924+
@pytest.fixture(scope="session")
925+
def html_index_for_packages(
926+
shared_data: TestData,
927+
fake_packages: Dict[str, List[FakePackage]],
928+
tmpdir_factory: pytest.TempPathFactory,
929+
) -> Path:
930+
"""Generate a PyPI HTML package index within a local directory pointing to
931+
synthetic test data."""
932+
html_dir = tmpdir_factory.mktemp("fake_index_html_content")
933+
934+
# (1) Generate the content for a PyPI index.html.
935+
pkg_links = "\n".join(
936+
f' <a href="{pkg}/index.html">{pkg}</a>' for pkg in fake_packages.keys()
937+
)
938+
index_html = f"""\
939+
<!DOCTYPE html>
940+
<html>
941+
<head>
942+
<meta name="pypi:repository-version" content="1.0">
943+
<title>Simple index</title>
944+
</head>
945+
<body>
946+
{pkg_links}
947+
</body>
948+
</html>"""
949+
# (2) Generate the index.html in a new subdirectory of the temp directory.
950+
(html_dir / "index.html").write_text(index_html)
951+
952+
# (3) Generate subdirectories for individual packages, each with their own
953+
# index.html.
954+
for pkg, links in fake_packages.items():
955+
pkg_subdir = html_dir / pkg
956+
pkg_subdir.mkdir()
957+
958+
download_links: List[str] = []
959+
for package_link in links:
960+
# (3.1) Generate the <a> tag which pip can crawl pointing to this
961+
# specific package version.
962+
download_links.append(
963+
f' <a href="{package_link.filename}" {package_link.generate_additional_tag()}>{package_link.filename}</a><br/>' # noqa: E501
964+
)
965+
# (3.2) Copy over the corresponding file in `shared_data.packages`.
966+
shutil.copy(
967+
shared_data.packages / package_link.filename,
968+
pkg_subdir / package_link.filename,
969+
)
970+
# (3.3) Write a metadata file, if applicable.
971+
if package_link.metadata != MetadataKind.NoFile:
972+
with open(pkg_subdir / package_link.metadata_filename(), "wb") as f:
973+
f.write(package_link.generate_metadata())
974+
975+
# (3.4) After collating all the download links and copying over the files,
976+
# write an index.html with the generated download links for each
977+
# copied file for this specific package name.
978+
download_links_str = "\n".join(download_links)
979+
pkg_index_content = f"""\
980+
<!DOCTYPE html>
981+
<html>
982+
<head>
983+
<meta name="pypi:repository-version" content="1.0">
984+
<title>Links for {pkg}</title>
985+
</head>
986+
<body>
987+
<h1>Links for {pkg}</h1>
988+
{download_links_str}
989+
</body>
990+
</html>"""
991+
with open(pkg_subdir / "index.html", "w") as f:
992+
f.write(pkg_index_content)
993+
994+
return html_dir

0 commit comments

Comments
 (0)