feat: addon registry with runtime introspection API (#253) #258
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user