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
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
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)
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())