pylance standardization

This commit is contained in:
2026-07-21 00:54:51 -07:00
parent 2b019c1bc0
commit 8b017005c5
20 changed files with 608 additions and 277 deletions
+24 -4
View File
@@ -1,20 +1,28 @@
"""
Filename: crossgroupbrainimage.py
Description: Logic for the Cross-Group Brain & Image analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in Imports
from pathlib import Path
from typing import Any, cast
# External library imports
from mne.io.base import BaseRaw
import pandas as pd
from pandas import DataFrame
from flares import aggregate_fnirs_group_geometry, plot_2d_3d_contrasts_between_groups
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "show_optodes",
@@ -52,7 +60,15 @@ PARAMETERIZED_INDEXES = {
class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str],
) -> None:
super().__init__("CrossGroupBrainImage")
self.setWindowTitle(f"Cross-Group Brain & Image Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -70,8 +86,9 @@ class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
if request is None:
return
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
# Build group-level contrast DataFrames
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
@@ -102,8 +119,11 @@ class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
if len(all_raw_objs) > 1:
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
elif len(all_raw_objs) == 1 and all_raw_objs[0] is not None:
processed_raw = all_raw_objs[0].copy()
processed_raw.pick(picks="hbo") # type: ignore
else:
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
processed_raw = None
# Visualizations
for idx in selected_indexes:
+49 -25
View File
@@ -1,20 +1,28 @@
"""
Filename: crossgroupstats.py
Description: Cross-Group stats analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, cast
# External library imports
import pandas as pd
from pandas import DataFrame
from mne.io.base import BaseRaw
from flares import run_cross_group_contrast_analysis, run_cross_group_laterality_analysis, run_cross_group_second_level_analysis
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "p_threshold",
@@ -136,7 +144,18 @@ DESCRIPTION = """0. Raw ROI Comparison (run_cross_group_second_level_analysis)
class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str],
json_location: str | Path
) -> None:
super().__init__("CrossGroupStats")
self.setWindowTitle(f"Cross-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -144,7 +163,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
# self.group_dict = group_dict
self.json_location = json_location
self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION)
@@ -155,23 +174,18 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
if request is None:
return
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
(selected_event, file_paths_a, file_paths_b, _, selected_indexes, raw_params) = request
if isinstance(self.df_ind_dict, dict):
# Filter out empty entries and concatenate
valid_dfs = [df for df in self.df_ind_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
if valid_dfs:
df_ind_combined = pd.concat(valid_dfs, ignore_index=True)
else:
df_ind_combined = pd.DataFrame()
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
valid_dfs = [df for df in self.df_ind_dict.values() if not df.empty]
if valid_dfs:
df_ind_combined = pd.concat(valid_dfs, ignore_index=True)
else:
df_ind_combined = self.df_ind_dict
if isinstance(self.cha_dict, dict):
valid_chas = [df for df in self.cha_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
cha_combined = pd.concat(valid_chas, ignore_index=True) if valid_chas else pd.DataFrame()
else:
cha_combined = self.cha_dict
df_ind_combined = pd.DataFrame()
valid_chas = [df for df in self.cha_dict.values() if not df.empty]
cha_combined = pd.concat(valid_chas, ignore_index=True) if valid_chas else pd.DataFrame()
sample_path = file_paths_a[0]
p_haemo = self.haemo_dict.get(sample_path)
@@ -213,8 +227,8 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
min_subjects = params.get("min_subjects", 3)
correction_method = params.get("correction_method", "None")
target_chroma = params.get("target_chroma", "hbo")
roi_a = params.get("roi_a", "").strip()
roi_b = params.get("roi_b", "").strip()
roi_a: str = params.get("roi_a", "").strip()
roi_b: str = params.get("roi_b", "").strip()
if not roi_a or not roi_b:
print("Both a contralateral and ipsilateral ROI name must be specified.")
@@ -225,14 +239,19 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
# Build each group's dataframe directly from the dict using
# the file-path lists as keys - no ID cleaning/matching needed.
def _build_group_df(file_paths, dict_source):
def _build_group_df(
file_paths: list[str],
dict_source: dict[str, DataFrame]
) -> DataFrame:
valid_dfs = [
dict_source[fp] for fp in file_paths
if fp in dict_source and isinstance(dict_source[fp], pd.DataFrame)
and not dict_source[fp].empty
if fp in dict_source and not dict_source[fp].empty
]
return pd.concat(valid_dfs, ignore_index=True) if valid_dfs else pd.DataFrame()
df_roi_a = _build_group_df(file_paths_a, self.df_ind_dict)
df_roi_b = _build_group_df(file_paths_b, self.df_ind_dict)
@@ -271,8 +290,13 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
# directly from contrast_results_dict, keyed by file path -
# same dict-key approach as the laterality patch, avoids
# any ID-string matching.
def _build_group_contrast_df(file_paths, contrast_dict, name):
all_rows = []
def _build_group_contrast_df(
file_paths: list[str],
contrast_dict: dict[str, dict[str, pd.DataFrame]],
name: str,
) -> pd.DataFrame:
all_rows: list[DataFrame] = []
for fp in file_paths:
condition_dfs = contrast_dict.get(fp)
if condition_dfs is None:
+26 -33
View File
@@ -1,6 +1,7 @@
"""
Filename: exporttocsv.py
Description: Logic for the Export To CSV analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
@@ -8,39 +9,51 @@ License: GPL-3.0
# Built-in imports
import os
from pathlib import Path
from typing import Any
# External library imports
import numpy as np
import pandas as pd
from pandas import DataFrame
from mne.io.base import BaseRaw
from PySide6.QtWidgets import QFileDialog, QMessageBox
from flares import sparks_csv_export
from src.shared.flaresbasewidget import CSVUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str],
) -> None:
super().__init__("ExportToCSV")
self.setWindowTitle(f"Export To CSV Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
self.df_ind = df_ind
self.design_matrix = design_matrix
self.group = group
self.contrast_results_dict = contrast_results_dict
# self.df_ind = df_ind_dict
# self.design_matrix = design_matrix_dict
# self.contrast_results_dict = contrast_results_dict
# self.group = group_dict
self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)",])
def process_request(self):
# TODO: Move this into flares for the call?
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
selected_file_paths: list[str] = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
if display_name == expected_display:
selected_file_paths.append(fp)
break
@@ -52,7 +65,6 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
return
# 2. ASK ONCE: Select Output Directory
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
if not output_dir:
@@ -78,29 +90,11 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
cha.to_csv(save_path)
success_count += 1
elif idx == 1:
# SPARKS Export
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
if haemo_obj is not None:
raw = haemo_obj
data, times = raw.get_data(return_times=True)
ann_col = np.full(times.shape, "", dtype=object)
if raw.annotations is not None and len(raw.annotations) > 0:
for onset, duration, desc in zip(
raw.annotations.onset,
raw.annotations.duration,
raw.annotations.description
):
mask = (times >= onset) & (times < onset + duration)
ann_col[mask] = desc
df = pd.DataFrame(data.T, columns=raw.ch_names)
df.insert(0, "annotation", ann_col)
df.insert(0, "time", times)
df.to_csv(save_path, index=False)
success_count += 1
sparks_csv_export(haemo_obj, save_path)
success_count += 1
else:
print(f"No method defined for index {idx}")
@@ -119,5 +113,4 @@ class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
# mode=EventUpdateMode.WRITE_JSON,
# caller="Video Alignment Tool"
# )
# win.show()
# win.show()
+55 -24
View File
@@ -1,20 +1,29 @@
"""
Filename: intergroupbrainimage.py
Description: Logic for the Inter-Group Brain & Image analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in Imports
from pathlib import Path
from typing import Any, cast
# External library imports
import pandas as pd
from pandas import DataFrame
from mne import Annotations
from mne.io.base import BaseRaw
from flares import aggregate_fnirs_group_geometry, plot_fir_model_results, brain_3d_visualization
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
from mne.io import BaseRaw
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "lower_bound",
@@ -74,15 +83,24 @@ PARAMETERIZED_INDEXES = {
class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str]
) -> None:
super().__init__("InterGroupBrainImage")
self.setWindowTitle(f"Inter-Group Brain & Image Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.cha_dict = cha_dict
self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
# self.group_dict = group_dict
self.setup_inter_group_ui(["0 (GLM Results)", "1 (Significance)", "2 (Brain Activity Visualization)",])
@@ -92,35 +110,45 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
all_cha = pd.DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if haemo_obj is None:
continue
if selected_event:
participant_events = set(haemo_obj.annotations.description)
raw_annotations = getattr(haemo_obj, "annotations", None)
if raw_annotations is not None:
annotations = cast(Annotations, raw_annotations)
descriptions = cast(list[str], list(annotations.description))
participant_events: set[str] = set(descriptions)
else:
participant_events: set[str] = set()
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
cha_df = self.cha_dict.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
# Pass the necessary arguments to each method
file_path = selected_file_paths[0]
p_haemo = self.haemo_dict.get(file_path)
p_design_matrix = self.design_matrix.get(file_path)
p_design_matrix = self.design_matrix_dict.get(file_path)
df_group = pd.DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
df = self.df_ind_dict.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
@@ -147,9 +175,9 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
print(f"Missing parameters for index {idx}, skipping.")
continue
all_contrasts = []
all_contrasts: list[DataFrame] = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp, {})
condition_dfs = self.contrast_results_dict.get(fp, {})
if selected_event in condition_dfs:
df = condition_dfs[selected_event].copy()
df["ID"] = fp
@@ -159,7 +187,8 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
print("No contrast data found for selected participants and event.")
return
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
# TODO: look at intergroupstats and figure out what to do
_ = pd.concat(all_contrasts, ignore_index=True)
#flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
elif idx == 2:
@@ -173,13 +202,15 @@ class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
print(f"Missing parameters for index {idx}, skipping.")
continue
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
all_raw_objs = [self.haemo_dict.get(fp) for fp in selected_file_paths if self.haemo_dict.get(fp)]
if len(selected_file_paths) > 1:
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
processed_raw = aggregate_fnirs_group_geometry(raw_list)
if len(all_raw_objs) > 1:
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
elif len(all_raw_objs) == 1 and all_raw_objs[0] is not None:
processed_raw = all_raw_objs[0].copy()
processed_raw.pick(picks="hbo") # type: ignore
else:
processed_raw = raw_list[0].copy().pick(picks="hbo")
processed_raw = None
brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
@@ -1,20 +1,27 @@
"""
Filename: intergroupfunctionalconnectivity.py
Description: Logic for the Inter-Group Functional Connectivity analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, cast
# External library imports
from PySide6.QtWidgets import QMessageBox
from mne.io.base import BaseRaw
from flares import run_group_functional_connectivity
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "n_lines",
@@ -34,11 +41,17 @@ PARAMETERIZED_INDEXES = {
class InterGroupFunctionalConnectivityWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, group, config_dict):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
group_dict: dict[str, str],
config_dict: dict[str, str]
) -> None:
super().__init__("InterGroupFunctionalConnectivity")
self.setWindowTitle(f"Inter-Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.group = group
#self.group_dict = group_dict
self.config_dict = config_dict
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in development and the results should currently be taken with a grain of salt. "
@@ -52,7 +65,9 @@ class InterGroupFunctionalConnectivityWidget(InterGroupUIMixin, FlaresBaseWidget
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
for idx in selected_indexes:
if idx == 0:
+52 -19
View File
@@ -1,20 +1,29 @@
"""
Filename: intergroupstats.py
Description: Logic for the Inter-Group Stats analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, cast
# External library imports
import pandas as pd
from pandas import DataFrame
from mne import Annotations
from mne.io.base import BaseRaw
from flares import run_roi_paired_contrast_analysis, run_roi_second_level_analysis, aggregate_channel_contrasts_to_roi
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "p_threshold",
@@ -148,40 +157,64 @@ DESCRIPTION = """0. ROI vs. Zero (run_roi_second_level_analysis)
class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group, json_location):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]],
group_dict: dict[str, str],
json_location: str | Path
) -> None:
super().__init__("InterGroupStats")
self.setWindowTitle(f"Inter-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.cha_dict = cha_dict
self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
self.json_location = json_location
self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION)
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results)
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results_dict)
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
all_cha = pd.DataFrame()
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
all_cha = DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if haemo_obj is None:
continue
if selected_event:
participant_events = set(haemo_obj.annotations.description)
raw_annotations = getattr(haemo_obj, "annotations", None)
if raw_annotations is not None:
annotations = cast(Annotations, raw_annotations)
descriptions = cast(list[str], list(annotations.description))
participant_events: set[str] = set(descriptions)
else:
participant_events: set[str] = set()
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
cha_df = self.cha_dict.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
@@ -189,10 +222,10 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
p_haemo = self.haemo_dict.get(file_path)
# Concatenate individual ROI stats (df_ind) for all chosen subjects
df_group = pd.DataFrame()
df_group = DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
df = self.df_ind_dict.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
@@ -226,7 +259,7 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
print(f"No ROI data matches the condition '{selected_event}'.")
continue
all_cha_filtered = pd.DataFrame()
all_cha_filtered = DataFrame()
if not all_cha.empty:
if selected_event and 'Condition' in all_cha.columns:
all_cha_filtered = all_cha[all_cha['Condition'] == selected_event]
@@ -304,9 +337,9 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
continue
all_contrasts = []
all_contrasts: list[DataFrame] = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp)
condition_dfs = self.contrast_results_dict.get(fp)
if condition_dfs is None:
print(f" [MISSING] '{fp}' not found in contrast_results.")
continue
+32 -7
View File
@@ -1,18 +1,28 @@
"""
Filename: participantbrain.py
Description: Logic for the Participant Brain analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, cast
# External library imports
from mne import Annotations
from pandas import DataFrame
from mne.io.base import BaseRaw
from flares import brain_3d_visualization, brain_landmarks_3d
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "show_optodes",
@@ -57,7 +67,12 @@ PARAMETERIZED_INDEXES = {
class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
cha_dict: dict[str, DataFrame],
) -> None:
super().__init__("ParticipantBrain")
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -72,21 +87,31 @@ class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if haemo_obj is None:
continue
if selected_event:
participant_events = set(haemo_obj.annotations.description)
raw_annotations = getattr(haemo_obj, "annotations", None)
if raw_annotations is not None:
annotations = cast(Annotations, raw_annotations)
descriptions = cast(list[str], list(annotations.description))
participant_events: set[str] = set(descriptions)
else:
participant_events: set[str] = set()
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
raise Exception("How did we get here?")
cha = self.cha_dict.get(file_path)
for idx in selected_indexes:
+1 -1
View File
@@ -71,7 +71,7 @@ def single_participant_worker(file_path, raw_data, result_queue, progress_queue)
try:
from flares import fold_channels
# Perform the heavy fold_channels logic
channel_results = fold_channels(raw_data, p_name, progress_queue)
channel_results = fold_channels(raw=raw_data, p_name=p_name, progress_queue=progress_queue)
# Hand back results and signal completion
result_queue.put({file_path: channel_results})
@@ -1,20 +1,30 @@
"""
Filename: participantfunctionalconnectivity.py
Description: Logic for the Participant Functional Connectivity analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in Imports
from pathlib import Path
from typing import Any, cast
# External library imports
from PySide6.QtWidgets import QMessageBox
from pandas import DataFrame
from mne import Annotations
from mne.io.base import BaseRaw
from flares import functional_connectivity_betas, functional_connectivity_envelope, functional_connectivity_spectral_epochs, functional_connectivity_spectral_time
from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [
{
"key": "n_lines",
@@ -79,7 +89,12 @@ PARAMETERIZED_INDEXES = {
class ParticipantFunctionalConnectivityWidget(ParticipantUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, epochs_dict):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
epochs_dict: dict[str, DataFrame],
) -> None:
super().__init__("ParticipantFunctionalConnectivity")
self.setWindowTitle(f"Participant Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
@@ -97,22 +112,32 @@ class ParticipantFunctionalConnectivityWidget(ParticipantUIMixin, FlaresBaseWidg
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
epochs_obj = self.epochs_dict.get(file_path)
if haemo_obj is None:
continue
if selected_event:
participant_events = set(haemo_obj.annotations.description)
raw_annotations = getattr(haemo_obj, "annotations", None)
if raw_annotations is not None:
annotations = cast(Annotations, raw_annotations)
descriptions = cast(list[str], list(annotations.description))
participant_events: set[str] = set(descriptions)
else:
participant_events: set[str] = set()
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
raise Exception("How did we get here?")
for idx in selected_indexes:
if idx == 0:
+23 -13
View File
@@ -1,17 +1,20 @@
"""
Filename: participantimage.py
Description: Logic for the Participant Image analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in Imports
import os
import os.path as op
from pathlib import Path
from datetime import datetime
# External library imports
from mne.io.base import BaseRaw
from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QPixmap
@@ -21,7 +24,13 @@ from src.shared.shareddata import APP_NAME
class ParticipantImageViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, fig_bytes_dict):
def __init__(
self,
haemo_dict: dict[str, BaseRaw],
fig_bytes_dict: dict[str, dict[str, bytes]]
) -> None:
super().__init__("ParticipantImage")
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.setWindowTitle(f"Participant Image Viewer - {APP_NAME.upper()}")
@@ -29,12 +38,12 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
self.fig_bytes_dict = fig_bytes_dict
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
self.participant_map: dict[str, str] = {}
self.participant_dropdown_items: list[str] = []
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
display_label = f"{short_label} ({op.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
@@ -87,23 +96,24 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
selected_display_names = self._get_checked_items(self.participant_dropdown)
# Map from display names back to file paths
selected_file_paths = []
selected_file_paths: list[str] = []
for display_name in selected_display_names:
# Find file_path by matching display name
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
expected_display = f"{short_label} ({Path(fp).name})"
if display_name == expected_display:
selected_file_paths.append(fp)
selected_file_paths.append(str(fp))
break
selected_labels = self._get_checked_items(self.image_index_dropdown)
row, col = 0, 0
for file_path in selected_file_paths:
fig_list = self.fig_bytes_dict.get(file_path, [])
participant_label = self.participant_map[file_path]
fig_map: dict[str, bytes] = self.fig_bytes_dict.get(file_path, {})
participant_label: str = self.participant_map.get(file_path, "Unknown")
for label in selected_labels:
fig_bytes = fig_list.get(label)
fig_bytes: bytes | None = fig_map.get(label)
if not fig_bytes:
continue
@@ -149,7 +159,7 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
for display_name in selected_display_names:
# Match display name to file path
for file_path, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(file_path)})"
expected_display = f"{short_label} ({op.basename(file_path)})"
if display_name == expected_display:
fig_dict = self.fig_bytes_dict.get(file_path, {})
for label in selected_image_labels:
@@ -157,7 +167,7 @@ class ParticipantImageViewerWidget(FlaresBaseWidget):
continue
fig_bytes = fig_dict[label]
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{os.path.basename(file_path)}_{label}_{timestamp}.png"
filename = f"{op.basename(file_path)}_{label}_{timestamp}.png"
output_path = save_dir / filename
with open(output_path, "wb") as f:
f.write(fig_bytes)
+67 -18
View File
@@ -9,6 +9,8 @@ License: GPL-3.0
import os
import json
from pathlib import Path
from typing import Sequence, Any
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget, QFrame, QSpinBox
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
from PySide6.QtCore import QEvent, QSize, Qt
@@ -807,7 +809,11 @@ class FlaresBaseWidget(QWidget):
self.image_index_dropdown = None
def _create_multiselect_dropdown(self, items):
def _create_multiselect_dropdown(
self,
items: Sequence[str]
) -> FullClickComboBox:
combo = FullClickComboBox()
combo.setView(QListView())
model = QStandardItemModel()
@@ -874,7 +880,11 @@ class FlaresBaseWidget(QWidget):
# checked.append(item.text())
# return checked
def _get_checked_items(self, combo=None):
def _get_checked_items(
self,
combo: QComboBox | None = None
) -> list[str]:
target = combo if combo is not None else getattr(self, 'participant_dropdown', None)
if target is None or target.model() is None:
@@ -897,7 +907,10 @@ class FlaresBaseWidget(QWidget):
return checked_items
def update_participant_dropdown_label(self, combo=None):
def update_participant_dropdown_label(
self,
combo: QComboBox | int | None = None
) -> None:
"""
Handles label updates for ANY participant dropdown.
If 'combo' is None, it defaults to the standard self.participant_dropdown.
@@ -1142,7 +1155,13 @@ class FlaresBaseWidget(QWidget):
class CrossGroupUIMixin:
def setup_cross_group_ui(self, index_texts, placeholder_text=""):
participant_map: dict[str, str]
def setup_cross_group_ui(
self,
index_texts: Sequence[str],
placeholder_text: str = ""
) -> None:
self.group_to_paths = {}
for file_path, group_name in self.group_dict.items():
@@ -1293,7 +1312,13 @@ class CrossGroupUIMixin:
return file_paths
def get_common_request_data(self, parameterized_indexes, json_location=None, contrast_dfs=None):
def get_common_request_data(
self,
parameterized_indexes: dict[int, list[dict[str, Any]]],
json_location: str | Path | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None:
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
@@ -1412,11 +1437,14 @@ class CrossGroupUIMixin:
class CSVUIMixin:
def setup_csv_ui(self, index_texts):
def setup_csv_ui(
self,
index_texts: Sequence[str]
) -> None:
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items: list[str] = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
@@ -1428,12 +1456,12 @@ class CSVUIMixin:
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown: FullClickComboBox = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
@@ -1456,13 +1484,20 @@ class CSVUIMixin:
self.showMaximized()
class InterGroupUIMixin:
def setup_inter_group_ui(self, index_texts, placeholder_text=""):
def setup_inter_group_ui(
self,
index_texts: Sequence[str],
placeholder_text: str = ""
) -> None:
self.show_all_events = True
self._updating_checkstates = False
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
@@ -1525,7 +1560,13 @@ class InterGroupUIMixin:
self.thumb_size = QSize(280, 180)
self.showMaximized()
def get_common_request_data(self, parameterized_indexes, json_location=None, contrast_dfs=None):
def get_common_request_data(
self,
parameterized_indexes: dict[int, list[dict[str, Any]]],
json_location: str | Path | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
@@ -1570,7 +1611,7 @@ class InterGroupUIMixin:
dynamic_rois = []
# 1. Check for the JSON file and parse ROI names
if os.path.exists(json_location):
if json_location is not None and os.path.exists(json_location):
try:
with open(json_location, 'r', encoding='utf-8') as f:
regions_data = json.load(f)
@@ -1645,9 +1686,13 @@ class InterGroupUIMixin:
)
class ParticipantUIMixin:
def setup_participant_ui(self, index_texts):
def setup_participant_ui(
self,
index_texts: Sequence[str]
) -> None:
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_map: dict[str, str] = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
@@ -1694,7 +1739,11 @@ class ParticipantUIMixin:
self.showMaximized()
def get_common_request_data(self, parameterized_indexes):
def get_common_request_data(
self,
parameterized_indexes: dict[int, list[dict[str, Any]]]
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
+12 -11
View File
@@ -1,22 +1,27 @@
"""
Filename: shareddata.py
Description: Shared constants and methods for FLARES
Description: Shared constants and methods other files depend on
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import sys
# Built-in imports
import os
import sys
import platform
CURRENT_VERSION = "1.5.0"
APP_NAME = "flares"
APP_NAME_EXPANDED = "fNIRS Lightweight Analysis, Research, & Evaluation Suite"
API_URL = f"https://git.research.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
API_URL_SECONDARY = f"https://git.research2.dezeeuw.ca/api/v1/repos/tyler/{APP_NAME}/releases"
PLATFORM_NAME = platform.system().lower()
CHANGELOG_URL = "https://git.research.dezeeuw.ca/tyler/flares/raw/branch/main/changelog_major.md"
WIKI_URL = "https://git.research.dezeeuw.ca/tyler/flares/wiki"
CHANGELOG_URL = f"https://git.research.dezeeuw.ca/tyler/{APP_NAME}/raw/branch/main/changelog_major.md"
WIKI_URL = f"https://git.research.dezeeuw.ca/tyler/{APP_NAME}/wiki"
PIPELINE_STAGES = [
"Preprocessing",
@@ -49,15 +54,11 @@ PIPELINE_STAGES = [
"Finishing Up"
]
def resource_path(relative_path):
def resource_path(relative_path: str) -> str:
"""
Get absolute path to resource regardless of running directly or packaged using PyInstaller
"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller bundle path
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
base_path = getattr(sys, "_MEIPASS", os.path.abspath("."))
return os.path.join(base_path, relative_path)
+5 -4
View File
@@ -1,6 +1,7 @@
"""
Filename: about.py
Description: About window for FLARES
Description: About window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
@@ -9,7 +10,7 @@ License: GPL-3.0
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import Qt
from src.shared.shareddata import APP_NAME, CURRENT_VERSION
from src.shared.shareddata import APP_NAME, APP_NAME_EXPANDED, CURRENT_VERSION
class AboutWindow(QWidget):
"""
@@ -19,14 +20,14 @@ class AboutWindow(QWidget):
parent (QWidget, optional): Parent widget of this window. Defaults to None.
"""
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"About {APP_NAME.upper()}")
self.resize(250, 100)
layout = QVBoxLayout()
label = QLabel(f"About {APP_NAME.upper()}", self)
label2 = QLabel("fNIRS Lightweight Analysis, Research, & Evaluation Suite", self)
label2 = QLabel(f"{APP_NAME_EXPANDED}", self)
label3 = QLabel(f"{APP_NAME.upper()} is licensed under the GPL-3.0 licence. For more information, visit https://www.gnu.org/licenses/gpl-3.0.en.html", self)
label4 = QLabel(f"Version v{CURRENT_VERSION}")
+13 -10
View File
@@ -1,21 +1,24 @@
"""
Filename: terminal.py
Description: Terminal window for FLARES
Description: Terminal window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
from typing import Any, Callable
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLineEdit
from PySide6.QtCore import Qt
from src.shared.shareddata import API_URL, API_URL_SECONDARY, APP_NAME, CURRENT_VERSION, PLATFORM_NAME
from src.window.about import AboutWindow
from updater import LocalPendingUpdateCheckThread, UpdateManager
from updater import UpdateManager
class TerminalWindow(QWidget):
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"Terminal - {APP_NAME.upper()}")
self.resize(320, 180)
@@ -30,7 +33,7 @@ class TerminalWindow(QWidget):
layout.addWidget(self.input_line)
self.setLayout(layout)
self.commands = {
self.commands: dict[str, Callable[..., Any]] = {
"hello": self.cmd_hello,
"help": self.cmd_help,
"version": self.cmd_version,
@@ -68,22 +71,22 @@ class TerminalWindow(QWidget):
self.output_area.append(f"[Unknown command] '{command_name}'")
def cmd_hello(self, *args):
def cmd_hello(self, *args: Any) -> str:
return "Hello from the terminal!"
def cmd_help(self, *args):
def cmd_help(self, *args: Any) -> str:
return f"Available commands: {', '.join(self.commands.keys())}"
def cmd_version(self, *args):
def cmd_version(self, *args: Any) -> str:
return f"{APP_NAME.upper()} is running version {CURRENT_VERSION}."
def cmd_about(self, *args):
def cmd_about(self, *args: Any) -> None:
self.about = AboutWindow(self)
self.about.show()
def cmd_update(self, *args):
def cmd_update(self, *args: Any) -> str:
main_win = self.parent()
if main_win is None:
if not isinstance(main_win, QWidget):
return "[Error] Main window context not found."
self.updater = UpdateManager(
+3 -3
View File
@@ -15,9 +15,9 @@ import numpy as np
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QMessageBox, QLineEdit, QPushButton, QFileDialog
from PySide6.QtCore import Qt
from mne.io import read_raw_snirf
from mne_nirs.io import write_raw_snirf
from mne.channels import make_dig_montage
from mne.io import read_raw_snirf #type: ignore
from mne_nirs.io import write_raw_snirf #type: ignore
from mne.channels import make_dig_montage #type: ignore
from src.shared.shareddata import APP_NAME
+1 -1
View File
@@ -20,7 +20,7 @@ class UserGuideWindow(QWidget):
parent (QWidget, optional): Parent widget of this window. Defaults to None.
"""
def __init__(self, parent=None):
def __init__(self, parent: QWidget | None = None) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"User Guide - {APP_NAME.upper()}")
self.resize(250, 100)
+1 -1
View File
@@ -40,7 +40,7 @@ class ViewerLauncherWidget(QWidget):
("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location], True),
("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Cross-Group Brain and Image Viewer", CrossGroupBrainImageWidget, [haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, group_dict, contrast_results_dict], True)
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True)
]
layout = QVBoxLayout(self)
+5 -4
View File
@@ -1,6 +1,7 @@
"""
Filename: welcome.py
Description: Welcome dialog for FLARES
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
@@ -9,13 +10,13 @@ License: GPL-3.0
from PySide6.QtWidgets import QTextBrowser, QVBoxLayout, QLabel, QDialog, QHBoxLayout, QPushButton
from PySide6.QtGui import QDesktopServices, QIcon
from PySide6.QtCore import QUrl
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from src.shared.shareddata import APP_NAME, CURRENT_VERSION, CHANGELOG_URL, resource_path
class WelcomeDialog(QDialog):
def __init__(self, parent=None, direct=True, first=False):
def __init__(self, parent: QDialog | None = None, direct: bool = True, first: bool = False):
super().__init__(parent)
self.setWindowTitle(f"What's New - {APP_NAME.upper()}")
self.setMinimumSize(550, 450)
@@ -64,10 +65,10 @@ class WelcomeDialog(QDialog):
self.network_manager.get(QNetworkRequest(QUrl(CHANGELOG_URL)))
def _on_download_complete(self, reply):
def _on_download_complete(self, reply: QNetworkReply) -> None:
"""Processes the downloaded markdown and drops it into the view frame."""
if reply.error() == reply.NetworkError.NoError:
raw_bytes = reply.readAll()
raw_bytes = reply.readAll().data()
# Convert raw bytes to standard text string
markdown_text = str(raw_bytes, encoding='utf-8')