summaryrefslogtreecommitdiff
path: root/plugins/plugin_manager.py
diff options
context:
space:
mode:
authorChristian Kolset <christian.kolset@gmail.com>2026-08-02 01:34:43 -0600
committerChristian Kolset <christian.kolset@gmail.com>2026-08-02 01:34:43 -0600
commita3aa1df99df8f413cac2ba6020b7cd0dec6d2390 (patch)
treea4c5feca6b0db326d9e54f417abc132c1270a7a3 /plugins/plugin_manager.py
parentf5066a8ca2fb50aa3dddf2c8847e52574cdde6ad (diff)
parentf1aaffbc3eb1e2c154315c556d2555803eea7997 (diff)
Merge origin/main: reconcile ConfigWindow consolidation with Debug window + protocol updates
origin/main (23 commits) added a Debug window/log system on top of the old separate Devices/Channels/Plot windows, plus protocol fixes (cml, mark10, modbus_rtu, scpi) and device/profile changes. Local main (3 commits) replaced the separate windows with a unified ConfigWindow + dock panel. Kept local's ConfigWindow/dock architecture and ported the Debug window onto it: new toolbar button + _open_debug(), gated by developer_mode same as origin's version. Deduped the two independent "developer mode" toggles that had collided in SettingsWindow (origin's General-tab checkbox gating Debug window + device sim-mode visibility, local's Advanced-tab checkbox setting verbose logging) into one General-tab checkbox that does both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'plugins/plugin_manager.py')
-rw-r--r--plugins/plugin_manager.py40
1 files changed, 39 insertions, 1 deletions
diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py
index 36ae71a..5cefc2e 100644
--- a/plugins/plugin_manager.py
+++ b/plugins/plugin_manager.py
@@ -10,12 +10,14 @@ per-plugin state separately via get_save_state / apply_save_state.
from __future__ import annotations
+import importlib.metadata
import importlib.util
import json
import os
+import re
import sys
import traceback
-from dataclasses import dataclass
+from dataclasses import dataclass, field
from typing import Dict, List, Optional
from plugins.base_plugin import LabPlugin, PluginContext
@@ -35,9 +37,35 @@ class PluginManifest:
description: str = ""
author: str = ""
entry_point: str = "plugin.Plugin" # "module.ClassName" relative to plugin dir
+ requires: List[str] = field(default_factory=list) # pip-style reqs, e.g. "opencv-python>=4.8.0"
plugin_dir: str = ""
+def _dist_name(requirement: str) -> str:
+ """Extract the distribution name from a requirement string, e.g.
+ "opencv-python>=4.8.0" -> "opencv-python"."""
+ return re.split(r"[<>=!~\[; ]", requirement.strip(), maxsplit=1)[0]
+
+
+def missing_requirements(requires: List[str]) -> List[str]:
+ """Return the subset of `requires` whose distribution isn't installed.
+
+ Checked by distribution name via importlib.metadata (matches what pip
+ installed it as), not by import name — those differ for packages like
+ opencv-python (imports as cv2) or pyserial (imports as serial).
+ """
+ missing = []
+ for req in requires:
+ name = _dist_name(req)
+ if not name:
+ continue
+ try:
+ importlib.metadata.version(name)
+ except importlib.metadata.PackageNotFoundError:
+ missing.append(req)
+ return missing
+
+
# ── Manager ───────────────────────────────────────────────────────────────────
class PluginManager:
@@ -94,6 +122,7 @@ class PluginManager:
description = data.get("description", ""),
author = data.get("author", ""),
entry_point = data.get("entry_point", "plugin.Plugin"),
+ requires = data.get("requires", []),
plugin_dir = plugin_dir,
)
self._manifests[m.plugin_id] = m
@@ -146,6 +175,11 @@ class PluginManager:
print(f"[PluginManager] No manifest for '{plugin_id}'")
return None
+ missing = missing_requirements(manifest.requires)
+ if missing:
+ print(f"[Plugin] '{plugin_id}' missing dependencies: {', '.join(missing)}")
+ return None
+
module_name, class_name = manifest.entry_point.rsplit(".", 1)
module_file = os.path.join(
manifest.plugin_dir, *module_name.split("/")
@@ -218,6 +252,10 @@ class PluginManager:
def get_manifests(self) -> List[PluginManifest]:
return list(self._manifests.values())
+ def get_missing_dependencies(self, plugin_id: str) -> List[str]:
+ manifest = self._manifests.get(plugin_id)
+ return missing_requirements(manifest.requires) if manifest else []
+
def get_loaded(self) -> List[LabPlugin]:
return list(self._loaded.values())