diff --git a/main.py b/main.py
index 7dea30f..1de2d47 100644
--- a/main.py
+++ b/main.py
@@ -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):
diff --git a/plugin_manager.py b/plugin_manager.py
index 2c43948..d1c81af 100644
--- a/plugin_manager.py
+++ b/plugin_manager.py
@@ -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."""
diff --git a/pylance_progress b/pylance_progress
index 96d8148..fda4f96 100644
--- a/pylance_progress
+++ b/pylance_progress
@@ -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
diff --git a/src/analysis/participantfoldchannels.py b/src/analysis/participantfoldchannels.py
index 6b6c6cc..d10c5be 100644
--- a/src/analysis/participantfoldchannels.py
+++ b/src/analysis/participantfoldchannels.py
@@ -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("Brodmann Area Legend")
- 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)
diff --git a/src/window/plugins.py b/src/window/plugins.py
index a7dd65c..82a8314 100644
--- a/src/window/plugins.py
+++ b/src/window/plugins.py
@@ -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"Version: {version} | Author: {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
diff --git a/src/window/terminal.py b/src/window/terminal.py
index a0c3b73..66b2775 100644
--- a/src/window/terminal.py
+++ b/src/window/terminal.py
@@ -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):