From 0bdb3ce18996c6f9819bbd2d444152e48b966088 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sat, 14 Feb 2026 23:49:13 +0000 Subject: [PATCH] feat: Add plugin-to-plugin dependencies support Plugins can now declare dependencies on other plugins. NEW FEATURES: - dependencies['plugins'] = ['plugin_id1', 'plugin_id2'] - Separates pip packages (auto-installed) from plugin dependencies (user enabled) - Settings dialog shows which plugins need to be enabled first - PluginDependencyCheck tracks installed/enabled status EXAMPLE: dependencies = { 'pip': ['requests'], 'plugins': ['plugins.dashboard.plugin.DashboardPlugin'] } --- core/overlay_window.py | 24 ++++-- core/plugin_dependency_manager.py | 119 ++++++++++++++++++++++++------ plugins/base_plugin.py | 6 +- plugins/log_parser_test/plugin.py | 5 ++ 4 files changed, 127 insertions(+), 27 deletions(-) diff --git a/core/overlay_window.py b/core/overlay_window.py index 8bbb946..96a1d3f 100644 --- a/core/overlay_window.py +++ b/core/overlay_window.py @@ -1333,8 +1333,22 @@ class OverlayWindow(QMainWindow): if plugin_id in all_plugins: plugin_class = all_plugins[plugin_id] if dep_manager.has_dependencies(plugin_class): - installed, missing = dep_manager.check_all_dependencies(plugin_class) - if missing: + pip_installed, pip_missing, plugin_installed, plugin_missing = dep_manager.check_all_dependencies(plugin_class) + + # Check for plugin dependencies that need to be enabled first + if plugin_missing: + plugin_dep_text = dep_manager.get_plugin_dependencies_text(plugin_class) + from PyQt6.QtWidgets import QMessageBox + + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Plugin Dependencies Required") + msg_box.setText(f"Plugin '{plugin_class.name}' requires other plugins to be enabled:\n\n{plugin_dep_text}") + msg_box.setInformativeText("These plugins must be enabled before this plugin can work properly.") + msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) + msg_box.exec() + + # Handle pip package installation + if pip_missing: # Show dependency dialog dep_text = dep_manager.get_missing_dependencies_text(plugin_class) from PyQt6.QtWidgets import QMessageBox @@ -1360,14 +1374,14 @@ class OverlayWindow(QMainWindow): f"Installing dependencies for {plugin_class.name}...", "Cancel", 0, - len(missing), + len(pip_missing), self ) progress.setWindowModality(Qt.WindowModality.WindowModal) progress.setWindowTitle("Installing Dependencies") current = 0 - for dep in missing: + for dep in pip_missing: if progress.wasCanceled(): break @@ -1383,7 +1397,7 @@ class OverlayWindow(QMainWindow): ) current += 1 - progress.setValue(len(missing)) + progress.setValue(len(pip_missing)) # Reload plugins self._reload_plugins() diff --git a/core/plugin_dependency_manager.py b/core/plugin_dependency_manager.py index a46525d..038058b 100644 --- a/core/plugin_dependency_manager.py +++ b/core/plugin_dependency_manager.py @@ -19,10 +19,20 @@ class DependencyCheck: name: str required: bool installed: bool + dep_type: str = 'pip' # 'pip' or 'plugin' version: Optional[str] = None error: Optional[str] = None +@dataclass +class PluginDependencyCheck: + """Result of a plugin dependency check.""" + plugin_id: str + installed: bool + enabled: bool = False + error: Optional[str] = None + + class PluginDependencyManager: """Manages plugin dependencies with auto-install capability.""" @@ -78,26 +88,66 @@ class PluginDependencyManager: error=str(e) ) - def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck]]: - """Check all dependencies for a plugin. + def check_plugin_dependency(self, plugin_id: str) -> PluginDependencyCheck: + """Check if a required plugin is available and enabled.""" + if not self.plugin_manager: + return PluginDependencyCheck( + plugin_id=plugin_id, + installed=False, + error="Plugin manager not available" + ) + + # Check if plugin is discovered (available) + all_plugins = self.plugin_manager.get_all_discovered_plugins() + if plugin_id not in all_plugins: + return PluginDependencyCheck( + plugin_id=plugin_id, + installed=False, + error=f"Plugin '{plugin_id}' not found" + ) + + # Check if plugin is enabled + is_enabled = self.plugin_manager.is_plugin_enabled(plugin_id) + + return PluginDependencyCheck( + plugin_id=plugin_id, + installed=True, + enabled=is_enabled + ) + + def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck], List[PluginDependencyCheck], List[PluginDependencyCheck]]: + """Check all dependencies for a plugin (both pip and plugin dependencies). Returns: - Tuple of (installed_deps, missing_deps) + Tuple of (installed_pip, missing_pip, installed_plugins, missing_plugins) """ deps = getattr(plugin_class, 'dependencies', {}) pip_deps = deps.get('pip', []) + plugin_deps = deps.get('plugins', []) - installed = [] - missing = [] + # Check pip dependencies + pip_installed = [] + pip_missing = [] for dep in pip_deps: check = self.check_dependency(dep) if check.installed: - installed.append(check) + pip_installed.append(check) else: - missing.append(check) + pip_missing.append(check) - return installed, missing + # Check plugin dependencies + plugin_installed = [] + plugin_missing = [] + + for dep in plugin_deps: + check = self.check_plugin_dependency(dep) + if check.installed and check.enabled: + plugin_installed.append(check) + else: + plugin_missing.append(check) + + return pip_installed, pip_missing, plugin_installed, plugin_missing def install_dependency(self, package_name: str) -> Tuple[bool, str]: """Install a pip package. @@ -130,20 +180,20 @@ class PluginDependencyManager: return False, f"Error installing {package_name}: {e}" def install_all_missing(self, plugin_class, progress_callback=None) -> Tuple[bool, List[str]]: - """Install all missing dependencies for a plugin. + """Install all missing pip dependencies for a plugin. Returns: (all_successful, messages) """ - _, missing = self.check_all_dependencies(plugin_class) + _, pip_missing, _, _ = self.check_all_dependencies(plugin_class) - if not missing: - return True, ["All dependencies already installed"] + if not pip_missing: + return True, ["All pip dependencies already installed"] messages = [] all_success = True - for dep in missing: + for dep in pip_missing: if progress_callback: progress_callback(f"Installing {dep.name}...") @@ -156,22 +206,49 @@ class PluginDependencyManager: return all_success, messages def get_missing_dependencies_text(self, plugin_class) -> str: - """Get a formatted text of missing dependencies.""" - _, missing = self.check_all_dependencies(plugin_class) + """Get a formatted text of all missing dependencies (pip and plugin).""" + _, pip_missing, _, plugin_missing = self.check_all_dependencies(plugin_class) - if not missing: + lines = [] + + if pip_missing: + lines.append(f"Missing Python packages ({len(pip_missing)}):") + for dep in pip_missing: + lines.append(f" • {dep.name}") + + if plugin_missing: + lines.append(f"\nRequired plugins ({len(plugin_missing)}) - must be enabled:") + for dep in plugin_missing: + status = "not found" if not dep.installed else "disabled" + lines.append(f" • {dep.plugin_id} ({status})") + + if not lines: return "All dependencies are installed." - lines = [f"Missing dependencies ({len(missing)}):"] - for dep in missing: - lines.append(f" • {dep.name}") + return "\n".join(lines) + + def get_plugin_dependencies_text(self, plugin_class) -> str: + """Get formatted text of plugin dependencies specifically.""" + _, _, _, plugin_missing = self.check_all_dependencies(plugin_class) + + if not plugin_missing: + return "" + + lines = [f"This plugin requires the following plugins to be enabled:"] + for dep in plugin_missing: + lines.append(f" • {dep.plugin_id}") return "\n".join(lines) def has_dependencies(self, plugin_class) -> bool: - """Check if a plugin has declared dependencies.""" + """Check if a plugin has declared dependencies (pip or plugin).""" deps = getattr(plugin_class, 'dependencies', {}) - return bool(deps.get('pip', [])) + return bool(deps.get('pip', []) or deps.get('plugins', [])) + + def has_plugin_dependencies(self, plugin_class) -> bool: + """Check if a plugin depends on other plugins.""" + deps = getattr(plugin_class, 'dependencies', {}) + return bool(deps.get('plugins', [])) # Singleton instance diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 63708bd..6467a9c 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -29,7 +29,11 @@ class BasePlugin(ABC): enabled: bool = True # Dependencies - override in subclass - # Format: {'pip': ['package1', 'package2>=1.0'], 'optional': {'package3': 'description'}} + # Format: { + # 'pip': ['package1', 'package2>=1.0'], + # 'plugins': ['plugin_id1', 'plugin_id2'], # Other plugins this plugin requires + # 'optional': {'package3': 'description'} + # } dependencies: Dict[str, Any] = {} def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]): diff --git a/plugins/log_parser_test/plugin.py b/plugins/log_parser_test/plugin.py index 95f3a0e..bbfffe8 100644 --- a/plugins/log_parser_test/plugin.py +++ b/plugins/log_parser_test/plugin.py @@ -26,6 +26,11 @@ class LogParserTestPlugin(BasePlugin): author = "EU-Utility" description = "Debug tool for testing log parsing and event detection" + # Example: This plugin could depend on Game Reader Test for shared OCR utilities + # dependencies = { + # 'plugins': ['plugins.game_reader_test.plugin.GameReaderTestPlugin'] + # } + def __init__(self, overlay_window, config): super().__init__(overlay_window, config) self.event_counts = {