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']
}
This commit is contained in:
LemonNexus 2026-02-14 23:49:13 +00:00
parent 706a5710a9
commit 0bdb3ce189
4 changed files with 127 additions and 27 deletions

View File

@ -1333,8 +1333,22 @@ class OverlayWindow(QMainWindow):
if plugin_id in all_plugins: if plugin_id in all_plugins:
plugin_class = all_plugins[plugin_id] plugin_class = all_plugins[plugin_id]
if dep_manager.has_dependencies(plugin_class): if dep_manager.has_dependencies(plugin_class):
installed, missing = dep_manager.check_all_dependencies(plugin_class) pip_installed, pip_missing, plugin_installed, plugin_missing = dep_manager.check_all_dependencies(plugin_class)
if missing:
# 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 # Show dependency dialog
dep_text = dep_manager.get_missing_dependencies_text(plugin_class) dep_text = dep_manager.get_missing_dependencies_text(plugin_class)
from PyQt6.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
@ -1360,14 +1374,14 @@ class OverlayWindow(QMainWindow):
f"Installing dependencies for {plugin_class.name}...", f"Installing dependencies for {plugin_class.name}...",
"Cancel", "Cancel",
0, 0,
len(missing), len(pip_missing),
self self
) )
progress.setWindowModality(Qt.WindowModality.WindowModal) progress.setWindowModality(Qt.WindowModality.WindowModal)
progress.setWindowTitle("Installing Dependencies") progress.setWindowTitle("Installing Dependencies")
current = 0 current = 0
for dep in missing: for dep in pip_missing:
if progress.wasCanceled(): if progress.wasCanceled():
break break
@ -1383,7 +1397,7 @@ class OverlayWindow(QMainWindow):
) )
current += 1 current += 1
progress.setValue(len(missing)) progress.setValue(len(pip_missing))
# Reload plugins # Reload plugins
self._reload_plugins() self._reload_plugins()

View File

@ -19,10 +19,20 @@ class DependencyCheck:
name: str name: str
required: bool required: bool
installed: bool installed: bool
dep_type: str = 'pip' # 'pip' or 'plugin'
version: Optional[str] = None version: Optional[str] = None
error: 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: class PluginDependencyManager:
"""Manages plugin dependencies with auto-install capability.""" """Manages plugin dependencies with auto-install capability."""
@ -78,26 +88,66 @@ class PluginDependencyManager:
error=str(e) error=str(e)
) )
def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck]]: def check_plugin_dependency(self, plugin_id: str) -> PluginDependencyCheck:
"""Check all dependencies for a plugin. """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: Returns:
Tuple of (installed_deps, missing_deps) Tuple of (installed_pip, missing_pip, installed_plugins, missing_plugins)
""" """
deps = getattr(plugin_class, 'dependencies', {}) deps = getattr(plugin_class, 'dependencies', {})
pip_deps = deps.get('pip', []) pip_deps = deps.get('pip', [])
plugin_deps = deps.get('plugins', [])
installed = [] # Check pip dependencies
missing = [] pip_installed = []
pip_missing = []
for dep in pip_deps: for dep in pip_deps:
check = self.check_dependency(dep) check = self.check_dependency(dep)
if check.installed: if check.installed:
installed.append(check) pip_installed.append(check)
else: 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]: def install_dependency(self, package_name: str) -> Tuple[bool, str]:
"""Install a pip package. """Install a pip package.
@ -130,20 +180,20 @@ class PluginDependencyManager:
return False, f"Error installing {package_name}: {e}" return False, f"Error installing {package_name}: {e}"
def install_all_missing(self, plugin_class, progress_callback=None) -> Tuple[bool, List[str]]: 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: Returns:
(all_successful, messages) (all_successful, messages)
""" """
_, missing = self.check_all_dependencies(plugin_class) _, pip_missing, _, _ = self.check_all_dependencies(plugin_class)
if not missing: if not pip_missing:
return True, ["All dependencies already installed"] return True, ["All pip dependencies already installed"]
messages = [] messages = []
all_success = True all_success = True
for dep in missing: for dep in pip_missing:
if progress_callback: if progress_callback:
progress_callback(f"Installing {dep.name}...") progress_callback(f"Installing {dep.name}...")
@ -156,22 +206,49 @@ class PluginDependencyManager:
return all_success, messages return all_success, messages
def get_missing_dependencies_text(self, plugin_class) -> str: def get_missing_dependencies_text(self, plugin_class) -> str:
"""Get a formatted text of missing dependencies.""" """Get a formatted text of all missing dependencies (pip and plugin)."""
_, missing = self.check_all_dependencies(plugin_class) _, 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." return "All dependencies are installed."
lines = [f"Missing dependencies ({len(missing)}):"] return "\n".join(lines)
for dep in missing:
lines.append(f"{dep.name}") 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) return "\n".join(lines)
def has_dependencies(self, plugin_class) -> bool: 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', {}) 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 # Singleton instance

View File

@ -29,7 +29,11 @@ class BasePlugin(ABC):
enabled: bool = True enabled: bool = True
# Dependencies - override in subclass # 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] = {} dependencies: Dict[str, Any] = {}
def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]): def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]):

View File

@ -26,6 +26,11 @@ class LogParserTestPlugin(BasePlugin):
author = "EU-Utility" author = "EU-Utility"
description = "Debug tool for testing log parsing and event detection" 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): def __init__(self, overlay_window, config):
super().__init__(overlay_window, config) super().__init__(overlay_window, config)
self.event_counts = { self.event_counts = {