|
|
""" |
|
|
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 = [] |
|
|
|
|
|
|
|
|
for file_path in self.plugin_dir.glob("*.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: |
|
|
|
|
|
module = importlib.import_module(f"plugins.{module_name}") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
if plugin_name in self.plugins: |
|
|
logger.info(f"Plugin {plugin_name} already loaded") |
|
|
return self.plugins[plugin_name] |
|
|
|
|
|
|
|
|
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} |
|
|
) |
|
|
|
|
|
|
|
|
self.plugin_classes[plugin_name] = plugin_class |
|
|
|
|
|
|
|
|
plugin_instance = plugin_class() |
|
|
|
|
|
|
|
|
if auto_initialize: |
|
|
plugin_instance.initialize() |
|
|
plugin_instance._initialized = True |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
try: |
|
|
plugin.cleanup() |
|
|
except Exception as e: |
|
|
logger.error(f"Error during plugin cleanup: {e}") |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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} |
|
|
) |
|
|
|
|
|
|
|
|
self.unload_plugin(plugin_name) |
|
|
|
|
|
|
|
|
importlib.reload( |
|
|
importlib.import_module(f"plugins.{module_name}") |
|
|
) |
|
|
|
|
|
|
|
|
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)})" |
|
|
|