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:
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()

View File

@ -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

View File

@ -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]):

View File

@ -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 = {