"""验证 wheel 元数据从 requirements.txt 声明了全部运行时依赖。""" from __future__ import annotations import argparse from email import policy from email.parser import BytesParser from pathlib import Path import re import sys import zipfile def normalize_project_name(name: str) -> str: """使用足以比较 requirements 与 Core Metadata 的项目名规范化规则。""" return re.sub(r"[-_.]+", "-", name).lower() def dependencies_from_requirements(requirements_file: Path) -> set[str]: """从唯一依赖来源读取项目名;当前 requirements 不允许间接或可编辑依赖。""" dependencies: set[str] = set() for line in requirements_file.read_text(encoding="utf-8").splitlines(): requirement = line.partition("#")[0].strip() if not requirement: continue match = re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", requirement) if match is None: raise ValueError(f"requirements.txt 包含不支持的依赖声明:{requirement}") dependencies.add(normalize_project_name(match.group())) return dependencies def dependencies_from_wheel(wheel_file: Path) -> set[str]: """读取 wheel 的 Core Metadata 中声明的 Requires-Dist 项目名。""" with zipfile.ZipFile(wheel_file) as wheel: metadata_members = [name for name in wheel.namelist() if name.endswith(".dist-info/METADATA")] if len(metadata_members) != 1: raise ValueError("wheel 中必须恰有一个 .dist-info/METADATA 文件") metadata = BytesParser(policy=policy.default).parsebytes(wheel.read(metadata_members[0])) dependencies = set() for requirement in metadata.get_all("Requires-Dist", []): match = re.match(r"[A-Za-z0-9][A-Za-z0-9._-]*", requirement) if match is None: raise ValueError(f"wheel METADATA 包含无效的 Requires-Dist:{requirement}") dependencies.add(normalize_project_name(match.group())) return dependencies def verify_wheel_metadata(wheel_file: Path, requirements_file: Path) -> set[str]: """返回没有被 wheel 元数据声明的 requirements 项目名。""" return dependencies_from_requirements(requirements_file) - dependencies_from_wheel(wheel_file) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="检查 wheel 是否包含 requirements.txt 的依赖元数据") parser.add_argument("wheel", type=Path, help="待检查的 wheel 文件") parser.add_argument( "--requirements", type=Path, default=Path(__file__).resolve().parents[1] / "requirements.txt", help="唯一依赖来源 requirements.txt 的路径", ) arguments = parser.parse_args(argv) missing = verify_wheel_metadata(arguments.wheel, arguments.requirements) if missing: print(f"wheel METADATA 缺少依赖:{', '.join(sorted(missing))}", file=sys.stderr) return 1 print("wheel METADATA 已声明 requirements.txt 中的全部依赖。") return 0 if __name__ == "__main__": raise SystemExit(main())