EU-Utility/core/dependency_helper.py

326 lines
11 KiB
Python

"""
EU-Utility - Dependency Helper (Core Framework Component)
Built-in dependency troubleshooting - not a plugin.
"""
import subprocess
import sys
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
@dataclass
class DependencyCheck:
"""Dependency check result."""
name: str
required: bool
installed: bool
version: Optional[str] = None
fix_command: Optional[str] = None
error_message: Optional[str] = None
class DependencyHelper:
"""Dependency helper - built into the framework.
Checks and helps install missing dependencies for plugins.
"""
CORE_DEPENDENCIES = [
('PyQt6', 'PyQt6', True),
('requests', 'requests', True),
('Pillow', 'Pillow', True),
]
OPTIONAL_DEPENDENCIES = [
('pytesseract', 'pytesseract', False),
('easyocr', 'easyocr', False),
('paddleocr', 'paddleocr', False),
('psutil', 'psutil', False),
('keyboard', 'keyboard', False),
('win32gui', 'pywin32', False),
]
def __init__(self):
self.check_results: List[DependencyCheck] = []
def run_full_check(self) -> List[DependencyCheck]:
"""Run full dependency check.
Returns:
List of dependency check results
"""
self.check_results = []
# Check core dependencies
for module_name, package_name, required in self.CORE_DEPENDENCIES:
result = self._check_module(module_name, package_name, required)
self.check_results.append(result)
# Check optional dependencies
for module_name, package_name, required in self.OPTIONAL_DEPENDENCIES:
result = self._check_module(module_name, package_name, required)
self.check_results.append(result)
return self.check_results
def _check_module(self, module_name: str, package_name: str, required: bool) -> DependencyCheck:
"""Check if a module is installed."""
try:
module = __import__(module_name)
version = getattr(module, '__version__', 'unknown')
return DependencyCheck(
name=package_name,
required=required,
installed=True,
version=version
)
except ImportError:
return DependencyCheck(
name=package_name,
required=required,
installed=False,
fix_command=f"pip install {package_name}"
)
def check_plugin_dependencies(self, plugin_class) -> List[DependencyCheck]:
"""Check dependencies for a specific plugin.
Args:
plugin_class: Plugin class to check
Returns:
List of dependency check results
"""
results = []
deps = getattr(plugin_class, 'dependencies', {})
pip_deps = deps.get('pip', [])
for dep in pip_deps:
# Parse package name (could be "package>=1.0")
package_name = dep.split('>=')[0].split('==')[0].split('<')[0].strip()
try:
__import__(package_name.lower().replace('-', '_'))
results.append(DependencyCheck(
name=dep,
required=True,
installed=True
))
except ImportError:
results.append(DependencyCheck(
name=dep,
required=True,
installed=False,
fix_command=f"pip install {dep}"
))
return results
def install_dependency(self, package_name: str) -> Tuple[bool, str]:
"""Install a dependency.
Args:
package_name: Package to install
Returns:
(success, message)
"""
try:
result = subprocess.run(
[sys.executable, '-m', 'pip', 'install', package_name],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
return True, f"Successfully installed {package_name}"
else:
return False, f"Installation failed: {result.stderr}"
except subprocess.TimeoutExpired:
return False, "Installation timed out"
except Exception as e:
return False, f"Error: {str(e)}"
def get_missing_core_deps(self) -> List[DependencyCheck]:
"""Get list of missing core dependencies."""
if not self.check_results:
self.run_full_check()
return [r for r in self.check_results if r.required and not r.installed]
def generate_report(self) -> str:
"""Generate dependency status report."""
if not self.check_results:
self.run_full_check()
lines = ["# Dependency Report\n"]
lines.append("## Core Dependencies\n")
for result in self.check_results:
if result.required:
status = "" if result.installed else ""
version = f" ({result.version})" if result.version else ""
lines.append(f"{status} {result.name}{version}")
lines.append("\n## Optional Dependencies\n")
for result in self.check_results:
if not result.required:
status = "" if result.installed else "⚠️"
version = f" ({result.version})" if result.version else ""
lines.append(f"{status} {result.name}{version}")
missing = self.get_missing_core_deps()
if missing:
lines.append("\n## Missing Required Dependencies\n")
lines.append("Install with:\n")
for dep in missing:
lines.append(f" {dep.fix_command}")
return "\n".join(lines)
def troubleshoot_plugin(self, plugin_id: str, plugin_manager) -> List[str]:
"""Troubleshoot a plugin's issues.
Returns:
List of issues found
"""
issues = []
# Get plugin class
all_plugins = plugin_manager.get_all_discovered_plugins()
plugin_class = all_plugins.get(plugin_id)
if not plugin_class:
issues.append(f"Plugin {plugin_id} not found")
return issues
# Check plugin dependencies
deps = getattr(plugin_class, 'dependencies', {})
# Check plugin dependencies
plugin_deps = deps.get('plugins', [])
for dep_id in plugin_deps:
if not plugin_manager.is_plugin_enabled(dep_id):
dep_class = all_plugins.get(dep_id)
dep_name = dep_class.name if dep_class else dep_id
issues.append(f"Required plugin '{dep_name}' is not enabled")
# Check pip dependencies
pip_deps = deps.get('pip', [])
for dep in pip_deps:
package_name = dep.split('>=')[0].split('==')[0].split('<')[0].strip()
try:
__import__(package_name.lower().replace('-', '_'))
except ImportError:
issues.append(f"Missing Python package: {dep}")
return issues
class DependencyHelperDialog:
"""UI dialog for dependency helper."""
@staticmethod
def show_dependency_check(parent=None):
"""Show dependency check dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTextEdit, QListWidget, QListWidgetItem
)
helper = DependencyHelper()
results = helper.run_full_check()
dialog = QDialog(parent)
dialog.setWindowTitle("Dependency Check")
dialog.setMinimumSize(500, 400)
dialog.setStyleSheet("""
QDialog {
background-color: #1a1f2e;
}
QLabel {
color: white;
}
""")
layout = QVBoxLayout(dialog)
# Status summary
missing_core = [r for r in results if r.required and not r.installed]
missing_optional = [r for r in results if not r.required and not r.installed]
if missing_core:
status = QLabel(f"{len(missing_core)} required dependency(s) missing!")
status.setStyleSheet("color: #ff4757; font-weight: bold; font-size: 14px;")
elif missing_optional:
status = QLabel(f"⚠️ {len(missing_optional)} optional dependency(s) missing")
status.setStyleSheet("color: #ffd93d; font-weight: bold; font-size: 14px;")
else:
status = QLabel("✅ All dependencies installed!")
status.setStyleSheet("color: #4ecdc4; font-weight: bold; font-size: 14px;")
layout.addWidget(status)
# Results list
list_widget = QListWidget()
list_widget.setStyleSheet("""
QListWidget {
background-color: #232837;
color: white;
border: 1px solid rgba(100, 110, 130, 80);
}
QListWidget::item {
padding: 8px;
}
""")
for result in results:
if result.required and not result.installed:
icon = ""
color = "#ff4757"
elif not result.installed:
icon = "⚠️"
color = "#ffd93d"
else:
icon = ""
color = "#4ecdc4"
version = f" ({result.version})" if result.version else ""
item_text = f"{icon} {result.name}{version}"
item = QListWidgetItem(item_text)
item.setForeground(Qt.GlobalColor.white)
list_widget.addItem(item)
layout.addWidget(list_widget)
# Buttons
btn_layout = QHBoxLayout()
install_btn = QPushButton("📥 Install Missing")
install_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
""")
btn_layout.addWidget(install_btn)
close_btn = QPushButton("Close")
close_btn.clicked.connect(dialog.close)
btn_layout.addWidget(close_btn)
layout.addLayout(btn_layout)
dialog.exec()