From 45ff7227fabb97273d4645601733f90f5f744eb1 Mon Sep 17 00:00:00 2001 From: Christian Kolset Date: Wed, 29 Jul 2026 13:21:05 -0600 Subject: Check plugin dependencies before loading, warn instead of silent console error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manifest.json already had an unused "requires" field (motion_capture's manifest lists opencv-python>=4.8.0) — PluginManifest never parsed it, so a missing dependency just crashed the import inside _load_plugin(), caught by the broad except and only ever printed to the console. Adds PluginManifest.requires, PluginManager.missing_requirements()/ get_missing_dependencies(), and an early check in _load_plugin() that skips the risky import entirely when a requirement is missing. main_window.plugin_enable() now checks this before calling PluginManager.enable() and shows a QMessageBox with the missing packages and a pip install command instead of failing silently. Checks by distribution name via importlib.metadata (what pip installed it as), not import name — those differ for packages like opencv-python (imports as cv2) or pyserial (imports as serial), so importlib.util. find_spec() would give false negatives. Also fixes a button-state bug this surfaces: SettingsWindow's plugin toggle optimistically flipped to "Disable" the instant Enable was clicked, before knowing whether enabling actually succeeds — pre-existing, but now trivially reproducible (any plugin missing a dependency). Enable no longer flips the button immediately; main_window calls the new sync_plugin_button() once the real outcome is known. Co-Authored-By: Claude Sonnet 5 --- ui/main_window.py | 20 ++++++++++++++++---- ui/windows/settings_window.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) (limited to 'ui') diff --git a/ui/main_window.py b/ui/main_window.py index d0e0ac4..95cdb84 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -296,10 +296,22 @@ class MainWindow(QMainWindow): def plugin_enable(self, plugin_id: str): """Called by SettingsWindow when user enables a plugin.""" - ctx = self._make_plugin_context() - plugin = self._plugin_mgr.enable(plugin_id, ctx) - if plugin: - self._install_plugin(plugin) + missing = self._plugin_mgr.get_missing_dependencies(plugin_id) + if missing: + from PyQt6.QtWidgets import QMessageBox + QMessageBox.warning( + self, "Missing Plugin Dependencies", + f"Can't enable this plugin — missing Python packages:\n\n" + f" {', '.join(missing)}\n\n" + f"Install with:\n pip install {' '.join(missing)}" + ) + else: + ctx = self._make_plugin_context() + plugin = self._plugin_mgr.enable(plugin_id, ctx) + if plugin: + self._install_plugin(plugin) + if self._win_settings: + self._win_settings.sync_plugin_button(plugin_id) def plugin_disable(self, plugin_id: str): """Called by SettingsWindow when user disables a plugin.""" diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py index 1630efa..553a3a7 100644 --- a/ui/windows/settings_window.py +++ b/ui/windows/settings_window.py @@ -50,6 +50,7 @@ class SettingsWindow(QWidget): self.registry = registry self.engine = engine self._plugin_mgr = plugin_manager + self._plugin_buttons: dict = {} # plugin_id -> QPushButton self.cfg = dict(self._defaults) if current: self.cfg.update(current) @@ -233,6 +234,7 @@ class SettingsWindow(QWidget): toggle.clicked.connect( lambda _, pid=manifest.plugin_id, btn=toggle: self._toggle_plugin(pid, btn) ) + self._plugin_buttons[manifest.plugin_id] = toggle hdr.addWidget(toggle) cl.addLayout(hdr) @@ -261,8 +263,16 @@ class SettingsWindow(QWidget): self.plugin_disable_requested.emit(plugin_id) btn.setText("Enable") else: + # Don't flip to "Disable" yet — enabling can fail (missing + # dependencies, bad plugin code). main_window confirms the + # real outcome via sync_plugin_button() once enable() returns. self.plugin_enable_requested.emit(plugin_id) - btn.setText("Disable") + + def sync_plugin_button(self, plugin_id: str): + """Refresh one plugin's toggle button to match its actual enabled state.""" + btn = self._plugin_buttons.get(plugin_id) + if btn is not None and self._plugin_mgr is not None: + btn.setText("Disable" if self._plugin_mgr.is_enabled(plugin_id) else "Enable") # ── Actions ─────────────────────────────────────────────────────────── -- cgit v1.2.3