326 lines
11 KiB
Python
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()
|