From 9dd43a7cc3a424458763bb710121bf8d73afbfdd Mon Sep 17 00:00:00 2001 From: forbes Date: Mon, 16 Feb 2026 17:33:28 -0600 Subject: [PATCH] feat: addon registry with runtime introspection API (#253) Add FreeCAD.getAddonRegistry() function for runtime addon introspection. Changes to addon_loader.py: - Add contexts field to AddonManifest for tracking context IDs - Add register_context() method for addons to declare contexts at runtime - Add contexts() method returning {context_id: [addon_names]} mapping - Parse element from in package.xml - Add getAddonRegistry() function returning the registry singleton Changes to Init.py: - Expose getAddonRegistry as FreeCAD.getAddonRegistry after loading Usage: registry = FreeCAD.getAddonRegistry() registry.get('ztools') # AddonManifest for ztools registry.loaded() # list of loaded addons registry.is_loaded('silo') # True/False registry.contexts() # {context_id: [addon_names]} Closes #253 --- src/Mod/Create/Init.py | 3 +- src/Mod/Create/addon_loader.py | 65 ++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/Mod/Create/Init.py b/src/Mod/Create/Init.py index 1769b66306..1c6c3331af 100644 --- a/src/Mod/Create/Init.py +++ b/src/Mod/Create/Init.py @@ -4,9 +4,10 @@ import FreeCAD try: - from addon_loader import load_addons + from addon_loader import getAddonRegistry, load_addons load_addons(gui=False) + FreeCAD.getAddonRegistry = getAddonRegistry except Exception as e: FreeCAD.Console.PrintWarning(f"Create: Addon loader failed: {e}\n") diff --git a/src/Mod/Create/addon_loader.py b/src/Mod/Create/addon_loader.py index f229b991ad..5a73f19370 100644 --- a/src/Mod/Create/addon_loader.py +++ b/src/Mod/Create/addon_loader.py @@ -14,7 +14,6 @@ from typing import Optional import FreeCAD - # --------------------------------------------------------------------------- # Data structures # --------------------------------------------------------------------------- @@ -53,11 +52,10 @@ class AddonManifest: state: AddonState = AddonState.DISCOVERED error: str = "" load_time_ms: float = 0.0 + contexts: list[str] = field(default_factory=list) def __repr__(self): - return ( - f"AddonManifest(name={self.name!r}, version={self.version!r}, state={self.state.value})" - ) + return f"AddonManifest(name={self.name!r}, version={self.version!r}, state={self.state.value})" # --------------------------------------------------------------------------- @@ -97,6 +95,20 @@ class AddonRegistry: m = self._addons.get(name) return m is not None and m.state == AddonState.LOADED + def register_context(self, addon_name: str, context_id: str): + """Register a context ID as provided by an addon.""" + m = self._addons.get(addon_name) + if m is not None and context_id not in m.contexts: + m.contexts.append(context_id) + + def contexts(self) -> dict[str, list[str]]: + """Return a mapping of context IDs to the addon names that provide them.""" + result: dict[str, list[str]] = {} + for m in self._addons.values(): + for ctx in m.contexts: + result.setdefault(ctx, []).append(m.name) + return result + def _by_load_order(self) -> list[AddonManifest]: ordered = [] for name in self._load_order: @@ -213,7 +225,9 @@ def parse_manifest(manifest: AddonManifest): except ET.ParseError as e: manifest.state = AddonState.FAILED manifest.error = f"XML parse error: {e}" - FreeCAD.Console.PrintWarning(f"Create: Failed to parse {manifest.package_xml_path}: {e}\n") + FreeCAD.Console.PrintWarning( + f"Create: Failed to parse {manifest.package_xml_path}: {e}\n" + ) return # Standard fields @@ -256,6 +270,11 @@ def parse_manifest(manifest: AddonManifest): for dep in _findall(deps, "dependency"): if dep.text and dep.text.strip(): manifest.dependencies.append(dep.text.strip()) + ctxs = _find(kindred, "contexts") + if ctxs is not None: + for ctx in _findall(ctxs, "context"): + if ctx.text and ctx.text.strip(): + manifest.contexts.append(ctx.text.strip()) FreeCAD.Console.PrintLog( f"Create: Parsed {manifest.name} v{manifest.version} from {manifest.package_xml_path}\n" @@ -285,25 +304,27 @@ def validate_manifest(manifest: AddonManifest, create_version: str) -> bool: if manifest.min_create_version: if cv < _parse_version(manifest.min_create_version): manifest.state = AddonState.SKIPPED - manifest.error = ( - f"Requires Create >= {manifest.min_create_version}, running {create_version}" + manifest.error = f"Requires Create >= {manifest.min_create_version}, running {create_version}" + FreeCAD.Console.PrintWarning( + f"Create: Skipping {manifest.name}: {manifest.error}\n" ) - FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n") return False if manifest.max_create_version: if cv > _parse_version(manifest.max_create_version): manifest.state = AddonState.SKIPPED - manifest.error = ( - f"Requires Create <= {manifest.max_create_version}, running {create_version}" + manifest.error = f"Requires Create <= {manifest.max_create_version}, running {create_version}" + FreeCAD.Console.PrintWarning( + f"Create: Skipping {manifest.name}: {manifest.error}\n" ) - FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n") return False if not os.path.isdir(manifest.workbench_path): manifest.state = AddonState.SKIPPED manifest.error = f"Workbench path not found: {manifest.workbench_path}" - FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n") + FreeCAD.Console.PrintWarning( + f"Create: Skipping {manifest.name}: {manifest.error}\n" + ) return False # At least one of Init.py or InitGui.py must exist @@ -312,7 +333,9 @@ def validate_manifest(manifest: AddonManifest, create_version: str) -> bool: if not has_init and not has_gui: manifest.state = AddonState.SKIPPED manifest.error = f"No Init.py or InitGui.py in {manifest.workbench_path}" - FreeCAD.Console.PrintWarning(f"Create: Skipping {manifest.name}: {manifest.error}\n") + FreeCAD.Console.PrintWarning( + f"Create: Skipping {manifest.name}: {manifest.error}\n" + ) return False manifest.state = AddonState.VALIDATED @@ -324,7 +347,9 @@ def validate_manifest(manifest: AddonManifest, create_version: str) -> bool: # --------------------------------------------------------------------------- -def resolve_load_order(manifests: list[AddonManifest], mods_dir: str) -> list[AddonManifest]: +def resolve_load_order( + manifests: list[AddonManifest], mods_dir: str +) -> list[AddonManifest]: """Sort addons by dependencies, then by (load_priority, name). If no addons declare a element, fall back to the legacy @@ -428,7 +453,9 @@ def _load_addon(manifest: AddonManifest, gui: bool = False): else: manifest.load_time_ms += elapsed manifest.state = AddonState.LOADED - FreeCAD.Console.PrintLog(f"Create: Loaded {manifest.name} {init_file} ({elapsed:.0f}ms)\n") + FreeCAD.Console.PrintLog( + f"Create: Loaded {manifest.name} {init_file} ({elapsed:.0f}ms)\n" + ) except Exception as e: manifest.state = AddonState.FAILED manifest.error = str(e) @@ -491,3 +518,11 @@ def load_addons(gui: bool = False): for m in _registry.loaded(): _load_addon(m, gui=True) + + +def getAddonRegistry() -> Optional[AddonRegistry]: + """Return the addon registry singleton, or None if not yet initialized. + + Exposed as FreeCAD.getAddonRegistry() for runtime introspection. + """ + return _registry -- 2.49.1