121 lines
3.3 KiB
Python
Executable File
121 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate Library Imports - Pre-commit Hook
|
|
|
|
Ensures all hooks and libs can be imported without errors.
|
|
Catches broken imports when libraries are deleted or renamed.
|
|
|
|
Usage:
|
|
python3 validate_lib_imports.py
|
|
|
|
Exit Codes:
|
|
0 - All imports successful
|
|
1 - Some imports failed
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
"""Find project root by looking for .git directory."""
|
|
current = Path.cwd()
|
|
while current != current.parent:
|
|
if (current / ".git").exists():
|
|
return current
|
|
current = current.parent
|
|
return Path.cwd()
|
|
|
|
|
|
def extract_local_imports(file_path: Path, lib_dir: Path) -> list[str]:
|
|
"""Extract local lib imports from a Python file.
|
|
|
|
Returns:
|
|
List of local library names that are imported
|
|
"""
|
|
try:
|
|
source = file_path.read_text()
|
|
tree = ast.parse(source)
|
|
except SyntaxError:
|
|
return [] # Syntax errors caught elsewhere
|
|
|
|
local_imports = []
|
|
lib_names = {f.stem for f in lib_dir.glob("*.py") if f.stem != "__init__"}
|
|
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
name = alias.name.split(".")[0]
|
|
if name in lib_names:
|
|
local_imports.append(name)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
if node.module:
|
|
name = node.module.split(".")[0]
|
|
if name in lib_names:
|
|
local_imports.append(name)
|
|
|
|
return local_imports
|
|
|
|
|
|
def validate_lib_imports() -> tuple[bool, list[str]]:
|
|
"""Validate all local imports resolve to existing libs.
|
|
|
|
Returns:
|
|
Tuple of (success, list of errors)
|
|
"""
|
|
project_root = get_project_root()
|
|
plugin_dir = project_root / "plugins" / "autonomous-dev"
|
|
hooks_dir = plugin_dir / "hooks"
|
|
lib_dir = plugin_dir / "lib"
|
|
|
|
if not lib_dir.exists():
|
|
return True, []
|
|
|
|
# Get all existing lib names
|
|
existing_libs = {f.stem for f in lib_dir.glob("*.py") if f.stem != "__init__"}
|
|
|
|
errors = []
|
|
|
|
# Check hooks for broken imports
|
|
for hook_file in hooks_dir.glob("*.py"):
|
|
if hook_file.name.startswith("test_"):
|
|
continue
|
|
imports = extract_local_imports(hook_file, lib_dir)
|
|
for imp in imports:
|
|
if imp not in existing_libs:
|
|
errors.append(f"{hook_file.name}: imports missing lib '{imp}'")
|
|
|
|
# Check libs for broken cross-imports
|
|
for lib_file in lib_dir.glob("*.py"):
|
|
if lib_file.name.startswith("test_") or lib_file.name == "__init__.py":
|
|
continue
|
|
imports = extract_local_imports(lib_file, lib_dir)
|
|
for imp in imports:
|
|
if imp not in existing_libs:
|
|
errors.append(f"{lib_file.name}: imports missing lib '{imp}'")
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point."""
|
|
success, errors = validate_lib_imports()
|
|
|
|
if success:
|
|
print("✅ All library imports valid")
|
|
return 0
|
|
else:
|
|
print("❌ Broken library imports detected!")
|
|
print("")
|
|
print("Errors:")
|
|
for error in sorted(errors):
|
|
print(f" - {error}")
|
|
print("")
|
|
print("Fix: Either restore the missing lib or update the import")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|