Esempio n. 1
0
    def test_callback(self):
        def dummy_call():
            receiver.dummy()

        receiver = MagicMock()

        dkt = ProfileDict()
        dkt.connect("", receiver.empty)
        dkt.connect("", dummy_call)
        dkt.connect("b", receiver.b)
        dkt.connect(["d", "c"], receiver.dc)

        dkt.set("test.a", 1)
        assert receiver.empty.call_count == 1
        assert receiver.dummy.call_count == 1
        receiver.empty.assert_called_with("a")
        receiver.dummy.assert_called_with()
        dkt.set("test.a", 1)
        assert receiver.empty.call_count == 1
        receiver.b.assert_not_called()
        dkt.set("test2.a", 1)
        assert receiver.empty.call_count == 2
        receiver.b.assert_not_called()
        dkt.set(["test", "b"], 1)
        assert receiver.empty.call_count == 3
        assert receiver.b.call_count == 1
        dkt.set("test.d.c", 1)
        receiver.dc.assert_called_once()
        dkt.set("test.a", 2)
        assert receiver.empty.call_count == 5
Esempio n. 2
0
class BaseSettings(ViewSettings):
    """
    :ivar json_folder_path: default location for saving/loading settings data
    :ivar last_executed_algorithm: name of last executed algorithm.
    :cvar save_locations_keys: list of names of distinct save location.
        location are stored in "io"

    """

    mask_changed = Signal()
    points_changed = Signal()
    request_load_files = Signal(list)
    """:py:class:`~.Signal` mask changed signal"""
    json_encoder_class = ProfileEncoder
    load_metadata = staticmethod(load_metadata_base)
    algorithm_changed = Signal()
    """:py:class:`~.Signal` emitted when current algorithm should be changed"""
    save_locations_keys = []

    def __init__(self,
                 json_path: Union[Path, str],
                 profile_name: str = "default"):
        """
        :param json_path: path to store
        :param profile_name: name of profile to be used. default value is "default"
        """
        super().__init__()
        napari_path = os.path.dirname(json_path) if os.path.basename(
            json_path) in ["analysis", "mask"] else json_path
        self.napari_settings: "NapariSettings" = napari_get_settings(
            napari_path)
        self._current_roi_dict = profile_name
        self._roi_dict = ProfileDict()
        self.json_folder_path = json_path
        self.last_executed_algorithm = ""
        self.history: List[HistoryElement] = []
        self.history_index = -1
        self.last_executed_algorithm = ""
        self._points = None

    def _image_changed(self):
        super()._image_changed()
        self.points = None

    @property
    def points(self):
        return self._points

    @points.setter
    def points(self, value):
        self._points = value if value is not None else None
        self.points_changed.emit()

    @property
    def theme_name(self) -> str:
        try:
            theme = self.napari_settings.appearance.theme
            if self.get_from_profile("first_start", True):
                theme = "light"
                self.napari_settings.appearance.theme = theme
                self.set_in_profile("first_start", False)
            return theme
        except AttributeError:  # pragma: no cover
            return "light"

    @theme_name.setter
    def theme_name(self, value: str):
        self.napari_settings.appearance.theme = value

    def set_segmentation_result(self, result: ROIExtractionResult):
        if (result.file_path is not None and result.file_path != "" and
                result.file_path != self.image.file_path):  # pragma: no cover
            if self._parent is not None:
                # TODO change to non disrupting popup
                QMessageBox().warning(
                    self._parent, "Result file bug",
                    "It looks like one try to set ROI form another file.")
            return
        if result.info_text and self._parent is not None:
            QMessageBox().information(self._parent, "Algorithm info",
                                      result.info_text)

        self._additional_layers = result.additional_layers
        self.last_executed_algorithm = result.parameters.algorithm
        self.set(f"algorithms.{result.parameters.algorithm}",
                 result.parameters.values)
        # Fixme not use EventedDict here
        try:
            roi_info = result.roi_info.fit_to_image(self.image)
        except ValueError:  # pragma: no cover
            raise ValueError(ROI_NOT_FIT)
        if result.points is not None:
            self.points = result.points
        self._roi_info = roi_info
        self.roi_changed.emit(self._roi_info)

    def _load_files_call(self, files_list: List[str]):
        self.request_load_files.emit(files_list)

    def add_history_element(self, elem: HistoryElement) -> None:
        self.history_index += 1
        if self.history_index < len(self.history) and self.cmp_history_element(
                elem, self.history[self.history_index]):
            self.history[self.history_index] = elem
        else:
            self.history = self.history[:self.history_index]
            self.history.append(elem)

    def history_size(self) -> int:
        return self.history_index + 1

    def history_redo_size(self) -> int:
        if self.history_index + 1 == len(self.history):
            return 0
        return len(self.history[self.history_index + 1:])

    def history_redo_clean(self) -> None:
        self.history = self.history[:self.history_size()]

    def history_current_element(self) -> HistoryElement:
        return self.history[self.history_index]

    def history_next_element(self) -> HistoryElement:
        return self.history[self.history_index + 1]

    def history_pop(self) -> Optional[HistoryElement]:
        if self.history_index != -1:
            self.history_index -= 1
            return self.history[self.history_index + 1]
        return None

    def set_history(self, history: List[HistoryElement]):
        self.history = history
        self.history_index = len(self.history) - 1

    def get_history(self) -> List[HistoryElement]:
        return self.history[:self.history_index + 1]

    @staticmethod
    def cmp_history_element(el1, el2):
        return False

    @property
    def mask(self):
        return self._image.mask

    @mask.setter
    def mask(self, value):
        try:
            self._image.set_mask(value)
            self.mask_changed.emit()
        except ValueError:
            raise ValueError("mask do not fit to image")

    def get_save_list(self) -> List[SaveSettingsDescription]:
        """List of files in which program save the state."""
        return [
            SaveSettingsDescription("segmentation_settings.json",
                                    self._roi_dict),
            SaveSettingsDescription("view_settings.json",
                                    self.view_settings_dict),
        ]

    def get_path_history(self) -> List[str]:
        """
        return list containing last 10 elements added with :py:meth:`.add_path_history` and
        last opened in each category form :py:attr:`save_location_keys`
        """
        res = self.get(DIR_HISTORY, [])[:]
        for name in self.save_locations_keys:
            val = self.get("io." + name, str(Path.home()))
            if val not in res:
                res = res + [val]
        return res

    @staticmethod
    def _add_elem_to_list(data_list: list, value: Any, keep_len=10) -> list:
        try:
            data_list.remove(value)
        except ValueError:
            data_list = data_list[:keep_len - 1]
        return [value] + data_list

    def get_last_files(self) -> List[Tuple[Tuple[Union[str, Path], ...], str]]:
        return self.get(FILE_HISTORY, [])

    def add_load_files_history(self, file_path: Sequence[Union[str, Path]],
                               load_method: str):  # pragma: no cover
        warnings.warn("`add_load_files_history` is deprecated",
                      FutureWarning,
                      stacklevel=2)
        return self.add_last_files(file_path, load_method)

    def add_last_files(self, file_path: Sequence[Union[str, Path]],
                       load_method: str):
        self.set(
            FILE_HISTORY,
            self._add_elem_to_list(self.get(FILE_HISTORY, []),
                                   [list(file_path), load_method]))
        # keep list of files as list because json serialize tuple to list
        self.add_path_history(os.path.dirname(file_path[0]))

    def get_last_files_multiple(
            self) -> List[Tuple[Tuple[Union[str, Path], ...], str]]:
        return self.get(MULTIPLE_FILES_OPEN_HISTORY, [])

    def add_last_files_multiple(self, file_paths: List[Union[str, Path]],
                                load_method: str):
        self.set(
            MULTIPLE_FILES_OPEN_HISTORY,
            self._add_elem_to_list(self.get(MULTIPLE_FILES_OPEN_HISTORY, []),
                                   [list(file_paths), load_method],
                                   keep_len=30),
        )
        # keep list of files as list because json serialize tuple to list
        self.add_path_history(os.path.dirname(file_paths[0]))

    def add_path_history(self, dir_path: Union[str, Path]):
        """Save path in history of visited directories. Store only 10 last"""
        dir_path = str(dir_path)
        self.set(DIR_HISTORY,
                 self._add_elem_to_list(self.get(DIR_HISTORY, []), dir_path))

    def set(self, key_path: str, value):
        """
        function for saving general state (not visualization). 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.
        """
        self._roi_dict.set(f"{self._current_roi_dict}.{key_path}", value)

    def get(self, key_path: str, default=None):
        """
        Function for getting general state (not visualization). 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._roi_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)

    def dump_part(self, file_path, path_in_dict, names=None):
        data = self.get(path_in_dict)
        if names is not None:
            data = {name: data[name] for name in names}
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        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, file_path):
        data = cls.load_metadata(file_path)
        bad_key = []
        if isinstance(data, MutableMapping) and not check_loaded_dict(data):
            for k, v in data.items():
                if not check_loaded_dict(v):
                    bad_key.append(k)
            for el in bad_key:
                del data[el]
        elif isinstance(data, ProfileDict) and not data.verify_data():
            bad_key = data.filter_data()
        return data, bad_key

    def dump(self, folder_path: Union[Path, str, None] = None):
        """
        Save current application settings to disc.

        :param folder_path: path to directory in which data should be saved.
            If is None then use :py:attr:`.json_folder_path`
        """
        if self.napari_settings.save is not None:
            self.napari_settings.save()
        else:
            self.napari_settings._save()  # pylint: disable=W0212
        if folder_path is None:
            folder_path = self.json_folder_path
        if not os.path.exists(folder_path):
            os.makedirs(folder_path)
        errors_list = []
        for el in self.get_save_list():
            try:
                dump_string = json.dumps(el.values,
                                         cls=self.json_encoder_class,
                                         indent=2)
                with open(os.path.join(folder_path, el.file_name),
                          "w",
                          encoding="utf-8") as ff:
                    ff.write(dump_string)
            except Exception as e:  # pylint: disable=W0703
                errors_list.append((e, os.path.join(folder_path,
                                                    el.file_name)))
        if errors_list:
            logger.error(errors_list)
        return errors_list

    def load(self, folder_path: Union[Path, str, None] = None):
        """
        Load settings state from given directory

        :param folder_path: path to directory in which data should be saved.
            If is None then use :py:attr:`.json_folder_path`
        """
        if folder_path is None:
            folder_path = self.json_folder_path
        errors_list = []
        for el in self.get_save_list():
            file_path = os.path.join(folder_path, el.file_name)
            if not os.path.exists(file_path):
                continue
            error = False
            try:
                data: ProfileDict = self.load_metadata(file_path)
                if not data.verify_data():
                    errors_list.append((file_path, data.filter_data()))
                    error = True
                el.values.update(data)
            except Exception as e:  # pylint: disable=W0703
                error = True
                errors_list.append((file_path, e))
            finally:
                if error:
                    timestamp = datetime.today().strftime("%Y-%m-%d_%H_%M_%S")
                    base_path, ext = os.path.splitext(file_path)
                    os.rename(file_path, base_path + "_" + timestamp + ext)

        if errors_list:
            logger.error(errors_list)
        return errors_list

    def get_project_info(self) -> ProjectInfoBase:
        """Get all information needed to save project"""
        raise NotImplementedError  # pragma: no cover

    def set_project_info(self, data: ProjectInfoBase):
        """Set project info"""
        raise NotImplementedError  # pragma: no cover

    @staticmethod
    def verify_image(image: Image, silent=True) -> Union[Image, bool]:
        if image.is_time:
            if image.is_stack:
                raise TimeAndStackException()
            if silent:
                return image.swap_time_and_stack()
            raise SwapTimeStackException()
        return True
Esempio n. 3
0
class ViewSettings(ImageSettings):
    colormap_changes = Signal()
    labels_changed = Signal()
    theme_changed = Signal()
    profile_data_changed = Signal(str, object)
    """Signal about changes in stored data (set with set_in_profile)"""
    def __init__(self):
        super().__init__()
        self.color_map = []
        self.border_val = []
        self.current_profile_dict = "default"
        self.view_settings_dict = ProfileDict()
        self.colormap_dict = ColormapDict(
            self.get_from_profile("custom_colormap", {}))
        self.label_color_dict = LabelColorDict(
            self.get_from_profile("custom_label_colors", {}))
        self.cached_labels: Optional[Tuple[str, np.ndarray]] = None

    @property
    def theme_name(self) -> str:
        """Name of current theme."""
        return self.get_from_profile("theme", "light")

    @property
    def theme(self):
        """Theme as structure."""
        try:
            return get_theme(self.theme_name, as_dict=False)
        except TypeError:  # pragma: no cover
            theme = get_theme(self.theme_name)
            return Namespace(
                **{
                    k: Color(v)
                    if isinstance(v, str) and v.startswith("rgb") else v
                    for k, v in theme.items()
                })

    @property
    def style_sheet(self):
        """QSS style sheet for current theme."""
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", FutureWarning)
            theme = get_theme(self.theme_name)
        # TODO understand qss overwrite mechanism
        return napari_template(
            "\n".join(register.qss_list) + get_stylesheet() +
            "\n".join(register.qss_list), **theme)

    @theme_name.setter
    def theme_name(self, value: str):
        """Name of current theme."""
        if value not in napari.utils.theme.available_themes():
            raise ValueError(
                f"Unsupported theme {value}. Supported one: {self.theme_list()}"
            )
        if value == self.theme_name:
            return
        self.set_in_profile("theme", value)
        self.theme_changed.emit()

    @staticmethod
    def theme_list():
        """Sequence of available themes"""
        try:
            return napari.utils.theme.available_themes()
        except:  # noqa: E722  # pylint: disable=W0702  # pragma: no cover
            return ("light", )

    @property
    def chosen_colormap(self):
        """Sequence of selected colormap to be available in dropdown"""
        data = self.get_from_profile("colormaps", starting_colors[:])
        res = [x for x in data if x in self.colormap_dict]
        if len(res) != data:
            if not res:
                res = starting_colors[:]
            self.set_in_profile("colormaps", res)
        return res

    @chosen_colormap.setter
    def chosen_colormap(self, val):
        self.set_in_profile("colormaps", val)
        self.colormap_changes.emit()

    @property
    def current_labels(self):
        """Current labels scheme for marking ROI"""
        return self.get_from_profile("labels_used", "default")

    @current_labels.setter
    def current_labels(self, val):
        if val not in self.label_color_dict:
            raise ValueError(f"Unknown label scheme name '{val}'")
        self.set_in_profile("labels_used", val)
        self.labels_changed.emit()

    @property
    def label_colors(self):
        key = self.current_labels
        if key not in self.label_color_dict:
            key = "default"
            self.current_labels = key

        if not (self.cached_labels and key == self.cached_labels[0]):
            self.cached_labels = key, self.label_color_dict.get_array(key)

        return self.cached_labels[1]

    def chosen_colormap_change(self, name, visibility):
        colormaps = set(self.chosen_colormap)
        if visibility:
            colormaps.add(name)
        else:
            with suppress(KeyError):
                colormaps.remove(name)
        # TODO update sorting rule
        self.chosen_colormap = list(
            sorted(colormaps, key=self.colormap_dict.get_position))

    def get_channel_info(
            self,
            view: str,
            num: int,
            default: Optional[str] = None) -> str:  # pragma: no cover
        warnings.warn(
            "get_channel_info is deprecated, use get_channel_colormap_name instead",
            category=DeprecationWarning,
            stacklevel=2,
        )
        return self.get_channel_colormap_name(view, num, default)

    def get_channel_colormap_name(self,
                                  view: str,
                                  num: int,
                                  default: Optional[str] = None) -> str:
        cm = self.chosen_colormap
        if default is None:
            default = cm[num % len(cm)]
        resp = self.get_from_profile(f"{view}.cmap.num{num}", default)
        if resp not in self.colormap_dict:
            resp = cm[num % len(cm)]
            self.set_in_profile(f"{view}.cmap.num{num}", resp)
        return resp

    def set_channel_info(self, view: str, num, value: str):  # pragma: no cover
        warnings.warn(
            "set_channel_info is deprecated, use set_channel_colormap_name instead",
            category=DeprecationWarning,
            stacklevel=2,
        )
        self.set_channel_colormap_name(view, num, value)

    def set_channel_colormap_name(self, view: str, num, value: str):
        self.set_in_profile(f"{view}.cmap.num{num}", value)

    def connect_channel_colormap_name(self, view: str, fun: Callable):
        self.connect_to_profile(f"{view}.cmap", fun)

    @property
    def available_colormaps(self):
        return list(self.colormap_dict.keys())

    def _image_changed(self):
        self.border_val = self.image.get_ranges()
        super()._image_changed()

    def change_profile(self, name):
        self.current_profile_dict = name
        self.view_settings_dict.profile_change()

    def set_in_profile(self, key_path, value):
        """
        Function for saving information used in visualization. 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."""
        self.view_settings_dict.set(f"{self.current_profile_dict}.{key_path}",
                                    value)
        self.profile_data_changed.emit(key_path, value)

    def get_from_profile(self, key_path, default=None):
        """
        Function for getting information used in visualization. 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.view_settings_dict.get(
            f"{self.current_profile_dict}.{key_path}", default)

    def connect_to_profile(self, key_path, callback):
        # TODO  fixme fix when introduce switch profiles
        self.view_settings_dict.connect(key_path, callback)
Esempio n. 4
0
class PartSettings(BaseSettings):
    """
    last_executed_algorithm - parameter for caring last used algorithm
    """

    compare_segmentation_change = Signal(ROIInfo)
    roi_profiles_changed = Signal()
    roi_pipelines_changed = Signal()
    measurement_profiles_changed = Signal()
    batch_plans_changed = Signal()
    json_encoder_class = PartEncoder
    load_metadata = staticmethod(load_metadata)
    last_executed_algorithm: str
    save_locations_keys = [
        "open_directory",
        "save_directory",
        "export_directory",
        "batch_plan_directory",
        "multiple_open_directory",
    ]

    def __init__(self, json_path, profile_name="default"):
        super().__init__(json_path, profile_name)
        self._mask = None
        self.compare_segmentation = None
        self._segmentation_pipelines_dict = ProfileDict()
        self._segmentation_profiles_dict = ProfileDict()
        self._batch_plans_dict = ProfileDict()
        self._measurement_profiles_dict = ProfileDict()
        self._segmentation_profiles_dict.connect(
            "", self.roi_profiles_changed.emit, maxargs=0)
        self._segmentation_pipelines_dict.connect(
            "", self.roi_pipelines_changed.emit, maxargs=0)
        self._measurement_profiles_dict.connect(
            "", self.measurement_profiles_changed.emit, maxargs=0)
        self._batch_plans_dict.connect("",
                                       self.batch_plans_changed.emit,
                                       maxargs=0)

    def fix_history(self, algorithm_name, algorithm_values):
        """
        set new algorithm parameters to

        :param str algorithm_name:
        :param dict algorithm_values:
        """
        self.history[self.history_index +
                     1] = self.history[self.history_index +
                                       1].replace_(roi_extraction_parameters={
                                           "algorithm_name": algorithm_name,
                                           "values": algorithm_values
                                       })

    @staticmethod
    def cmp_history_element(el1: HistoryElement, el2: HistoryElement):
        return el1.mask_property == el2.mask_property

    def set_segmentation_to_compare(self, segmentation: ROIInfo):
        self.compare_segmentation = segmentation
        self.compare_segmentation_change.emit(segmentation)

    def _image_changed(self):
        super()._image_changed()
        self._mask = None

    def get_project_info(self) -> ProjectTuple:
        algorithm_name = self.last_executed_algorithm
        if algorithm_name:
            value = self.get(f"algorithms.{algorithm_name}")
            if isinstance(value, EventedDict):
                value = value.as_dict_deep()
            algorithm_val = {
                "algorithm_name": algorithm_name,
                "values": deepcopy(value),
            }
        else:
            algorithm_val = {}
        return ProjectTuple(
            file_path=self.image.file_path,
            image=self.image.substitute(),
            roi_info=self.roi_info,
            additional_layers=self.additional_layers,
            mask=self.mask,
            history=self.history[:self.history_index + 1],
            algorithm_parameters=algorithm_val,
            points=self.points,
        )

    def set_project_info(self, data: typing.Union[ProjectTuple, MaskInfo,
                                                  PointsInfo]):
        if isinstance(data, MaskInfo):
            self.mask = data.mask_array
            return
        if isinstance(data, PointsInfo):
            self.points = data.points
            return
        if not isinstance(data, ProjectTuple):
            return
        if self.image.file_path == data.image.file_path and self.image.shape == data.image.shape:
            if data.roi_info.roi is not None:
                try:
                    self.image.fit_array_to_image(data.roi_info.roi)
                    self.mask = data.mask
                except ValueError:
                    self.image = data.image.substitute()
            else:
                self.mask = data.mask
        else:
            self.image = data.image.substitute(mask=data.mask)
        self.roi = data.roi_info
        self._additional_layers = data.additional_layers
        self.additional_layers_changed.emit()
        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.algorithm_changed.emit()

    def get_save_list(self) -> typing.List[SaveSettingsDescription]:
        return super().get_save_list() + [
            SaveSettingsDescription("segmentation_pipeline_save.json",
                                    self._segmentation_pipelines_dict),
            SaveSettingsDescription("segmentation_profiles_save.json",
                                    self._segmentation_profiles_dict),
            SaveSettingsDescription("statistic_profiles_save.json",
                                    self._measurement_profiles_dict),
            SaveSettingsDescription("batch_plans_save.json",
                                    self._batch_plans_dict),
        ]

    @property
    def segmentation_pipelines(self) -> typing.Dict[str, SegmentationPipeline]:
        warnings.warn(
            "segmentation_pipelines is deprecated, use roi_pipelines",
            DeprecationWarning,
            stacklevel=2)
        return self.roi_pipelines

    @property
    def roi_pipelines(self) -> typing.Dict[str, SegmentationPipeline]:
        return self._segmentation_pipelines_dict.get(self._current_roi_dict,
                                                     EventedDict())

    @property
    def segmentation_profiles(self) -> typing.Dict[str, ROIExtractionProfile]:
        warnings.warn("segmentation_profiles is deprecated, use roi_profiles",
                      DeprecationWarning,
                      stacklevel=2)
        return self.roi_profiles

    @property
    def roi_profiles(self) -> typing.Dict[str, ROIExtractionProfile]:
        return self._segmentation_profiles_dict.get(self._current_roi_dict,
                                                    EventedDict())

    @property
    def batch_plans(self) -> typing.Dict[str, CalculationPlan]:
        return self._batch_plans_dict.get(self._current_roi_dict,
                                          EventedDict())

    @property
    def measurement_profiles(self) -> typing.Dict[str, MeasurementProfile]:
        return self._measurement_profiles_dict.get(self._current_roi_dict,
                                                   EventedDict())