80 lines
2.8 KiB
Python
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())
|