81 lines
3.0 KiB
Python
81 lines
3.0 KiB
Python
"""验证 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())
|