from __future__ import annotations import importlib import shutil import subprocess import sys import tarfile import zipfile from dataclasses import dataclass from enum import Enum from pathlib import Path, PurePosixPath import pytest import yaml from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name, parse_wheel_filename from packaging.version import Version import dlp_io try: import tomllib except ModuleNotFoundError: # Python 3.10 import tomli as tomllib PROJECT_ROOT = Path(__file__).resolve().parents[1] WORKFLOW_PATH = PROJECT_ROOT / ".gitea" / "workflows" / "publish-dlp-io.yaml" class DynamicField(Enum): VERSION = "version" class PackageName(Enum): DLP_IO = canonicalize_name("dlp-io") class PackageClassifier(Enum): WINDOWS = "Operating System :: Microsoft :: Windows" class WorkflowStep(Enum): CLONE_TAG = "Clone immutable package tag" TEST_MATRIX = "Test Python 3.8-3.14" BUILD = "Build and check distribution" SMOKE = "Smoke test release artifacts" RELEASE = "Create Gitea release with wheels" PYPI = "Publish packages to Gitea PyPI" @dataclass(frozen=True) class BuiltDistributions: wheel: Path sdist: Path def _load_pyproject() -> dict: with (PROJECT_ROOT / "pyproject.toml").open("rb") as file: return tomllib.load(file) def _load_workflow() -> dict: with WORKFLOW_PATH.open(encoding="utf-8") as file: return yaml.safe_load(file) def _copy_source_tree(source: Path) -> None: source.mkdir() shutil.copy2(PROJECT_ROOT / "pyproject.toml", source) shutil.copy2(PROJECT_ROOT / "MANIFEST.in", source) shutil.copy2(PROJECT_ROOT / "setup.py", source) shutil.copy2(PROJECT_ROOT / "dlp_io.py", source) shutil.copytree(PROJECT_ROOT / "docs", source / "docs") @pytest.fixture(scope="module") def built_distributions(tmp_path_factory: pytest.TempPathFactory) -> BuiltDistributions: root = tmp_path_factory.mktemp("dlp-io-build") source = root / "source" _copy_source_tree(source) dist = root / "dist" completed = subprocess.run( [ sys.executable, "-m", "build", "--no-isolation", "--outdir", str(dist), ], cwd=source, check=False, capture_output=True, ) assert completed.returncode == 0 wheels = tuple(dist.glob("*.whl")) sdists = tuple(dist.glob("*.tar.gz")) assert len(wheels) == 1 assert len(sdists) == 1 return BuiltDistributions(wheel=wheels[0], sdist=sdists[0]) def test_pyproject_declares_structured_public_package_contract() -> None: config = _load_pyproject() project = config["project"] assert PackageName(canonicalize_name(project["name"])) is PackageName.DLP_IO assert SpecifierSet(project["requires-python"]) == SpecifierSet(">=3.8") assert {DynamicField(item) for item in project["dynamic"]} == { DynamicField.VERSION } known_classifier_values = {item.value for item in PackageClassifier} known_classifiers = { PackageClassifier(item) for item in project["classifiers"] if item in known_classifier_values } assert PackageClassifier.WINDOWS in known_classifiers version_ref = config["tool"]["setuptools"]["dynamic"]["version"]["attr"] module_name, attribute_name = version_ref.rsplit(".", 1) declared_version = getattr(importlib.import_module(module_name), attribute_name) assert Version(declared_version) == Version(dlp_io.__version__) def test_wheel_contains_only_installable_dlp_io_runtime( built_distributions: BuiltDistributions, ) -> None: name, version, build, tags = parse_wheel_filename(built_distributions.wheel.name) with zipfile.ZipFile(built_distributions.wheel) as archive: members = {PurePosixPath(item) for item in archive.namelist()} assert PackageName(name) is PackageName.DLP_IO assert version == Version(dlp_io.__version__) assert build == () assert tags assert PurePosixPath("dlp_io.py") in members assert not [item for item in members if item.suffix == ".pyd"] member_roots = {Path(item.parts[0]) for item in members} assert member_roots.isdisjoint({Path("tests"), Path("Lib")}) assert PurePosixPath(".env") not in members def test_sdist_contains_package_sources_and_public_readme_only( built_distributions: BuiltDistributions, ) -> None: with tarfile.open(built_distributions.sdist, mode="r:gz") as archive: members = { PurePosixPath(*PurePosixPath(item.name).parts[1:]) for item in archive.getmembers() if item.isfile() } assert PurePosixPath("pyproject.toml") in members assert PurePosixPath("setup.py") in members assert PurePosixPath("docs/DLP_IO_LIBRARY.md") in members assert PurePosixPath("dlp_io.py") in members member_roots = {Path(item.parts[0]) for item in members} assert Path("tests") not in member_roots assert PurePosixPath("README.md") not in members assert PurePosixPath(".env") not in members def test_publish_workflow_tests_builds_and_releases_tagged_distributions() -> None: document = _load_workflow() trigger = document.get("on", document.get(True)) assert set(trigger["push"]["tags"]) == {"dlp-io-v*.*.*"} assert "workflow_dispatch" in trigger job = document["jobs"]["publish"] assert job["runs-on"] == "windows-latest" assert Path(job["defaults"]["run"]["working-directory"]) == Path("repo") step_names = [step["name"] for step in job["steps"]] assert step_names == [step.value for step in WorkflowStep]