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:
parent
706a5710a9
commit
0bdb3ce189
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]):
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue