#!/usr/bin/env python3 """Generate a JSON manifest for addon plugins.""" from __future__ import annotations import importlib.util import inspect import json import sys from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] PLUGIN_DIR = ROOT_DIR / "addon" / "plugins" OUTPUT_PATH = ROOT_DIR / "docs" / "PLUGIN_MANIFEST.json" sys.path.insert(0, str(ROOT_DIR / "addon")) try: from plugin_interface import BasisPlugin # type: ignore except Exception as exc: # pragma: no cover raise SystemExit(f"Failed to import BasisPlugin: {exc}") def _import_module(path: Path): spec = importlib.util.spec_from_file_location(path.stem, path) if spec is None or spec.loader is None: raise ImportError(f"Missing spec for {path}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def _collect_plugins(): plugins = [] for file_path in sorted(PLUGIN_DIR.glob("*.py")): if file_path.name.startswith("_"): continue entry = { "file": str(file_path.relative_to(ROOT_DIR)), "module": file_path.stem, "name": None, "class": None, "version": None, "capabilities": [], "prefer_source_metadata": False, "base_url_setting": None, "available": None, "unavailable_reason": None, "error": None, } try: module = _import_module(file_path) preferred = getattr(module, "Plugin", None) if inspect.isclass(preferred) and issubclass(preferred, BasisPlugin) and preferred is not BasisPlugin: classes = [preferred] else: classes = [ obj for obj in module.__dict__.values() if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin ] classes.sort(key=lambda cls: cls.__name__.casefold()) if not classes: entry["error"] = "No plugin classes found" plugins.append(entry) continue cls = classes[0] instance = cls() entry["class"] = cls.__name__ entry["name"] = str(getattr(instance, "name", "") or "") or None entry["version"] = str(getattr(instance, "version", "0.0.0") or "0.0.0") entry["prefer_source_metadata"] = bool(getattr(instance, "prefer_source_metadata", False)) entry["available"] = bool(getattr(instance, "is_available", True)) entry["unavailable_reason"] = getattr(instance, "unavailable_reason", None) try: caps = instance.capabilities() # type: ignore[call-arg] entry["capabilities"] = sorted([str(c) for c in caps]) if caps else [] except Exception: entry["capabilities"] = [] entry["base_url_setting"] = getattr(module, "SETTING_BASE_URL", None) except Exception as exc: # pragma: no cover entry["error"] = str(exc) plugins.append(entry) plugins.sort(key=lambda item: (item.get("name") or item["module"]).casefold()) return plugins def main() -> int: if not PLUGIN_DIR.exists(): raise SystemExit("Plugin directory missing") manifest = { "schema_version": 1, "plugins": _collect_plugins(), } OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) OUTPUT_PATH.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") print(str(OUTPUT_PATH)) return 0 if __name__ == "__main__": raise SystemExit(main())