chore(checks): discover workspace module boundaries
This commit is contained in:
113
tools/checks/check-manifest-shapes.py
Normal file
113
tools/checks/check-manifest-shapes.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Load and validate every available GovOPlaN module manifest from source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import json
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--workspace-root",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Directory containing the GovOPlaN repositories. Defaults to repositories.json default_parent.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||
workspace_root = (args.workspace_root or Path(catalog["default_parent"])).resolve()
|
||||
repositories = tuple(catalog["repositories"])
|
||||
|
||||
source_roots: list[Path] = []
|
||||
manifest_sources: list[tuple[str, Path, Path]] = []
|
||||
for repository in repositories:
|
||||
repository_root = workspace_root / repository["path"]
|
||||
source_root = repository_root / "src"
|
||||
if not source_root.is_dir():
|
||||
continue
|
||||
source_roots.append(source_root)
|
||||
manifest_sources.extend(
|
||||
(repository["name"], source_root, manifest_path)
|
||||
for manifest_path in sorted(source_root.glob("*/backend/manifest.py"))
|
||||
)
|
||||
|
||||
if not manifest_sources:
|
||||
print(f"No module manifests found below {workspace_root}.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
core_source = workspace_root / "govoplan-core" / "src"
|
||||
ordered_source_roots = [core_source, *(root for root in source_roots if root != core_source)]
|
||||
sys.path[:0] = [str(root) for root in ordered_source_roots]
|
||||
|
||||
from govoplan_core.core.modules import ModuleManifest # noqa: PLC0415
|
||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError # noqa: PLC0415
|
||||
|
||||
manifests: list[ModuleManifest] = []
|
||||
errors: list[str] = []
|
||||
for repository_name, source_root, manifest_path in manifest_sources:
|
||||
module_name = ".".join(manifest_path.relative_to(source_root).with_suffix("").parts)
|
||||
try:
|
||||
loaded_module = importlib.import_module(module_name)
|
||||
get_manifest = getattr(loaded_module, "get_manifest")
|
||||
manifest = get_manifest()
|
||||
except Exception as exc: # pragma: no cover - rendered as a check failure
|
||||
errors.append(f"{repository_name}: could not load {module_name}: {exc}")
|
||||
continue
|
||||
|
||||
if not isinstance(manifest, ModuleManifest):
|
||||
errors.append(f"{repository_name}: {module_name}.get_manifest() did not return ModuleManifest")
|
||||
continue
|
||||
|
||||
entry_points = _module_entry_points(manifest_path.parents[3] / "pyproject.toml")
|
||||
expected_target = f"{module_name}:get_manifest"
|
||||
actual_target = entry_points.get(manifest.id)
|
||||
if actual_target != expected_target:
|
||||
declared = ", ".join(f"{name}={target}" for name, target in sorted(entry_points.items())) or "none"
|
||||
errors.append(
|
||||
f"{repository_name}: module {manifest.id!r} must declare entry point "
|
||||
f"{manifest.id}={expected_target}; found {declared}"
|
||||
)
|
||||
continue
|
||||
|
||||
manifests.append(manifest)
|
||||
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
registry = PlatformRegistry()
|
||||
try:
|
||||
for manifest in manifests:
|
||||
registry.register(manifest)
|
||||
snapshot = registry.validate()
|
||||
except RegistryError as exc:
|
||||
print(f"Manifest registry validation failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(
|
||||
f"Manifest registry check passed: {len(snapshot.manifests)} manifests "
|
||||
f"from {len(manifest_sources)} source files."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _module_entry_points(pyproject_path: Path) -> dict[str, str]:
|
||||
if not pyproject_path.is_file():
|
||||
return {}
|
||||
pyproject = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
|
||||
values = pyproject.get("project", {}).get("entry-points", {}).get("govoplan.modules", {})
|
||||
return {str(name): str(target) for name, target in values.items()}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user