feat: addon registry with runtime introspection API (#253) #258

Merged
forbes merged 1 commits from feat/addon-registry-api into main 2026-02-16 23:41:19 +00:00
2 changed files with 52 additions and 16 deletions

View File

@@ -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")

View File

@@ -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 <kindred> 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