diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9acf902ff..f07596b4a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,7 +17,7 @@ stages: - stage: GetTestData jobs: - job: linux - pool: {vmImage: 'Ubuntu-18.04'} + pool: {vmImage: 'Ubuntu-20.04'} steps: - script: bash build_utils/download_data.sh displayName: "download data" @@ -91,7 +91,7 @@ stages: artifactName: docs - job: Notebook_check - pool: {vmImage: 'Ubuntu-18.04'} + pool: {vmImage: 'Ubuntu-20.04'} continueOnError: true variables: DATA_PATH: typy_neuronow2 @@ -150,7 +150,7 @@ stages: dependsOn: Tests_linux jobs: - job: sdist - pool: {vmImage: 'Ubuntu-18.04'} + pool: {vmImage: 'Ubuntu-20.04'} steps: - task: UsePythonVersion@0 - bash: pip install -r requirements/requirements_dev.txt @@ -167,7 +167,7 @@ stages: test_path: dist/PartSeg/PartSeg _test DISPLAY: ':99.0' pip_cache_dir: $(Pipeline.Workspace)/.pip - pool: { vmImage: 'ubuntu-18.04' } + pool: { vmImage: 'Ubuntu-20.04' } steps: - template: .azure-pipelines/linux_libs.yaml - template: .azure-pipelines/pyinstaller.yaml diff --git a/package/PartSeg/_roi_analysis/main_window.py b/package/PartSeg/_roi_analysis/main_window.py index c7ae091dc..d94ffe613 100644 --- a/package/PartSeg/_roi_analysis/main_window.py +++ b/package/PartSeg/_roi_analysis/main_window.py @@ -183,7 +183,7 @@ def save_pipeline(self): if not name: QMessageBox.information(self, "No segmentation", "No segmentation executed", QMessageBox.Ok) return - values = self._settings.get(f"algorithms.{name}", {}) + values = self._settings.get_algorithm(f"algorithms.{name}", {}) if len(values) == 0: QMessageBox.information(self, "Some problem", "Pleas run execution again", QMessageBox.Ok) return @@ -500,7 +500,7 @@ def next_mask(self): def prev_mask(self): history: HistoryElement = self.settings.history_pop() algorithm_name = self.settings.last_executed_algorithm - algorithm_values = self.settings.get(f"algorithms.{algorithm_name}") + algorithm_values = self.settings.get_algorithm(f"algorithms.{algorithm_name}") self.settings.fix_history(algorithm_name=algorithm_name, algorithm_values=algorithm_values) self.settings.set("current_algorithm", history.roi_extraction_parameters["algorithm_name"]) self.settings.set( diff --git a/package/PartSeg/_roi_analysis/partseg_settings.py b/package/PartSeg/_roi_analysis/partseg_settings.py index e72cefdc2..e980e798d 100644 --- a/package/PartSeg/_roi_analysis/partseg_settings.py +++ b/package/PartSeg/_roi_analysis/partseg_settings.py @@ -79,7 +79,7 @@ def _image_changed(self): def get_project_info(self) -> ProjectTuple: algorithm_name = self.last_executed_algorithm if algorithm_name: - value = self.get(f"algorithms.{algorithm_name}") + value = self.get_algorithm(f"algorithms.{algorithm_name}") if isinstance(value, EventedDict): value = value.as_dict_deep() algorithm_val = { @@ -125,7 +125,9 @@ def set_project_info(self, data: typing.Union[ProjectTuple, MaskInfo, PointsInfo self.set_history(data.history[:]) if data.algorithm_parameters: self.last_executed_algorithm = data.algorithm_parameters["algorithm_name"] - self.set(f"algorithms.{self.last_executed_algorithm}", deepcopy(data.algorithm_parameters["values"])) + self.set_algorithm( + f"algorithms.{self.last_executed_algorithm}", deepcopy(data.algorithm_parameters["values"]) + ) self.algorithm_changed.emit() def get_save_list(self) -> typing.List[SaveSettingsDescription]: diff --git a/package/PartSeg/_roi_mask/stack_settings.py b/package/PartSeg/_roi_mask/stack_settings.py index 2bfb49933..605c5478d 100644 --- a/package/PartSeg/_roi_mask/stack_settings.py +++ b/package/PartSeg/_roi_mask/stack_settings.py @@ -61,7 +61,7 @@ def set_segmentation_result(self, result: ROIExtractionResult): self._additional_layers = result.additional_layers self.additional_layers_changed.emit() self.last_executed_algorithm = result.parameters.algorithm - self.set(f"algorithms.{result.parameters.algorithm}", result.parameters.values) + self.set_algorithm(f"algorithms.{result.parameters.algorithm}", result.parameters.values) self._set_roi_info(result.roi_info, True, [], parameters_dict) if result.points is not None: self.points = result.points diff --git a/package/PartSeg/common_backend/base_settings.py b/package/PartSeg/common_backend/base_settings.py index d29d14d27..3a9791f09 100644 --- a/package/PartSeg/common_backend/base_settings.py +++ b/package/PartSeg/common_backend/base_settings.py @@ -25,7 +25,7 @@ from PartSegCore import register from PartSegCore.color_image import default_colormap_dict, default_label_dict from PartSegCore.color_image.base_colors import starting_colors -from PartSegCore.io_utils import find_problematic_entries, load_matadata_part, load_metadata_base +from PartSegCore.io_utils import find_problematic_entries, load_metadata_base from PartSegCore.json_hooks import PartSegEncoder from PartSegCore.project_info import AdditionalLayerDescription, HistoryElement, ProjectInfoBase from PartSegCore.roi_info import ROIInfo @@ -459,6 +459,7 @@ def __init__(self, json_path: Union[Path, str], profile_name: str = "default"): self.napari_settings: "NapariSettings" = napari_get_settings(napari_path) self._current_roi_dict = profile_name self._roi_dict = ProfileDict() + self._last_algorithm_dict = ProfileDict() self.json_folder_path = json_path self.last_executed_algorithm = "" self.history: List[HistoryElement] = [] @@ -510,7 +511,7 @@ def set_segmentation_result(self, result: ROIExtractionResult): self._additional_layers = result.additional_layers self.last_executed_algorithm = result.parameters.algorithm - self.set(f"algorithms.{result.parameters.algorithm}", result.parameters.values) + self.set_algorithm(f"algorithms.{result.parameters.algorithm}", result.parameters.values) # Fixme not use EventedDict here try: roi_info = result.roi_info.fit_to_image(self.image) @@ -583,6 +584,7 @@ def get_save_list(self) -> List[SaveSettingsDescription]: return [ SaveSettingsDescription("segmentation_settings.json", self._roi_dict), SaveSettingsDescription("view_settings.json", self.view_settings_dict), + SaveSettingsDescription("algorithm_settings.json", self._last_algorithm_dict), ] def get_path_history(self) -> List[str]: @@ -643,6 +645,14 @@ def set(self, key_path: str, value): :param key_path: dot separated path :param value: value to store. The value need to be json serializable. """ + if ( + key_path.startswith("algorithms.") + or key_path.startswith("algorithm_widget_state.") + or key_path == "current_algorithm" + ): + warnings.warn("Use `set_algorithm_state` instead of `set` for algorithm state", FutureWarning, stacklevel=2) + self.set_algorithm(key_path, value) + return self._roi_dict.set(f"{self._current_roi_dict}.{key_path}", value) def get(self, key_path: str, default=None): @@ -653,8 +663,36 @@ def get(self, key_path: str, default=None): :param key_path: dot separated path :param default: default value if key is missed """ + if ( + key_path.startswith("algorithms.") + or key_path.startswith("algorithm_widget_state.") + or key_path == "current_algorithm" + ): + warnings.warn("Use `set_algorithm_state` instead of `set` for algorithm state", FutureWarning, stacklevel=2) + return self.get_algorithm(key_path, default) return self._roi_dict.get(f"{self._current_roi_dict}.{key_path}", default) + def set_algorithm(self, key_path: str, value): + """ + function for saving last algorithm used information. This is accessor to + :py:meth:`~.ProfileDict.set` of inner variable. + + :param key_path: dot separated path + :param value: value to store. The value need to be json serializable. + """ + # if key_path.startswith("") + self._last_algorithm_dict.set(f"{self._current_roi_dict}.{key_path}", value) + + def get_algorithm(self, key_path: str, default=None): + """ + Function for getting last algorithm used information. This is accessor to + :py:meth:`~.ProfileDict.get` of inner variable. + + :param key_path: dot separated path + :param default: default value if key is missed + """ + return self._last_algorithm_dict.get(f"{self._current_roi_dict}.{key_path}", default) + def connect_(self, key_path, callback): # TODO fixme fix when introduce switch profiles self._roi_dict.connect(key_path, callback) @@ -667,21 +705,6 @@ def dump_part(self, file_path, path_in_dict, names=None): with open(file_path, "w", encoding="utf-8") as ff: json.dump(data, ff, cls=self.json_encoder_class, indent=2) - @classmethod - def load_part(cls, data: Union[Path, str]) -> Tuple[dict, List[str]]: # pragma: no cover - """ - Load serialized data. Get valid entries. - - :param data: path to file or string to be decoded. - :return: - """ - warnings.warn( - f"{cls.__name__}.load_part is deprecated. Please use PartSegCore.utils.load_matadata_part", - stacklevel=2, - category=FutureWarning, - ) - return load_matadata_part(data) - def dump(self, folder_path: Union[Path, str, None] = None): """ Save current application settings to disc. @@ -753,6 +776,7 @@ def load(self, folder_path: Union[Path, str, None] = None): timestamp = datetime.now().strftime("%Y-%m-%d_%H_%M_%S") base_path, ext = os.path.splitext(file_path) os.rename(file_path, f"{base_path}_{timestamp}{ext}") + return errors_dict def get_project_info(self) -> ProjectInfoBase: diff --git a/package/PartSeg/common_gui/algorithms_description.py b/package/PartSeg/common_gui/algorithms_description.py index 76974b382..4e3ce6741 100644 --- a/package/PartSeg/common_gui/algorithms_description.py +++ b/package/PartSeg/common_gui/algorithms_description.py @@ -1,4 +1,4 @@ -import collections +import collections.abc import inspect import logging import typing @@ -13,15 +13,16 @@ from qtpy.QtCore import QObject, Signal from qtpy.QtGui import QHideEvent, QPainter, QPaintEvent, QResizeEvent from qtpy.QtWidgets import ( - QAbstractSpinBox, QApplication, QCheckBox, QComboBox, + QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QMessageBox, QScrollArea, + QSpinBox, QStackedLayout, QVBoxLayout, QWidget, @@ -231,7 +232,7 @@ def get_change_signal(widget: typing.Union[QWidget, Widget]): return widget.currentIndexChanged if isinstance(widget, QCheckBox): return widget.stateChanged - if isinstance(widget, QAbstractSpinBox): + if isinstance(widget, (QSpinBox, QDoubleSpinBox)): return widget.valueChanged if isinstance(widget, QLineEdit): return widget.textChanged @@ -250,9 +251,7 @@ def get_getter_and_setter_function( widget: typing.Union[QWidget, Widget], ) -> typing.Tuple[ typing.Callable[ - [ - QWidget, - ], + [typing.Union[QWidget, Widget]], typing.Any, ], typing.Callable[[QWidget, typing.Any], None], # noqa E231 @@ -264,9 +263,7 @@ def get_getter_and_setter_function( """ if isinstance(widget, Widget): return _value_get, _value_set - if isinstance(widget, ProfileSelect): - return widget.__class__.get_value, widget.__class__.set_value - if isinstance(widget, ChannelComboBox): + if isinstance(widget, (ProfileSelect, ChannelComboBox)): return widget.__class__.get_value, widget.__class__.set_value if isinstance(widget, QEnumComboBox): return widget.__class__.currentEnum, widget.__class__.setCurrentEnum @@ -274,7 +271,7 @@ def get_getter_and_setter_function( return widget.__class__.currentText, widget.__class__.setCurrentText if isinstance(widget, QCheckBox): return widget.__class__.isChecked, widget.__class__.setChecked - if isinstance(widget, QAbstractSpinBox): + if isinstance(widget, (QSpinBox, QDoubleSpinBox)): return widget.__class__.value, widget.__class__.setValue if isinstance(widget, QLineEdit): return widget.__class__.text, widget.__class__.setText @@ -282,7 +279,7 @@ def get_getter_and_setter_function( return widget.__class__.get_values, widget.__class__.set_values if isinstance(widget, ListInput): return widget.__class__.get_value, widget.__class__.set_value - if hasattr(widget, "get_value") and hasattr(widget, "set_value"): + if hasattr(widget.__class__, "get_value") and hasattr(widget.__class__, "set_value"): return widget.__class__.get_value, widget.__class__.set_value raise ValueError(f"Unsupported type: {type(widget)}") @@ -388,19 +385,20 @@ def _add_to_layout( self.widgets_dict[ap.name] = ap ap.change_fun.connect(_any_arguments(self.value_changed.emit)) if isinstance(ap.get_field(), SubAlgorithmWidget): - layout.addRow(label, ap.get_field().choose) + w = typing.cast(SubAlgorithmWidget, ap.get_field()) + layout.addRow(label, w.choose) layout.addRow(ap.get_field()) if ap.name in start_values: - ap.get_field().set_starting(start_values[ap.name]) + w.set_starting(start_values[ap.name]) ap.change_fun.connect(_any_arguments(self.value_changed.emit)) - self.channels_chose.append(ap.get_field()) + self.channels_chose.append(w) return if isinstance(ap.get_field(), Widget): - layout.addRow(label, ap.get_field().native) + layout.addRow(label, typing.cast(Widget, ap.get_field()).native) return if isinstance(ap.get_field(), FieldsList): layout.addRow(label) - for el in ap.get_field().field_list: + for el in typing.cast(FieldsList, ap.get_field()).field_list: self._add_to_layout(layout, el, start_values.get(ap.name, {}), settings, add_to_widget_dict=False) return layout.addRow(label, ap.get_field()) @@ -427,9 +425,7 @@ def update_size(self): def get_values(self): res = {name: el.get_value() for name, el in self.widgets_dict.items()} - if self._model_class is not None: - return self._model_class(**res) - return res + return self._model_class(**res) if self._model_class is not None else res def recursive_get_values(self): return {name: el.recursive_get_values() for name, el in self.widgets_dict.items()} @@ -523,7 +519,7 @@ def set_values(self, val: typing.Mapping): def recursive_get_values(self): return {name: el.recursive_get_values() for name, el in self.widgets_dict.items()} - def get_values(self): + def get_values(self) -> typing.Dict[str, typing.Any]: name = self.choose.currentText() values = self.widgets_dict[name].get_values() return {"name": name, "values": values} @@ -532,7 +528,7 @@ def change_channels_num(self, image: Image): for i in range(self.layout().count()): el = self.layout().itemAt(i) if el.widget() and isinstance(el.widget(), FormWidget): - el.widget().image_changed(image) + typing.cast(FormWidget, el.widget()).image_changed(image) def algorithm_choose(self, name): if name not in self.widgets_dict: @@ -583,11 +579,11 @@ def __init__(self, settings: BaseSettings, algorithm: typing.Type[ROIExtractionA self.info_label.setHidden(True) # FIXME verify inflo_label usage main_layout.addWidget(self.info_label) - start_values = settings.get(f"algorithm_widget_state.{self.name}", {}) + start_values = settings.get_algorithm(f"algorithm_widget_state.{self.name}", {}) self.form_widget = self._form_widget(algorithm, start_values=start_values) self.form_widget.value_changed.connect(self.values_changed.emit) self.setWidget(self.form_widget) - value_dict = self.settings.get(f"algorithms.{self.name}", {}) + value_dict = self.settings.get_algorithm(f"algorithms.{self.name}", {}) self.set_values(value_dict) self.algorithm_thread = SegmentationThread(algorithm()) self.algorithm_thread.info_signal.connect(self.show_info) @@ -647,7 +643,7 @@ def get_values(self): def execute(self, exclude_mask=None): values = self.get_values() - self.settings.set(f"algorithms.{self.name}", deepcopy(values)) + self.settings.set_algorithm(f"algorithms.{self.name}", deepcopy(values)) if isinstance(values, dict): self.algorithm_thread.set_parameters(**values) else: @@ -705,6 +701,8 @@ class AlgorithmChooseBase(QWidget): progress_signal = Signal(str, int) algorithm_changed = Signal(str) + algorithm_dict: typing.Dict[str, InteractiveAlgorithmSettingsWidget] + def __init__(self, settings: BaseSettings, algorithms: typing.Type[AlgorithmSelection], parent=None): super().__init__(parent) self.settings = settings @@ -740,7 +738,7 @@ def add_widgets_to_algorithm(self): widget.algorithm_thread.progress_signal.connect(self.progress_signal.emit) widget.values_changed.connect(self.value_changed.emit) self.stack_layout.addWidget(widget) - name = self.settings.get("current_algorithm", "") + name = self.settings.get_algorithm("current_algorithm", "") self.algorithm_choose.blockSignals(False) if name: self.algorithm_choose.setCurrentText(name) @@ -749,7 +747,7 @@ def reload(self, algorithms=None): if algorithms is not None: self.algorithms = algorithms for _ in range(self.stack_layout.count()): - widget: InteractiveAlgorithmSettingsWidget = self.stack_layout.takeAt(0).widget() + widget = typing.cast(InteractiveAlgorithmSettingsWidget, self.stack_layout.takeAt(0).widget()) widget.algorithm_thread.execution_done.disconnect() widget.algorithm_thread.finished.disconnect() widget.algorithm_thread.started.disconnect() @@ -762,20 +760,21 @@ def reload(self, algorithms=None): def updated_algorithm(self): self.change_algorithm( self.settings.last_executed_algorithm, - self.settings.get(f"algorithms.{self.settings.last_executed_algorithm}"), + self.settings.get_algorithm(f"algorithms.{self.settings.last_executed_algorithm}"), ) def recursive_get_values(self): result = {key: widget.recursive_get_values() for key, widget in self.algorithm_dict.items()} - self.settings.set( - "algorithm_widget_state", recursive_update(self.settings.get("algorithm_widget_state", {}), result) + self.settings.set_algorithm( + "algorithm_widget_state", + recursive_update(self.settings.get_algorithm("algorithm_widget_state", {}), result), ) return result def change_algorithm(self, name, values: dict = None): - self.settings.set("current_algorithm", name) - widget = self.stack_layout.currentWidget() + self.settings.set_algorithm("current_algorithm", name) + widget = typing.cast(InteractiveAlgorithmSettingsWidget, self.stack_layout.currentWidget()) blocked = self.blockSignals(True) if name != widget.name: widget = self.algorithm_dict[name] @@ -793,7 +792,7 @@ def change_algorithm(self, name, values: dict = None): self.algorithm_changed.emit(name) def current_widget(self) -> InteractiveAlgorithmSettingsWidget: - return self.stack_layout.currentWidget() + return typing.cast(InteractiveAlgorithmSettingsWidget, self.stack_layout.currentWidget()) def current_parameters(self) -> ROIExtractionProfile: widget = self.current_widget() @@ -809,7 +808,7 @@ def __init__(self, settings: BaseSettings, algorithms: typing.Type[AlgorithmSele self.settings.image_changed.connect(self.image_changed) def image_changed(self): - current_widget: InteractiveAlgorithmSettingsWidget = self.stack_layout.currentWidget() + current_widget = typing.cast(InteractiveAlgorithmSettingsWidget, self.stack_layout.currentWidget()) current_widget.image_changed(self.settings.image) if hasattr(self.settings, "mask") and hasattr(current_widget, "change_mask"): current_widget.change_mask() diff --git a/package/tests/test_PartSeg/test_common_gui.py b/package/tests/test_PartSeg/test_common_gui.py index fb94bd5a0..7df34841f 100644 --- a/package/tests/test_PartSeg/test_common_gui.py +++ b/package/tests/test_PartSeg/test_common_gui.py @@ -1525,11 +1525,11 @@ def test_execute(self, qtbot, part_settings, monkeypatch): qtbot.addWidget(widget) mock = MagicMock() monkeypatch.setattr(widget.algorithm_thread, "start", mock) - assert part_settings.get(f"algorithms.{LowerThresholdAlgorithm.get_name()}") == {} + assert part_settings.get_algorithm(f"algorithms.{LowerThresholdAlgorithm.get_name()}") == {} widget.execute() mock.assert_called_once() assert ( - part_settings.get(f"algorithms.{LowerThresholdAlgorithm.get_name()}") + part_settings.get_algorithm(f"algorithms.{LowerThresholdAlgorithm.get_name()}") == LowerThresholdAlgorithm.__argument_class__() ) @@ -1582,7 +1582,7 @@ def test_init(self, qtbot, part_settings): def test_restore_algorithm(self, qtbot, part_settings): assert BorderRim.get_name() != AnalysisAlgorithmSelection.get_default().name - part_settings.set("current_algorithm", BorderRim.get_name()) + part_settings.set_algorithm("current_algorithm", BorderRim.get_name()) widget = AlgorithmChooseBase(part_settings, AnalysisAlgorithmSelection) qtbot.addWidget(widget) assert widget.algorithm_choose.currentText() == BorderRim.get_name() diff --git a/package/tests/test_PartSeg/test_settings.py b/package/tests/test_PartSeg/test_settings.py index 81875a59f..06a50629c 100644 --- a/package/tests/test_PartSeg/test_settings.py +++ b/package/tests/test_PartSeg/test_settings.py @@ -318,6 +318,15 @@ def test_verify_image(self): with pytest.raises(TimeAndStackException): BaseSettings.verify_image(Image(np.zeros((2, 2, 10, 10), dtype=np.uint8), (1, 1, 1), axes_order="TZXY")) + def test_algorithm_redirect(self, tmp_path): + settings = BaseSettings(tmp_path) + settings.set_algorithm("algorithms.aa", 2) + with pytest.warns(FutureWarning, match="Use `set_algorithm_state` instead"): + assert settings.get("algorithms.aa") == 2 + with pytest.warns(FutureWarning, match="Use `set_algorithm_state` instead"): + settings.set("algorithms.aa", 3) + assert settings.get_algorithm("algorithms.aa") == 3 + class TestPartSettings: def test_set_mask_info(self, qtbot, tmp_path, image): @@ -344,7 +353,7 @@ def test_project_info_set(self, qtbot, analysis_segmentation, analysis_segmentat def test_get_project_info(self, qtbot, tmp_path, image): settings = PartSettings(tmp_path) settings.last_executed_algorithm = "aa" - settings.set("algorithms.aa", 1) + settings.set_algorithm("algorithms.aa", 1) settings.image = image pt = settings.get_project_info() assert pt.algorithm_parameters["algorithm_name"] == "aa"