netbox/scripts/verify_dependencies.py

80 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Verify requirements.txt is consistent with base_requirements.txt.
Guards against dependency drift before publishing. The wheel sources its pinned
dependencies from requirements.txt, which must stay in sync with the maintainer
policy in base_requirements.txt (same package set, and every pin satisfies its
declared constraint).
"""
import importlib.util
import sys
from pathlib import Path
from packaging.requirements import Requirement
def load_hatch_metadata():
"""Load scripts/packaging/hatch_metadata.py by path.
scripts/packaging is not a package (no __init__.py), and importing it by name would
collide with the third-party packaging distribution, so load it from its file path.
"""
path = Path(__file__).resolve().parent / 'packaging' / 'hatch_metadata.py'
spec = importlib.util.spec_from_file_location('netbox_hatch_metadata', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def parse(path, hatch_metadata):
reqs = {}
for line in hatch_metadata.read_requirements(Path(path).read_text()):
req = Requirement(line)
reqs[req.name.lower().replace('_', '-')] = req
return reqs
def check(base, pinned):
errors = []
only_base = sorted(set(base) - set(pinned))
only_pinned = sorted(set(pinned) - set(base))
if only_base:
errors.append(f"In base_requirements.txt but not requirements.txt: {only_base}")
if only_pinned:
errors.append(f"In requirements.txt but not base_requirements.txt: {only_pinned}")
for name in sorted(set(base) & set(pinned)):
if base[name].extras != pinned[name].extras:
errors.append(
f"{name}: extras differ (base {sorted(base[name].extras)} vs "
f"requirements.txt {sorted(pinned[name].extras)})"
)
spec = pinned[name].specifier
if not spec or not all(s.operator == '==' for s in spec):
errors.append(f"{name}: requirements.txt must pin exactly (got '{pinned[name]}')")
continue
version = next(iter(spec)).version
if not base[name].specifier.contains(version, prereleases=True):
errors.append(f"{name}: pinned {version} violates base constraint '{base[name].specifier}'")
return errors
def main():
root = Path(__file__).resolve().parent.parent
hatch_metadata = load_hatch_metadata()
errors = check(
parse(root / 'base_requirements.txt', hatch_metadata),
parse(root / 'requirements.txt', hatch_metadata),
)
if errors:
print("Dependency drift detected:")
for error in errors:
print(f" - {error}")
return 1
print("OK: requirements.txt is consistent with base_requirements.txt")
return 0
if __name__ == '__main__':
sys.exit(main())