pylance fixes and terminal plugin launching

This commit is contained in:
2026-09-01 21:09:45 -07:00
parent 05dc9203ba
commit 5d46d3b3e1
6 changed files with 79 additions and 51 deletions
+1 -1
View File
@@ -1337,7 +1337,7 @@ class MainApplication(QMainWindow):
def terminal_gui(self):
if self.terminal is None or not self.terminal.isVisible():
self.terminal = TerminalWindow(self)
self.terminal = TerminalWindow(self, self.plugin_manager)
self.terminal.show()
def update_optode_positions(self):
+8 -4
View File
@@ -1,6 +1,7 @@
"""
Filename: plugin_manager.py
Description: Manager file for anything plugin related
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
@@ -117,7 +118,7 @@ class PluginManager(QObject):
print(f"[PluginManager] {summary_msg}")
parent_widget = getattr(self, "main_window", None) or self
parent_widget = getattr(self, "main_window", None)
QMessageBox.warning(
parent_widget,
"Plugin Load Failures",
@@ -166,7 +167,9 @@ class PluginManager(QObject):
for action in menubar.actions():
clean_text = action.text().replace("&", "").strip().lower()
if clean_text == "plugins":
plugins_menu = action.menu()
menu = action.menu()
if isinstance(menu, QMenu):
plugins_menu = menu
break
# If it doesn't exist yet, create it
@@ -191,8 +194,9 @@ class PluginManager(QObject):
plugins_menu.addSeparator()
manager_action = plugins_menu.addAction("Manage Plugins...")
if hasattr(self.main_window, "plugins_gui") and callable(self.main_window.plugins_gui):
manager_action.triggered.connect(self.main_window.plugins_gui)
plugins_gui_func = getattr(self.main_window, "plugins_gui", None)
if callable(plugins_gui_func):
manager_action.triggered.connect(plugins_gui_func)
def unload_plugins(self) -> None:
"""Clears current active plugin instances."""
+1 -1
View File
@@ -1,4 +1,4 @@
src\analysis\participantfoldchannels.py 158
src\analysis\participantfoldchannels.py 84
src\shared\flaresbasewidget.py 1155
flares.py 2900
main_unit_tests.py 152
+28 -28
View File
@@ -8,29 +8,31 @@ License: GPL-3.0
# Built-in Imports
import os
from pathlib import Path
import time
import traceback
from pathlib import Path
from multiprocessing import Process, current_process, Manager
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
# External library imports
from matplotlib.backend_bases import Event
import numpy as np
from pandas import DataFrame
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.figure import Figure
from matplotlib.backend_bases import Event
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from PySide6.QtWidgets import QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QWidget, QDialog, QVBoxLayout
from PySide6.QtCore import QThread, Qt, QSize, QTimer, QObject, Signal
from PySide6.QtGui import QCloseEvent, QMouseEvent, QPixmap, QImage
from pandas import DataFrame
from mne.io.base import BaseRaw
from src.shared.flaresbasewidget import FlaresBaseWidget
from src.shared.shareddata import APP_NAME, resource_path
from flares import fold_channels
class MultiProgressDialog(QDialog):
@@ -82,7 +84,6 @@ def single_participant_worker(
""" Runs inside its own dedicated process """
p_name = os.path.basename(file_path)
try:
from flares import fold_channels
# Perform the heavy fold_channels logic
channel_results = fold_channels(raw=raw_data, p_name=p_name, progress_queue=progress_queue)
@@ -284,7 +285,7 @@ class StaticChannelCanvas(FigureCanvas):
class StandaloneLegendDialog(QWidget):
def __init__(self, canvas_engine, title_prefix, parent=None):
def __init__(self, canvas_engine: Any, title_prefix: str, parent: QWidget | None = None) -> None:
super().__init__(None)
self.setWindowTitle("Full View - Brodmann Legend")
self.setMinimumSize(500, 600)
@@ -302,7 +303,14 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
"""The Big Grid Canvas.
Dynamically scales row and column configurations to maintain a crisp 16:9 layout orientation.
"""
def __init__(self, channels_data, color_map, is_fullscreen_copy=False, parent=None):
def __init__(
self,
channels_data: dict[str, list[dict[str, Any]]],
color_map: dict[str, str],
is_fullscreen_copy: bool = False,
parent: QWidget | None = None
) -> None:
self.channels_data = channels_data
self.color_map = color_map
self.is_fullscreen_copy = is_fullscreen_copy
@@ -494,7 +502,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
self._fullscreen_refs.append(fullscreen_window)
def _calculate_total_brodmann_profile(self, channels_data: Dict[str, Any]):
def _calculate_total_brodmann_profile(self, channels_data: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]:
"""Sums and normalizes the specificity profile across all channels."""
totals = {}
num_channels = len(channels_data)
@@ -503,7 +511,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
return []
# Sum up specificities across all channels
for channel_name, data_list in channels_data.items():
for _, data_list in channels_data.items():
for entry in data_list:
landmark = entry['Landmark']
specificity = entry['Specificity']
@@ -532,7 +540,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
return normalized_data_list
def _open_expanded_view(self, channel_name, data_list):
def _open_expanded_view(self, channel_name: str, data_list: list[dict[str, Any]]) -> None:
# 1. Create a plain QWidget with NO parent (None)
# This instantly makes it a top-level desktop window
popup = QWidget(None)
@@ -746,11 +754,12 @@ class ProcessOrchestrator(QObject):
setup_finished = Signal(object, object, object, list)
setup_failed = Signal(str)
def __init__(self,
selected_files,
haemo_dict: dict[str, BaseRaw],
worker_func
):
def __init__(
self,
selected_files: list[str],
haemo_dict: dict[str, Any],
worker_func: Callable[..., Any]
) -> None:
super().__init__()
self.selected_files = selected_files
@@ -810,9 +819,6 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
self.index_texts = [
"0 (Fold Channels)",
# "1 (second image)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
@@ -888,13 +894,8 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
self.multi_progress.add_participant(os.path.basename(file_path), total_channels)
from datetime import datetime
print(f"Before: {datetime.now()}")
self.multi_progress.show()
print(f"After 1: {datetime.now()}")
if current_process().name == 'MainProcess':
# Create a clean background thread worker execution channel
self.orchestrator_thread = QThread()
@@ -914,7 +915,6 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
self.orchestrator.setup_failed.connect(self.orchestrator.deleteLater)
self.orchestrator_thread.start()
print(f"After 4: {datetime.now()}")
def on_orchestration_success(
self,
@@ -1102,7 +1102,7 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
vbox = QVBoxLayout(container)
title = QLabel("<b>Brodmann Area Legend</b>")
title.setAlignment(Qt.AlignCenter)
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
vbox.addWidget(title)
pixmap = self._bytes_to_pixmap(legend_bytes)
@@ -1110,10 +1110,10 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
# Legends are usually tall, so we scale it differently or keep it smaller
legend_label.setPixmap(pixmap.scaled(
self.thumb_size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
))
legend_label.setAlignment(Qt.AlignCenter)
legend_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
legend_label.mousePressEvent = lambda e, p=pixmap: self._open_full_size(p, "Brodmann Legend")
vbox.addWidget(legend_label)
+32 -16
View File
@@ -1,13 +1,15 @@
"""
Filename: plugins.py
Description: Plugins window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from typing import Any
from pathlib import Path
from typing import Any, cast
# External library imports
from PySide6.QtCore import Qt, QThread, Signal
@@ -104,12 +106,14 @@ class PluginsWindow(QWidget):
self._clear_details_panel()
return
name = info.get("name", "Unknown")
version = info.get("version", "1.0.0")
author = info.get("author", "Unknown")
desc = info.get("description", "No description provided.")
is_disabled = info.get("is_disabled", False)
path = info.get("path", "")
plugin_info = cast(dict[str, Any], info)
name = str(plugin_info.get("name", "Unknown"))
version = str(plugin_info.get("version", "1.0.0"))
author = str(plugin_info.get("author", "Unknown"))
desc = str(plugin_info.get("description", "No description provided."))
is_disabled = bool(plugin_info.get("is_disabled", False))
path = str(plugin_info.get("path", ""))
self.lbl_plugin_title.setText(name)
self.lbl_plugin_meta.setText(f"<b>Version:</b> {version} &nbsp;|&nbsp; <b>Author:</b> {author}")
@@ -134,25 +138,37 @@ class PluginsWindow(QWidget):
selected = self.installed_list.currentItem()
if not selected:
return
info = selected.data(Qt.ItemDataRole.UserRole)
if isinstance(info, dict) and "path" in info:
self.manager.toggle_plugin_state(info["path"])
raw_info = selected.data(Qt.ItemDataRole.UserRole)
if isinstance(raw_info, dict):
info = cast(dict[str, Any], raw_info)
plugin_path = info.get("path")
if isinstance(plugin_path, str) and plugin_path:
self.manager.toggle_plugin_state(Path(plugin_path))
def uninstall_plugin(self) -> None:
selected = self.installed_list.currentItem()
if not selected:
return
info = selected.data(Qt.ItemDataRole.UserRole)
if not isinstance(info, dict) or "path" not in info:
raw_info = selected.data(Qt.ItemDataRole.UserRole)
if not isinstance(raw_info, dict):
return
info = cast(dict[str, Any], raw_info)
plugin_path = info.get("path")
plugin_name = info.get("name", "this plugin")
if not isinstance(plugin_path, str) or not plugin_path:
return
reply = QMessageBox.question(
self, "Confirm Uninstall",
f"Are you sure you want to delete '{info['name']}'?",
self,
"Confirm Uninstall",
f"Are you sure you want to delete '{plugin_name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
self.manager.uninstall_plugin(info["path"])
self.manager.uninstall_plugin(Path(plugin_path))
def fetch_remote_plugins(self) -> None:
"""Asynchronously fetches remote plugins on a background thread."""
@@ -197,7 +213,7 @@ class PluginsWindow(QWidget):
p_id = plugin.get("id", "")
version = plugin.get("version", "v0.0")
desc = plugin.get("description", "")
platforms = plugin.get("platforms", [])
platforms = str(plugin.get("platforms", []))
min_v_str = plugin.get("min_app_version", "0.0.0")
is_platform_ok = not platforms or self.manager.current_platform in platforms
+9 -1
View File
@@ -17,8 +17,10 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit, QMainW
from PySide6.QtCore import QProcess, Qt, QThread, Signal
from file_ext_registration import register_file_association, is_windows_admin
from plugin_manager import PluginManager
from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME
from src.window.about import AboutWindow
from src.window.plugins import PluginsWindow
from updater import UpdateManager
@@ -42,7 +44,7 @@ class _AssocWorker(QThread):
class TerminalWindow(QWidget):
def __init__(self, parent: QWidget | None = None) -> None:
def __init__(self, parent: QWidget | None, plugin_manager: PluginManager) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"Terminal - {APP_NAME.upper()}")
self.resize(320, 180)
@@ -58,6 +60,7 @@ class TerminalWindow(QWidget):
self.setLayout(layout)
self._process: QProcess | None = None
self.plugin_manager = plugin_manager
self.commands: dict[str, Callable[..., Any]] = {
"hello": self.cmd_hello,
@@ -66,6 +69,7 @@ class TerminalWindow(QWidget):
"about": self.cmd_about,
"assoc": self.cmd_assoc,
"update": self.cmd_update,
"plugins": self.cmd_plugins,
"utest": self.cmd_utest,
}
@@ -122,6 +126,10 @@ class TerminalWindow(QWidget):
self.about = AboutWindow(self)
self.about.show()
def cmd_plugins(self, *args: Any) -> None:
self.about = PluginsWindow(self, self.plugin_manager)
self.about.show()
def cmd_update(self, *args: Any) -> str:
main_win = self.parent()
if not isinstance(main_win, QMainWindow):