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