Skip to content

Commit 0a38fc6

Browse files
test: Add more tests for common GUI elements (#622)
* add tests * small fixes * 'Refactored by Sourcery' (#623) Co-authored-by: Sourcery AI <> * base main window tests * silence obsolete warnings * add more tests * fix tests * skip part of tests on windows * add waitng steeps * try fix pyside tests * add drop test * add test for MultipleFilesTreeWidget * fix test for pyside * enable ImageView QMenu tests for pyside * add load_data_exception_hook test * update algorithm description tests * BaseAlgorithmSettingsWidget use name from algorithm * add test for AlgorithmChoose Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
1 parent fdc5e37 commit 0a38fc6

15 files changed

Lines changed: 824 additions & 132 deletions

File tree

package/PartSeg/_roi_mask/main_window.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from .._roi_mask.segmentation_info_dialog import SegmentationInfoDialog
4141
from ..common_backend.base_settings import IO_SAVE_DIRECTORY, ROI_NOT_FIT
4242
from ..common_gui.advanced_tabs import AdvancedWindow
43-
from ..common_gui.algorithms_description import AlgorithmChoose, AlgorithmSettingsWidget
43+
from ..common_gui.algorithms_description import AlgorithmChoose, InteractiveAlgorithmSettingsWidget
4444
from ..common_gui.channel_control import ChannelProperty
4545
from ..common_gui.custom_load_dialog import PLoadDialog
4646
from ..common_gui.custom_save_dialog import PSaveDialog
@@ -720,7 +720,7 @@ def execute_action(self):
720720
# TODO Fix This
721721
blank = blank[0]
722722
self.progress_bar.setHidden(False)
723-
widget: AlgorithmSettingsWidget = self.algorithm_choose_widget.current_widget()
723+
widget: InteractiveAlgorithmSettingsWidget = self.algorithm_choose_widget.current_widget()
724724
widget.set_mask(blank)
725725
self.progress_bar.setRange(0, widget.algorithm.get_steps_num())
726726
widget.execute()

package/PartSeg/common_gui/algorithms_description.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@
4848
from .universal_gui_part import ChannelComboBox, CustomDoubleSpinBox, CustomSpinBox
4949

5050

51-
def update(d, u):
51+
def recursive_update(d, u):
5252
if not isinstance(d, typing.MutableMapping):
5353
d = {}
5454
for k, v in u.items():
55-
d[k] = update(d.get(k, {}), v) if isinstance(v, collections.abc.Mapping) else v
55+
d[k] = recursive_update(d.get(k, {}), v) if isinstance(v, collections.abc.Mapping) else v
5656
return d
5757

5858

@@ -70,13 +70,20 @@ def __init__(self):
7070
super().__init__()
7171
self._settings = None
7272

73-
def add_settings(self, settings: BaseSettings):
73+
def _update_choices(self):
74+
if hasattr(self._settings, "roi_profiles"):
75+
self.clear()
76+
self.addItems(list(self._settings.roi_profiles.keys()))
77+
78+
def set_settings(self, settings: BaseSettings):
7479
self._settings = settings
75-
if hasattr(settings, "roi_profiles"):
76-
self.addItems(list(settings.roi_profiles.keys()))
80+
if hasattr(self._settings, "roi_profiles"):
81+
self._settings.roi_profiles.setted.connect(self._update_choices)
82+
self._settings.roi_profiles.deleted.connect(self._update_choices)
83+
self._update_choices()
7784

7885
def get_value(self):
79-
if self._settings is not None and hasattr(self._settings, "roi_profiles"):
86+
if self._settings is not None and hasattr(self._settings, "roi_profiles") and self.currentText() != "":
8087
return self._settings.roi_profiles[self.currentText()]
8188
return None
8289

@@ -386,6 +393,7 @@ def _add_to_layout(
386393
if ap.name in start_values:
387394
ap.get_field().set_starting(start_values[ap.name])
388395
ap.change_fun.connect(_any_arguments(self.value_changed.emit))
396+
self.channels_chose.append(ap.get_field())
389397
return
390398
if isinstance(ap.get_field(), Widget):
391399
layout.addRow(label, ap.get_field().native)
@@ -402,7 +410,7 @@ def _add_to_layout(
402410
self.channels_chose.append(ap.get_field())
403411
if issubclass(ap.value_type, ROIExtractionProfile):
404412
# noinspection PyTypeChecker
405-
ap.get_field().add_settings(settings)
413+
ap.get_field().set_settings(settings)
406414
if ap.name in start_values:
407415
with suppress(KeyError, ValueError, TypeError):
408416
ap.set_value(start_values[ap.name])
@@ -562,20 +570,20 @@ class BaseAlgorithmSettingsWidget(QScrollArea):
562570
values_changed = Signal()
563571
algorithm_thread: SegmentationThread
564572

565-
def __init__(self, settings: BaseSettings, name, algorithm: typing.Type[ROIExtractionAlgorithm]):
573+
def __init__(self, settings: BaseSettings, algorithm: typing.Type[ROIExtractionAlgorithm]):
566574
"""
567575
For algorithm which works on one channel
568576
"""
569577
super().__init__()
570578
self.settings = settings
571579
self.widget_list = []
572-
self.name = name
573580
self.algorithm = algorithm
574581
main_layout = QVBoxLayout()
575582
self.info_label = QLabel()
576583
self.info_label.setHidden(True)
584+
# FIXME verify inflo_label usage
577585
main_layout.addWidget(self.info_label)
578-
start_values = settings.get(f"algorithm_widget_state.{name}", {})
586+
start_values = settings.get(f"algorithm_widget_state.{self.name}", {})
579587
self.form_widget = self._form_widget(algorithm, start_values=start_values)
580588
self.form_widget.value_changed.connect(self.values_changed.emit)
581589
self.setWidget(self.form_widget)
@@ -585,6 +593,10 @@ def __init__(self, settings: BaseSettings, name, algorithm: typing.Type[ROIExtra
585593
self.algorithm_thread.info_signal.connect(self.show_info)
586594
self.algorithm_thread.exception_occurred.connect(self.exception_occurred)
587595

596+
@property
597+
def name(self):
598+
return self.algorithm.get_name()
599+
588600
@staticmethod
589601
def _form_widget(algorithm, start_values) -> FormWidget:
590602
return FormWidget(
@@ -615,7 +627,7 @@ def exception_occurred(exc: Exception):
615627

616628
def show_info(self, text):
617629
self.info_label.setText(text)
618-
self.info_label.setVisible(True)
630+
self.info_label.setVisible(text != "")
619631

620632
def image_changed(self, image: Image):
621633
self.form_widget.image_changed(image)
@@ -633,9 +645,6 @@ def set_values(self, values_dict):
633645
def get_values(self):
634646
return self.form_widget.get_values()
635647

636-
def channel_num(self):
637-
return self.channels_chose.currentIndex()
638-
639648
def execute(self, exclude_mask=None):
640649
values = self.get_values()
641650
self.settings.set(f"algorithms.{self.name}", deepcopy(values))
@@ -659,29 +668,18 @@ def recursive_get_values(self):
659668
return self.form_widget.recursive_get_values()
660669

661670

662-
class AlgorithmSettingsWidget(BaseAlgorithmSettingsWidget):
663-
def execute(self, exclude_mask=None):
664-
self.algorithm_thread.algorithm.set_image(self.settings.image)
665-
super().execute(exclude_mask)
666-
667-
668671
class InteractiveAlgorithmSettingsWidget(BaseAlgorithmSettingsWidget):
669672
algorithm_thread: SegmentationThread
670673

671-
def __init__(self, settings, name, algorithm: typing.Type[ROIExtractionAlgorithm], selector: typing.List[QWidget]):
672-
super().__init__(settings, name, algorithm)
673-
self.selector = selector
674+
def __init__(self, settings, algorithm: typing.Type[ROIExtractionAlgorithm], selector: typing.List[QWidget]):
675+
super().__init__(settings, algorithm)
676+
self.selector = selector[:]
674677
self.algorithm_thread.finished.connect(self.enable_selector)
675678
self.algorithm_thread.started.connect(self.disable_selector)
676679
# noinspection PyUnresolvedReferences
677680
if hasattr(settings, "mask_changed"):
678681
settings.mask_changed.connect(self.change_mask)
679682

680-
def value_updated(self):
681-
if not self.parent().interactive:
682-
return
683-
self.execute()
684-
685683
def change_mask(self):
686684
if not self.isVisible():
687685
return
@@ -726,15 +724,15 @@ def __init__(self, settings: BaseSettings, algorithms: typing.Type[AlgorithmSele
726724
self.setLayout(layout)
727725

728726
@staticmethod
729-
def _algorithm_widget(settings, name, val) -> InteractiveAlgorithmSettingsWidget:
730-
return InteractiveAlgorithmSettingsWidget(settings, name, val, [])
727+
def _algorithm_widget(settings, val) -> InteractiveAlgorithmSettingsWidget:
728+
return InteractiveAlgorithmSettingsWidget(settings, val, [])
731729

732730
def add_widgets_to_algorithm(self):
733731
self.algorithm_choose.blockSignals(True)
734732
self.algorithm_choose.clear()
735733
for name, val in self.algorithms.__register__.items():
736734
self.algorithm_choose.addItem(name)
737-
widget = self._algorithm_widget(self.settings, name, val)
735+
widget = self._algorithm_widget(self.settings, val)
738736
self.algorithm_dict[name] = widget
739737
widget.algorithm_thread.execution_done.connect(self.result.emit)
740738
widget.algorithm_thread.finished.connect(self.finished.emit)
@@ -770,7 +768,9 @@ def updated_algorithm(self):
770768
def recursive_get_values(self):
771769
result = {key: widget.recursive_get_values() for key, widget in self.algorithm_dict.items()}
772770

773-
self.settings.set("algorithm_widget_state", update(self.settings.get("algorithm_widget_state", {}), result))
771+
self.settings.set(
772+
"algorithm_widget_state", recursive_update(self.settings.get("algorithm_widget_state", {}), result)
773+
)
774774
return result
775775

776776
def change_algorithm(self, name, values: dict = None):

package/PartSeg/common_gui/exception_hooks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import logging
22

33
from PartSeg.common_backend.except_hook import show_warning
4+
from PartSegImage.image_reader import INCOMPATIBLE_IMAGE_MASK
45

56
OPEN_ERROR = "Open error"
67

78

89
def load_data_exception_hook(exception):
9-
if isinstance(exception, ValueError) and exception.args[0] == "Incompatible shape of mask and image":
10+
if isinstance(exception, ValueError) and exception.args[0] == INCOMPATIBLE_IMAGE_MASK:
11+
# TODO think about incompatibilible axes (image_reader:417)
1012
show_warning(OPEN_ERROR, "Most probably you try to load mask from other image. Check selected files.")
1113
elif isinstance(exception, MemoryError):
1214
show_warning(OPEN_ERROR, f"Not enough memory to read this image: {exception}")

package/PartSeg/common_gui/main_window.py

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -30,58 +30,69 @@
3030
OPEN_DIRECTORY = "io.open_directory"
3131
OPEN_FILE_FILTER = "io.open_filter"
3232

33+
_EXIT = object()
34+
3335

3436
class BaseMainMenu(QWidget):
3537
def __init__(self, settings: BaseSettings, main_window):
3638
super().__init__()
3739
self.settings = settings
3840
self.main_window = main_window
3941

42+
def _set_data_list(self, data_list):
43+
if len(data_list) == 0:
44+
QMessageBox.warning(self, "Empty list", "List of files to load is empty")
45+
return _EXIT
46+
if hasattr(self.main_window, "multiple_files"):
47+
self.main_window.multiple_files.add_states(data_list)
48+
self.main_window.multiple_files.setVisible(True)
49+
self.settings.set("multiple_files", True)
50+
return data_list[0]
51+
52+
def _set_project_info_base(self, project_info_base: ProjectInfoBase):
53+
if project_info_base.errors != "": # pragma: no cover
54+
resp = QMessageBox.question(
55+
self,
56+
"Load problem",
57+
f"During load data "
58+
f"some problems occur: {project_info_base.errors}."
59+
"Do you would like to try load it anyway?",
60+
QMessageBox.Yes | QMessageBox.No,
61+
)
62+
if resp == QMessageBox.No:
63+
return _EXIT
64+
try:
65+
image = self.settings.verify_image(project_info_base.image, False)
66+
except SwapTimeStackException:
67+
res = QMessageBox.question(
68+
self,
69+
"Not supported",
70+
"Time data are currently not supported. Maybe You would like to treat time as z-stack",
71+
QMessageBox.Yes | QMessageBox.No,
72+
QMessageBox.No,
73+
)
74+
75+
if res == QMessageBox.Yes:
76+
image = project_info_base.image.swap_time_and_stack()
77+
else:
78+
return _EXIT
79+
except TimeAndStackException:
80+
QMessageBox.warning(self, "image error", "Do not support time and stack image")
81+
return _EXIT
82+
if not image:
83+
return _EXIT
84+
if isinstance(image, Image):
85+
# noinspection PyProtectedMember
86+
project_info_base = dataclasses.replace(project_info_base, image=image)
87+
return project_info_base
88+
4089
def set_data(self, data):
4190
if isinstance(data, list):
42-
if len(data) == 0:
43-
QMessageBox.warning(self, "Empty list", "List of files to load is empty")
44-
return
45-
if hasattr(self.main_window, "multiple_files"):
46-
self.main_window.multiple_files.add_states(data)
47-
self.main_window.multiple_files.setVisible(True)
48-
self.settings.set("multiple_files", True)
49-
data = data[0]
91+
data = self._set_data_list(data)
5092
if isinstance(data, ProjectInfoBase):
51-
if data.errors != "":
52-
resp = QMessageBox.question(
53-
self,
54-
"Load problem",
55-
f"During load data "
56-
f"some problems occur: {data.errors}."
57-
"Do you would like to try load it anyway?",
58-
QMessageBox.Yes | QMessageBox.No,
59-
)
60-
if resp == QMessageBox.No:
61-
return
62-
try:
63-
image = self.settings.verify_image(data.image, False)
64-
except SwapTimeStackException:
65-
res = QMessageBox.question(
66-
self,
67-
"Not supported",
68-
"Time data are currently not supported. Maybe You would like to treat time as z-stack",
69-
QMessageBox.Yes | QMessageBox.No,
70-
QMessageBox.No,
71-
)
72-
73-
if res == QMessageBox.Yes:
74-
image = data.image.swap_time_and_stack()
75-
else:
76-
return
77-
except TimeAndStackException:
78-
QMessageBox.warning(self, "image error", "Do not support time and stack image")
79-
return
80-
if not image:
81-
return
82-
if isinstance(image, Image):
83-
# noinspection PyProtectedMember
84-
data = dataclasses.replace(data, image=image)
93+
data = self._set_project_info_base(data)
94+
if data is _EXIT:
95+
return
8596
if data is None:
8697
QMessageBox().warning(self, "Data loading failure", "Error during data loading", QMessageBox.Ok)
8798
return
@@ -148,7 +159,7 @@ def __init__(
148159
self.setWindowTitle(title)
149160
self.title_base = title
150161
app = QApplication.instance()
151-
if app is not None and "PYTEST_CURRENT_TEST" not in os.environ:
162+
if app is not None and "PYTEST_CURRENT_TEST" not in os.environ: # pragma: no cover
152163
# FIXME the PYTEST_CURRENT_TEST is used to prevent pytest session fail on CI.
153164
app.setStyleSheet(settings.get_style_sheet())
154165
self.settings.theme_changed.connect(self.change_theme)
@@ -193,11 +204,11 @@ def _load_recent(self):
193204
self.settings.set(OPEN_DIRECTORY, os.path.dirname(data[0][0]))
194205
self.settings.set(OPEN_FILE, data[0][0])
195206
self.settings.set(OPEN_FILE_FILTER, data[1])
196-
except KeyError:
207+
except KeyError: # pragma: no cover
197208
self.read_drop(data[0])
198209

199210
def toggle_multiple_files(self):
200-
self.settings.set("multiple_files_widget", not self.settings.get("multiple_files_widget"))
211+
self.settings.set("multiple_files_widget", not self.settings.get("multiple_files_widget", False))
201212

202213
def get_colormaps(self) -> List[Optional[colormap.Colormap]]:
203214
channel_num = self.settings.image.channels
@@ -233,7 +244,7 @@ def close_viewer(self, obj):
233244
def change_theme(self, event):
234245
style_sheet = self.settings.style_sheet
235246
app = QApplication.instance()
236-
if app is not None:
247+
if app is not None and "PYTEST_CURRENT_TEST" not in os.environ: # pragma: no cover
237248
app.setStyleSheet(style_sheet)
238249
self.setStyleSheet(style_sheet)
239250

package/PartSeg/common_gui/multiple_file_widget.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from .waiting_dialog import ExecuteFunctionDialog
3636

3737

38-
class CustomTreeWidget(QTreeWidget):
38+
class MultipleFilesTreeWidget(QTreeWidget):
3939
context_load = Signal(QTreeWidgetItem)
4040
context_compare = Signal(QTreeWidgetItem)
4141
context_forget = Signal(QTreeWidgetItem)
@@ -123,7 +123,7 @@ def __init__(self, settings: BaseSettings, load_dict: Dict[str, LoadBase], compa
123123
self.state_dict_count = Counter()
124124
self.file_list = []
125125
self.load_register = load_dict
126-
self.file_view = CustomTreeWidget(compare_in_context_menu)
126+
self.file_view = MultipleFilesTreeWidget(compare_in_context_menu)
127127
self.file_view.header().close()
128128
self.save_state_btn = QPushButton("Save state")
129129
self.save_state_btn.setStyleSheet("QPushButton{font-weight: bold;}")
@@ -222,7 +222,7 @@ def load_files(self):
222222
errors_message.setText("There are errors during load files")
223223
errors_message.setInformativeText("During load files cannot found some of files on disc")
224224
errors_message.setStandardButtons(QMessageBox.Ok)
225-
text = "\n".join("File: " + x[0] + "\n" + str(x[1]) for x in self.error_list)
225+
text = "\n".join(f"File: {x[0]}" + "\n" + str(x[1]) for x in self.error_list)
226226
errors_message.setDetailedText(text)
227227
errors_message.exec_()
228228

0 commit comments

Comments
 (0)