""" Plugin Loader Dynamically loads and manages plugins. """ import importlib import inspect from pathlib import Path from typing import Dict, List, Type, Optional, Any from loguru import logger from plugins.base import BasePlugin, PluginMetadata from core.exceptions import PluginError, PluginLoadError class PluginLoader: """ Load and manage plugins dynamically. Discovers, loads, and manages plugin lifecycle. """ def __init__(self, plugin_dir: Optional[Path] = None): """ Initialize PluginLoader. Args: plugin_dir: Directory containing plugin modules """ if plugin_dir is None: plugin_dir = Path(__file__).parent self.plugin_dir = Path(plugin_dir) self.plugins: Dict[str, BasePlugin] = {} self.plugin_classes: Dict[str, Type[BasePlugin]] = {} logger.info(f"PluginLoader initialized with directory: {plugin_dir}") def discover_plugins(self) -> List[str]: """ Discover available plugins in the plugin directory. Returns: List of discovered plugin module names """ discovered = [] # Look for Python files in the plugin directory for file_path in self.plugin_dir.glob("*.py"): # Skip __init__.py, base.py, loader.py if file_path.stem in ["__init__", "base", "loader"]: continue module_name = file_path.stem discovered.append(module_name) logger.debug(f"Discovered plugin module: {module_name}") logger.info(f"Discovered {len(discovered)} plugin modules") return discovered def load_plugin_class(self, module_name: str) -> Optional[Type[BasePlugin]]: """ Load a plugin class from a module. Args: module_name: Name of the module to load Returns: Plugin class or None if not found """ try: # Import the module module = importlib.import_module(f"plugins.{module_name}") # Find all classes that inherit from BasePlugin for name, obj in inspect.getmembers(module, inspect.isclass): if (issubclass(obj, BasePlugin) and obj is not BasePlugin and obj.__module__ == module.__name__): logger.info(f"Loaded plugin class: {name} from {module_name}") return obj logger.warning(f"No plugin class found in module: {module_name}") return None except Exception as e: logger.error(f"Failed to load plugin module {module_name}: {e}") raise PluginLoadError( f"Cannot load plugin module {module_name}: {str(e)}", {"module": module_name, "error": str(e)} ) def load_plugin( self, plugin_name: str, auto_initialize: bool = True ) -> BasePlugin: """ Load and optionally initialize a plugin. Args: plugin_name: Name of the plugin module auto_initialize: Whether to automatically initialize the plugin Returns: Loaded plugin instance """ try: # Check if already loaded if plugin_name in self.plugins: logger.info(f"Plugin {plugin_name} already loaded") return self.plugins[plugin_name] # Load plugin class plugin_class = self.load_plugin_class(plugin_name) if plugin_class is None: raise PluginLoadError( f"No plugin class found in {plugin_name}", {"plugin": plugin_name} ) # Store plugin class self.plugin_classes[plugin_name] = plugin_class # Create instance plugin_instance = plugin_class() # Initialize if requested if auto_initialize: plugin_instance.initialize() plugin_instance._initialized = True # Store instance self.plugins[plugin_instance.metadata.name] = plugin_instance logger.info( f"Plugin loaded: {plugin_instance.metadata.name} " f"v{plugin_instance.metadata.version}" ) return plugin_instance except Exception as e: logger.error(f"Failed to load plugin {plugin_name}: {e}") raise PluginLoadError( f"Cannot load plugin {plugin_name}: {str(e)}", {"plugin": plugin_name, "error": str(e)} ) def load_all_plugins(self, auto_initialize: bool = True) -> Dict[str, BasePlugin]: """ Discover and load all available plugins. Args: auto_initialize: Whether to automatically initialize plugins Returns: Dictionary of loaded plugins """ discovered = self.discover_plugins() for module_name in discovered: try: self.load_plugin(module_name, auto_initialize=auto_initialize) except Exception as e: logger.error(f"Failed to load plugin {module_name}: {e}") # Continue loading other plugins continue logger.info(f"Loaded {len(self.plugins)} plugins") return self.plugins def unload_plugin(self, plugin_name: str) -> None: """ Unload a plugin and clean up resources. Args: plugin_name: Name of the plugin to unload """ if plugin_name not in self.plugins: logger.warning(f"Plugin {plugin_name} not loaded") return plugin = self.plugins[plugin_name] # Clean up resources try: plugin.cleanup() except Exception as e: logger.error(f"Error during plugin cleanup: {e}") # Remove from loaded plugins del self.plugins[plugin_name] logger.info(f"Plugin unloaded: {plugin_name}") def unload_all_plugins(self) -> None: """Unload all plugins.""" plugin_names = list(self.plugins.keys()) for plugin_name in plugin_names: self.unload_plugin(plugin_name) logger.info("All plugins unloaded") def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]: """ Get a loaded plugin by name. Args: plugin_name: Name of the plugin Returns: Plugin instance or None """ return self.plugins.get(plugin_name) def list_plugins(self) -> List[PluginMetadata]: """ List all loaded plugins. Returns: List of plugin metadata """ return [plugin.metadata for plugin in self.plugins.values()] def reload_plugin(self, plugin_name: str) -> BasePlugin: """ Reload a plugin (unload then load). Args: plugin_name: Name of the plugin to reload Returns: Reloaded plugin instance """ logger.info(f"Reloading plugin: {plugin_name}") # Find the module name module_name = None for name, plugin in self.plugins.items(): if name == plugin_name: module_name = plugin.__class__.__module__.split(".")[-1] break if module_name is None: raise PluginError( f"Plugin {plugin_name} not found", {"plugin": plugin_name} ) # Unload self.unload_plugin(plugin_name) # Reload module importlib.reload( importlib.import_module(f"plugins.{module_name}") ) # Load again return self.load_plugin(module_name) def get_plugins_by_category(self, category: str) -> List[BasePlugin]: """ Get all plugins in a specific category. Args: category: Plugin category Returns: List of plugins in the category """ return [ plugin for plugin in self.plugins.values() if plugin.metadata.category == category ] def get_enabled_plugins(self) -> List[BasePlugin]: """ Get all enabled plugins. Returns: List of enabled plugins """ return [ plugin for plugin in self.plugins.values() if plugin.is_enabled() ] def get_plugin_info(self) -> Dict[str, Dict[str, Any]]: """ Get information about all loaded plugins. Returns: Dictionary with plugin information """ info = {} for name, plugin in self.plugins.items(): info[name] = { "name": plugin.metadata.name, "version": plugin.metadata.version, "description": plugin.metadata.description, "author": plugin.metadata.author, "category": plugin.metadata.category, "enabled": plugin.is_enabled(), "initialized": plugin.is_initialized(), "priority": plugin.metadata.priority, } return info def __repr__(self) -> str: """String representation.""" return f"PluginLoader(plugins={len(self.plugins)})"