Entelechy / plugins /registry.py
qa296
refactor: simplify architecture and rebrand to Entelechy
b8e5043
"""Plugin registry - auto-discovery and registration of plugins."""
from __future__ import annotations
import importlib
import importlib.util
import sys
from pathlib import Path
from typing import TYPE_CHECKING
import yaml
from loguru import logger
if TYPE_CHECKING:
from plugins.base_plugin import BasePlugin
class PluginRegistry:
"""Registry for discovering and tracking plugin classes."""
def __init__(self):
self.plugin_classes: list[type[BasePlugin]] = []
self.metadata: dict[str, dict] = {}
def register_plugin_class(self, cls: type[BasePlugin]):
"""Register a plugin class (called automatically by __init_subclass__)."""
if cls not in self.plugin_classes:
self.plugin_classes.append(cls)
name = getattr(cls, "plugin_name", "") or cls.__name__
logger.debug(f"Registered plugin class: {name}")
def discover_and_load(self, plugins_dir: Path):
"""Discover and import plugins from a directory.
Each plugin is a subdirectory containing:
- metadata.yaml: Plugin metadata
- plugin.py: Plugin module with a BasePlugin subclass
"""
plugins_dir = Path(plugins_dir)
if not plugins_dir.exists():
logger.warning(f"Plugins directory does not exist: {plugins_dir}")
return
for plugin_dir in sorted(plugins_dir.iterdir()):
if not plugin_dir.is_dir():
continue
metadata_file = plugin_dir / "metadata.yaml"
plugin_file = plugin_dir / "plugin.py"
if not plugin_file.exists():
continue
# Load metadata
metadata = {}
if metadata_file.exists():
try:
with open(metadata_file, "r", encoding="utf-8") as f:
metadata = yaml.safe_load(f) or {}
except Exception as e:
logger.warning(f"Failed to load metadata for {plugin_dir.name}: {e}")
plugin_name = metadata.get("name", plugin_dir.name)
self.metadata[plugin_name] = metadata
# Import the plugin module
try:
self._import_plugin(plugin_dir.name, plugin_file)
logger.info(f"Loaded plugin: {plugin_name}")
except Exception as e:
logger.error(f"Failed to load plugin {plugin_name}: {e}")
def _import_plugin(self, name: str, plugin_file: Path):
"""Import a plugin module, triggering __init_subclass__ registration."""
module_name = f"plugins._loaded.{name}"
spec = importlib.util.spec_from_file_location(module_name, plugin_file)
if spec is None or spec.loader is None:
raise ImportError(f"Cannot create module spec for {plugin_file}")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
def get_plugin_class(self, name: str) -> type[BasePlugin] | None:
"""Get a plugin class by name."""
for cls in self.plugin_classes:
cls_name = getattr(cls, "plugin_name", "") or cls.__name__
if cls_name == name:
return cls
return None
def list_plugins(self) -> list[str]:
"""List all registered plugin names."""
names = []
for cls in self.plugin_classes:
names.append(getattr(cls, "plugin_name", "") or cls.__name__)
return names
# Global registry instance
plugin_registry = PluginRegistry()