verify_dependencies.py 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. #!/usr/bin/env python3
  2. """Verify requirements.txt is consistent with base_requirements.txt.
  3. Guards against dependency drift before publishing. The wheel sources its pinned
  4. dependencies from requirements.txt, which must stay in sync with the maintainer
  5. policy in base_requirements.txt (same package set, and every pin satisfies its
  6. declared constraint).
  7. """
  8. import sys
  9. from pathlib import Path
  10. from packaging.requirements import Requirement
  11. def parse(path):
  12. reqs = {}
  13. for raw_line in Path(path).read_text().splitlines():
  14. line = raw_line.split('#', 1)[0].strip()
  15. if not line or line.startswith('-'):
  16. continue
  17. req = Requirement(line)
  18. reqs[req.name.lower().replace('_', '-')] = req
  19. return reqs
  20. def check(base, pinned):
  21. errors = []
  22. only_base = sorted(set(base) - set(pinned))
  23. only_pinned = sorted(set(pinned) - set(base))
  24. if only_base:
  25. errors.append(f"In base_requirements.txt but not requirements.txt: {only_base}")
  26. if only_pinned:
  27. errors.append(f"In requirements.txt but not base_requirements.txt: {only_pinned}")
  28. for name in sorted(set(base) & set(pinned)):
  29. if base[name].extras != pinned[name].extras:
  30. errors.append(
  31. f"{name}: extras differ (base {sorted(base[name].extras)} vs "
  32. f"requirements.txt {sorted(pinned[name].extras)})"
  33. )
  34. spec = pinned[name].specifier
  35. if not spec or not all(s.operator == '==' for s in spec):
  36. errors.append(f"{name}: requirements.txt must pin exactly (got '{pinned[name]}')")
  37. continue
  38. version = next(iter(spec)).version
  39. if not base[name].specifier.contains(version, prereleases=True):
  40. errors.append(f"{name}: pinned {version} violates base constraint '{base[name].specifier}'")
  41. return errors
  42. def main():
  43. root = Path(__file__).resolve().parent.parent
  44. errors = check(parse(root / 'base_requirements.txt'), parse(root / 'requirements.txt'))
  45. if errors:
  46. print("Dependency drift detected:")
  47. for error in errors:
  48. print(f" - {error}")
  49. return 1
  50. print("OK: requirements.txt is consistent with base_requirements.txt")
  51. return 0
  52. if __name__ == '__main__':
  53. sys.exit(main())