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:
|
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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]):
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue