From b5752ce907d46fbe5db5b417c46539c0ed88600b Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Sun, 7 Dec 2025 22:26:18 -0600 Subject: [PATCH 1/3] ruff check with new config --- pyproject.toml | 29 +------- src/ndevio/_bioio_plugin_utils.py | 4 +- src/ndevio/_napari_reader.py | 8 +- src/ndevio/_plugin_manager.py | 4 +- src/ndevio/helpers.py | 4 +- src/ndevio/nimage.py | 19 ++--- src/ndevio/widgets/_plugin_install_widget.py | 8 +- src/ndevio/widgets/_scene_widget.py | 7 +- src/ndevio/widgets/_utilities_container.py | 78 +++++++------------- tests/test_helpers.py | 8 +- tests/test_nimage.py | 38 +++------- tests/test_sampledata.py | 4 +- tests/test_utilities_container.py | 30 ++------ 13 files changed, 65 insertions(+), 176 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6907ae1..4e70aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,12 +95,9 @@ markers = [ [tool.coverage] report.fail_under = 80 -[tool.black] -line-length = 79 -target-version = ['py311', 'py312', 'py313'] - [tool.ruff] line-length = 79 +format.quote-style = "single" lint.select = [ "E", "F", "W", #flake8 "UP", # pyupgrade @@ -115,31 +112,9 @@ lint.select = [ "SIM", # flake8-simplify ] lint.ignore = [ - "E501", # line too long. let black handle this + "E501", # line too long. handled by formatter "UP006", "UP007", # type annotation. As using magicgui require runtime type annotation then we disable this. ] - -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".mypy_cache", - ".pants.d", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", - "*vendored*", - "*_vendor*", -] fix = true [tool.pixi.workspace] diff --git a/src/ndevio/_bioio_plugin_utils.py b/src/ndevio/_bioio_plugin_utils.py index db9497b..2f9313e 100644 --- a/src/ndevio/_bioio_plugin_utils.py +++ b/src/ndevio/_bioio_plugin_utils.py @@ -300,9 +300,9 @@ def _format_plugin_list(plugin_names: list[str]) -> str: continue lines.append(f" • {plugin_name}") - lines.append(f" {info['description']}") + lines.append(f' {info["description"]}') if info.get("note"): - lines.append(f" Note: {info['note']}") + lines.append(f' Note: {info["note"]}') lines.append(f" Install: pip install {plugin_name}\n") return "\n".join(lines) diff --git a/src/ndevio/_napari_reader.py b/src/ndevio/_napari_reader.py index 7eaff17..975a1c4 100644 --- a/src/ndevio/_napari_reader.py +++ b/src/ndevio/_napari_reader.py @@ -140,9 +140,7 @@ def napari_reader_function( return [(None,)] -def _open_scene_container( - path: PathLike, img: nImage, in_memory: bool -) -> None: +def _open_scene_container(path: PathLike, img: nImage, in_memory: bool) -> None: from pathlib import Path import napari @@ -157,9 +155,7 @@ def _open_scene_container( ) -def _open_plugin_installer( - path: PathLike, error: UnsupportedFileFormatError -) -> None: +def _open_plugin_installer(path: PathLike, error: UnsupportedFileFormatError) -> None: """Open the plugin installer widget for an unsupported file. Parameters diff --git a/src/ndevio/_plugin_manager.py b/src/ndevio/_plugin_manager.py index 314b157..68bcaff 100644 --- a/src/ndevio/_plugin_manager.py +++ b/src/ndevio/_plugin_manager.py @@ -223,9 +223,7 @@ def installable_plugins(self) -> list[str]: and plugin_name not in installed ] - def get_working_reader( - self, preferred_reader: str | None = None - ) -> Reader | None: + def get_working_reader(self, preferred_reader: str | None = None) -> Reader | None: """Get a reader that can actually read this file. Tries readers in priority order: diff --git a/src/ndevio/helpers.py b/src/ndevio/helpers.py index ea279cc..eb17438 100644 --- a/src/ndevio/helpers.py +++ b/src/ndevio/helpers.py @@ -141,9 +141,7 @@ def get_squeezed_dim_order( """ if isinstance(skip_dims, str): skip_dims = (skip_dims,) - return "".join( - {k: v for k, v in img.dims.items() if v > 1 and k not in skip_dims} - ) + return "".join({k: v for k, v in img.dims.items() if v > 1 and k not in skip_dims}) def create_id_string(img: nImage | BioImage, identifier: str) -> str: diff --git a/src/ndevio/nimage.py b/src/ndevio/nimage.py index e8268ac..dd712ea 100644 --- a/src/ndevio/nimage.py +++ b/src/ndevio/nimage.py @@ -205,9 +205,7 @@ def _get_layer_data(self, in_memory: bool | None = None) -> xr.DataArray: if DimensionNames.MosaicTile in self.reader.dims.order: try: if in_memory: - self.napari_layer_data = ( - self.reader.mosaic_xarray_data.squeeze() - ) + self.napari_layer_data = self.reader.mosaic_xarray_data.squeeze() else: self.napari_layer_data = ( self.reader.mosaic_xarray_dask_data.squeeze() @@ -301,9 +299,7 @@ def _build_layer_name( Formatted layer name. """ - path_stem = ( - Path(self.path).stem if self.path is not None else "unknown path" - ) + path_stem = Path(self.path).stem if self.path is not None else "unknown path" # Check if scene info is meaningful no_scene = len(self.scenes) == 1 and self.current_scene == "Image:0" @@ -369,9 +365,7 @@ def _build_single_layer_tuple( if layer_type == "image": from ._colormap_utils import get_colormap_for_channel - meta["colormap"] = get_colormap_for_channel( - channel_idx, n_channels - ) + meta["colormap"] = get_colormap_for_channel(channel_idx, n_channels) meta["blending"] = ( "additive" if channel_idx > 0 and n_channels > 1 @@ -489,9 +483,7 @@ def get_layer_data_tuples( } if scale: meta["scale"] = scale - self.layer_data_tuples = [ - (self.napari_layer_data.data, meta, "image") - ] + self.layer_data_tuples = [(self.napari_layer_data.data, meta, "image")] return self.layer_data_tuples # Single channel image (no channel dimension) @@ -511,8 +503,7 @@ def get_layer_data_tuples( # Multichannel image - split into separate layers channel_names = [ - str(c) - for c in self.napari_layer_data.coords[channel_dim].data.tolist() + str(c) for c in self.napari_layer_data.coords[channel_dim].data.tolist() ] channel_axis = self.napari_layer_data.dims.index(channel_dim) n_channels = self.napari_layer_data.shape[channel_axis] diff --git a/src/ndevio/widgets/_plugin_install_widget.py b/src/ndevio/widgets/_plugin_install_widget.py index 1479d59..28d76a2 100644 --- a/src/ndevio/widgets/_plugin_install_widget.py +++ b/src/ndevio/widgets/_plugin_install_widget.py @@ -80,14 +80,10 @@ def _init_widgets(self): if self.manager.path is not None: # Error mode: show file that failed file_name = self.manager.path.name - self._title_label = Label( - value=f"Cannot read file: {file_name}" - ) + self._title_label = Label(value=f"Cannot read file: {file_name}") else: # Standalone mode: general title - self._title_label = Label( - value="Install BioIO Reader Plugin" - ) + self._title_label = Label(value="Install BioIO Reader Plugin") self.append(self._title_label) self._info_label = Label(value="Select a plugin to install:") diff --git a/src/ndevio/widgets/_scene_widget.py b/src/ndevio/widgets/_scene_widget.py index 6501607..2105825 100644 --- a/src/ndevio/widgets/_scene_widget.py +++ b/src/ndevio/widgets/_scene_widget.py @@ -92,8 +92,7 @@ def __init__( self.in_memory = in_memory self.settings = get_settings() self.scenes = [ - f"{idx}{DELIMITER}{scene}" - for idx, scene in enumerate(self.img.scenes) + f"{idx}{DELIMITER}{scene}" for idx, scene in enumerate(self.img.scenes) ] self._init_widgets() @@ -129,8 +128,6 @@ def open_scene(self) -> None: # Get layer tuples and add to viewer using napari's Layer.create() from napari.layers import Layer - for ldt in self.img.get_layer_data_tuples( - in_memory=self.in_memory - ): + for ldt in self.img.get_layer_data_tuples(in_memory=self.in_memory): layer = Layer.create(*ldt) self.viewer.add_layer(layer) diff --git a/src/ndevio/widgets/_utilities_container.py b/src/ndevio/widgets/_utilities_container.py index 518a9f8..aef65ed 100644 --- a/src/ndevio/widgets/_utilities_container.py +++ b/src/ndevio/widgets/_utilities_container.py @@ -151,9 +151,7 @@ def concatenate_and_save_files( array_list.append(array) if not array_list: - raise ValueError( - f"No valid channels found in files: {[str(f) for f in files]}" - ) + raise ValueError(f"No valid channels found in files: {[str(f) for f in files]}") img_data = np.concatenate(array_list, axis=1) @@ -320,8 +318,7 @@ def _on_batch_complete(self): self._progress_bar.label = f"Completed {total} file sets" self._set_batch_button_state(running=False) self._results.value = ( - "Batch concatenated files in directory." - f"\nAt {time.strftime('%H:%M:%S')}" + f"Batch concatenated files in directory.\nAt {time.strftime('%H:%M:%S')}" ) def _on_batch_error(self, ctx, exception): @@ -392,9 +389,7 @@ def _init_save_name_container(self): self._append_scene_button = PushButton( label="Append Scene to Name", ) - self._save_name_container.extend( - [self._save_name, self._append_scene_button] - ) + self._save_name_container.extend([self._save_name, self._append_scene_button]) def _init_open_image_container(self): """Initialize the open image container.""" @@ -604,12 +599,8 @@ def _connect_events(self): ) self._scale_layers_button.clicked.connect(self.rescale_by) - self._concatenate_files_button.clicked.connect( - self.save_files_as_ome_tiff - ) - self._concatenate_batch_button.clicked.connect( - self._on_batch_button_clicked - ) + self._concatenate_files_button.clicked.connect(self.save_files_as_ome_tiff) + self._concatenate_batch_button.clicked.connect(self._on_batch_button_clicked) self._extract_scenes.clicked.connect(self.save_scenes_ome_tiff) self._save_layers_button.clicked.connect(self.save_layers_as_ome_tiff) self._export_figure_button.clicked.connect(self.canvas_export_figure) @@ -702,7 +693,7 @@ def update_metadata_from_layer(self): except AttributeError: self._results.value = ( "Tried to update metadata, but no layer selected." - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) except KeyError: scale = selected_layer.scale @@ -714,7 +705,7 @@ def update_metadata_from_layer(self): self._results.value = ( "Tried to update metadata, but could only update scale" " because layer not opened with ndevio reader." - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) def open_images(self): @@ -744,9 +735,7 @@ def select_next_images(self): img = nImage(next_files[0]) - self._save_name.value = helpers.create_id_string( - img, next_files[0].stem - ) + self._save_name.value = helpers.create_id_string(img, next_files[0].stem) self._files.value = next_files self.update_metadata_on_file_select() @@ -799,17 +788,13 @@ def _get_dims_for_shape_layer(self) -> tuple[int, ...]: None, ) if dim_layer is None: - raise ValueError( - "No image or labels present to convert shapes layer." - ) + raise ValueError("No image or labels present to convert shapes layer.") label_dim = dim_layer.data.shape label_dim = label_dim[:-1] if label_dim[-1] == 3 else label_dim return label_dim - def _get_save_loc( - self, root_dir: Path, parent: str, file_name: str - ) -> Path: + def _get_save_loc(self, root_dir: Path, parent: str, file_name: str) -> Path: """Get the save location based on the parent directory.""" save_directory = root_dir / parent save_directory.mkdir(parents=False, exist_ok=True) @@ -855,7 +840,7 @@ def _on_concat_complete(self, save_path: Path) -> None: self._progress_bar.value = 1 self._results.value = ( f"Saved Concatenated Image: {save_path.name}" - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_concat_error(self, exception: Exception) -> None: @@ -865,7 +850,7 @@ def _on_concat_error(self, exception: Exception) -> None: self._progress_bar.value = 0 self._results.value = ( f"Error concatenating files: {exception}" - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) def _build_file_sets(self) -> list[tuple[list[Path], str]]: @@ -899,7 +884,7 @@ def batch_concatenate_files(self) -> None: if not file_sets: self._results.value = ( - f"No complete file sets found.\nAt {time.strftime('%H:%M:%S')}" + f'No complete file sets found.\nAt {time.strftime("%H:%M:%S")}' ) return @@ -974,15 +959,14 @@ def _on_scene_extracted(self, result: tuple[int, str]) -> None: self._progress_bar.value = self._progress_bar.value + 1 self._results.value = ( f"Extracted scene {scene_idx}: {scene_name}" - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_scenes_complete(self, scenes_list: list, _=None) -> None: """Handle completion of all scene extractions.""" self._progress_bar.label = "" self._results.value = ( - f"Saved extracted scenes: {scenes_list}" - f"\nAt {time.strftime('%H:%M:%S')}" + f"Saved extracted scenes: {scenes_list}\nAt {time.strftime('%H:%M:%S')}" ) def _on_scene_error(self, exc: Exception) -> None: @@ -991,7 +975,7 @@ def _on_scene_error(self, exc: Exception) -> None: self._progress_bar.max = 1 self._progress_bar.value = 0 self._results.value = ( - f"Error extracting scenes: {exc}\nAt {time.strftime('%H:%M:%S')}" + f'Error extracting scenes: {exc}\nAt {time.strftime("%H:%M:%S")}' ) def canvas_export_figure(self) -> None: @@ -1000,7 +984,7 @@ def canvas_export_figure(self) -> None: self._results.value = ( "Exporting Figure only works in 2D mode." "\nUse Screenshot for 3D figures." - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) return @@ -1022,16 +1006,14 @@ def canvas_export_figure(self) -> None: f"Exported canvas figure to Figures directory." f"\nSaved as {save_name}" f"\nWith scale factor of {scale}" - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) return def canvas_screenshot(self) -> None: """Export the current canvas screenshot to the save directory.""" save_name = f"{self._save_name.value}_canvas.png" - save_path = self._get_save_loc( - self._save_directory.value, "Figures", save_name - ) + save_path = self._get_save_loc(self._save_directory.value, "Figures", save_name) scale = self._settings.ndevio_export.canvas_scale if self._settings.ndevio_export.override_canvas_size: @@ -1051,7 +1033,7 @@ def canvas_screenshot(self) -> None: f"\nSaved as {save_name}" f"\nWith canvas dimensions of {canvas_size}" f"\nWith scale factor of {scale}" - f"\nAt {time.strftime('%H:%M:%S')}" + f'\nAt {time.strftime("%H:%M:%S")}' ) return @@ -1059,16 +1041,10 @@ def save_layers_as_ome_tiff(self) -> None: """Save the selected layers as OME-TIFF.""" from napari.qt import create_worker - layer_data = self.concatenate_layers( - list(self._viewer.layers.selection) - ) - layer_types = [ - type(layer).__name__ for layer in self._viewer.layers.selection - ] + layer_data = self.concatenate_layers(list(self._viewer.layers.selection)) + layer_types = [type(layer).__name__ for layer in self._viewer.layers.selection] - layer_save_type = ( - "Layers" if len(set(layer_types)) > 1 else layer_types[0] - ) + layer_save_type = "Layers" if len(set(layer_types)) > 1 else layer_types[0] layer_save_dir = self._determine_save_directory(layer_save_type) layer_save_name = f"{self._save_name.value}.tiff" layer_save_loc = self._get_save_loc( @@ -1094,9 +1070,7 @@ def save_layers_as_ome_tiff(self) -> None: dim_order = "C" + self._squeezed_dims_order else: num_dims = len(layer_data.shape) - dim_order = "C" + "".join( - [str(d) for d in "TZYX"[-(num_dims - 1) :]] - ) + dim_order = "C" + "".join([str(d) for d in "TZYX"[-(num_dims - 1) :]]) self._layer_save_type = layer_save_type @@ -1118,7 +1092,7 @@ def _on_layer_save_complete(self, result: None = None) -> None: self._results.value = ( f"Saved {self._layer_save_type}: " + str(self._save_name.value) - + f"\nAt {time.strftime('%H:%M:%S')}" + + f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_layer_save_error(self, exc: Exception) -> None: @@ -1127,5 +1101,5 @@ def _on_layer_save_error(self, exc: Exception) -> None: self._progress_bar.max = 1 self._progress_bar.value = 0 self._results.value = ( - f"Error saving layers: {exc}\nAt {time.strftime('%H:%M:%S')}" + f'Error saving layers: {exc}\nAt {time.strftime("%H:%M:%S")}' ) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index bfbcdb6..cab0685 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -48,9 +48,7 @@ def test_missing_file_str(self, tmp_path): file1 = directory / "file1.txt" file1.write_text("Test file 1") - missing = check_for_missing_files( - ["file1.txt", "file3.txt"], directory - ) + missing = check_for_missing_files(["file1.txt", "file3.txt"], directory) assert missing == [("file3.txt", "test_dir")] @@ -193,9 +191,7 @@ def test_middle_elision(self): def test_start_elision(self): """Test start elision.""" - assert ( - elide_string("thisisaverylongstring", 10, "start") == "...gstring" - ) + assert elide_string("thisisaverylongstring", 10, "start") == "...gstring" def test_end_elision(self): """Test end elision.""" diff --git a/tests/test_nimage.py b/tests/test_nimage.py index 9c86459..1050bf1 100644 --- a/tests/test_nimage.py +++ b/tests/test_nimage.py @@ -16,10 +16,10 @@ from ndevio import nImage from ndevio.nimage import determine_reader_plugin -RGB_TIFF = ( - "RGB_bad_metadata.tiff" # has two scenes, with really difficult metadata +RGB_TIFF = "RGB_bad_metadata.tiff" # has two scenes, with really difficult metadata +CELLS3D2CH_OME_TIFF = ( + "cells3d2ch_legacy.tiff" # 2 channel, 3D OME-TIFF, from old napari-ndev saving ) -CELLS3D2CH_OME_TIFF = "cells3d2ch_legacy.tiff" # 2 channel, 3D OME-TIFF, from old napari-ndev saving LOGO_PNG = "nDev-logo-small.png" # small PNG file (fix typo) CZI_FILE = "0T-4C-0Z-7pos.czi" # multi-scene CZI file ND2_FILE = "ND2_dims_rgb.nd2" # ND2 file requiring bioio-nd2 @@ -61,9 +61,7 @@ def test_nImage_ome_reader(resources_dir: Path): # available. The project does not require bioio_tifffile as a test # dependency, so skip this part when it's missing. if bioio_tifffile is None: # pragma: no cover - optional - pytest.skip( - "bioio_tifffile not installed; skipping reader-override checks" - ) + pytest.skip("bioio_tifffile not installed; skipping reader-override checks") nimg = nImage(img_path, reader=bioio_tifffile.Reader) @@ -119,9 +117,7 @@ def test_nImage_determine_in_memory_large_file(resources_dir: Path): """Test in-memory determination for large files.""" img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) with ( - mock.patch( - "psutil.virtual_memory", return_value=mock.Mock(available=1e9) - ), + mock.patch("psutil.virtual_memory", return_value=mock.Mock(available=1e9)), mock.patch( "bioio_base.io.pathlike_to_fs", return_value=(mock.Mock(size=lambda x: 5e9), ""), @@ -226,9 +222,7 @@ def test_get_layer_data_tuples_ome_not_implemented_silent( type(img), "ome_metadata", new_callable=mock.PropertyMock, - side_effect=NotImplementedError( - "Reader does not support OME metadata" - ), + side_effect=NotImplementedError("Reader does not support OME metadata"), ): caplog.clear() layer_tuples = img.get_layer_data_tuples() @@ -251,9 +245,7 @@ def test_get_layer_data_mosaic_tile_in_memory(resources_dir: Path): with mock.patch.object(nImage, "reader", create=True) as mock_reader: mock_reader.dims.order = [DimensionNames.MosaicTile] - mock_reader.mosaic_xarray_data.squeeze.return_value = xr.DataArray( - [1, 2, 3] - ) + mock_reader.mosaic_xarray_data.squeeze.return_value = xr.DataArray([1, 2, 3]) img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) img._get_layer_data(in_memory=True) assert img.napari_layer_data is not None @@ -269,8 +261,8 @@ def test_get_layer_data_mosaic_tile_not_in_memory( with mock.patch.object(nImage, "reader", create=True) as mock_reader: mock_reader.dims.order = [DimensionNames.MosaicTile] - mock_reader.mosaic_xarray_dask_data.squeeze.return_value = ( - xr.DataArray([1, 2, 3]) + mock_reader.mosaic_xarray_dask_data.squeeze.return_value = xr.DataArray( + [1, 2, 3] ) img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) img._get_layer_data(in_memory=False) @@ -378,9 +370,7 @@ def test_nimage_init_with_various_formats( error_msg = str(e) # Should contain at least one of the expected error texts if expected_error_contains: - assert any( - text in error_msg for text in expected_error_contains - ) + assert any(text in error_msg for text in expected_error_contains) # ============================================================================= @@ -427,9 +417,7 @@ def test_layer_names_include_channel_names(self, resources_dir: Path): assert "membrane" in names[0] assert "nuclei" in names[1] - def test_single_channel_image_returns_single_tuple( - self, resources_dir: Path - ): + def test_single_channel_image_returns_single_tuple(self, resources_dir: Path): """Test that single channel images return single tuple.""" # PNG is single channel (or RGB treated as single layer) img = nImage(resources_dir / LOGO_PNG) @@ -661,6 +649,4 @@ def test_channel_kwargs_override_metadata(self, resources_dir: Path): assert layer_tuples[0][1]["contrast_limits"] == (0, 1000) # Second channel should have opacity override but default colormap assert layer_tuples[1][1]["opacity"] == 0.5 - assert ( - layer_tuples[1][1]["colormap"] == "green" - ) # default for 2-channel + assert layer_tuples[1][1]["colormap"] == "green" # default for 2-channel diff --git a/tests/test_sampledata.py b/tests/test_sampledata.py index 33c9aba..4d74926 100644 --- a/tests/test_sampledata.py +++ b/tests/test_sampledata.py @@ -14,9 +14,7 @@ ) -def _validate_layer_data_tuples( - result: list, expected_layer_type: str | None = None -): +def _validate_layer_data_tuples(result: list, expected_layer_type: str | None = None): """Helper to validate LayerDataTuple structure. Parameters diff --git a/tests/test_utilities_container.py b/tests/test_utilities_container.py index 328c02e..5acea81 100644 --- a/tests/test_utilities_container.py +++ b/tests/test_utilities_container.py @@ -9,9 +9,7 @@ image_2d = np.asarray([[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 1, 1], [2, 2, 1, 1]]) shapes_2d = np.array([[0.25, 0.25], [0.25, 2.75], [2.75, 2.75], [2.75, 0.25]]) -labels_2d = np.asarray( - [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]] -) +labels_2d = np.asarray([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]]) image_4d = np.random.random((1, 1, 10, 10)) shapes_4d = [ @@ -102,9 +100,7 @@ def test_save_labels(qtbot, make_napari_viewer, tmp_path: Path, test_data): assert saved_img.channel_names == ["Labels"] -def test_save_image_layer( - qtbot, make_napari_viewer, test_data, tmp_path: Path -): +def test_save_image_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): test_image, _, _, squeezed_dims = test_data viewer = make_napari_viewer() viewer.add_image(test_image) @@ -127,9 +123,7 @@ def test_save_image_layer( assert saved_img.channel_names == ["0"] -def test_save_multi_layer( - qtbot, make_napari_viewer, test_data, tmp_path: Path -): +def test_save_multi_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): test_image, _, test_labels, squeezed_dims = test_data viewer = make_napari_viewer() viewer.add_image(test_image) @@ -170,10 +164,7 @@ def test_update_metadata_from_file(make_napari_viewer, test_rgb_image): container.update_metadata_on_file_select() assert container._save_name.value == "RGB_bad_metadata" - assert ( - container._dim_shape.value - == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" - ) + assert container._dim_shape.value == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" assert container._squeezed_dims_order == "YX" assert container._channel_names.value == "['red', 'green', 'blue']" @@ -295,9 +286,7 @@ def test_batch_cancel_button(tmp_path: Path, resources_dir: Path, qtbot): container._concatenate_batch_button.clicked() # Wait for cancellation to complete - qtbot.waitUntil( - lambda: not container._batch_runner.is_running, timeout=10000 - ) + qtbot.waitUntil(lambda: not container._batch_runner.is_running, timeout=10000) # Verify it stopped assert not container._batch_runner.is_running @@ -392,9 +381,7 @@ def test_extract_and_save_scenes_ome_tiff_specific_scenes( save_dir = tmp_path / "ExtractedScenes" # Extract only scenes 0 and 2 - results = list( - extract_and_save_scenes_ome_tiff(path, save_dir, scenes=[0, 2]) - ) + results = list(extract_and_save_scenes_ome_tiff(path, save_dir, scenes=[0, 2])) assert len(results) == 2 assert len(list(save_dir.iterdir())) == 2 @@ -408,10 +395,7 @@ def test_open_images(make_napari_viewer, test_rgb_image): container._files.value = path container.open_images() - assert ( - container._dim_shape.value - == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" - ) + assert container._dim_shape.value == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" assert container._squeezed_dims_order == "YX" assert container._channel_names.value == "['red', 'green', 'blue']" From cea7ba12c869b6dea13feafdd96cce1d4270cb15 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Sun, 7 Dec 2025 22:28:40 -0600 Subject: [PATCH 2/3] update ruff config --- .pre-commit-config.yaml | 21 +- src/ndevio/__init__.py | 12 +- src/ndevio/_bioio_plugin_utils.py | 148 ++++---- src/ndevio/_colormap_utils.py | 18 +- src/ndevio/_napari_reader.py | 30 +- src/ndevio/_plugin_installer.py | 6 +- src/ndevio/_plugin_manager.py | 30 +- src/ndevio/helpers.py | 70 ++-- src/ndevio/nimage.py | 65 ++-- src/ndevio/sampledata/__init__.py | 12 +- src/ndevio/sampledata/_sample_data.py | 46 +-- src/ndevio/widgets/__init__.py | 10 +- src/ndevio/widgets/_plugin_install_widget.py | 42 ++- src/ndevio/widgets/_scene_widget.py | 9 +- src/ndevio/widgets/_utilities_container.py | 378 ++++++++++--------- tests/conftest.py | 2 +- tests/test_bioio_plugin_utils.py | 70 ++-- tests/test_helpers.py | 124 +++--- tests/test_napari_reader.py | 56 +-- tests/test_nimage.py | 212 ++++++----- tests/test_plugin_installer.py | 74 ++-- tests/test_plugin_installer_integration.py | 82 ++-- tests/test_sampledata.py | 22 +- tests/test_utilities_container.py | 142 +++---- 24 files changed, 875 insertions(+), 806 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce0168f..8abd962 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,18 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v4.6.0 hooks: - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace exclude: ^\.napari-hub/.* - id: check-yaml # checks for correct yaml syntax for github actions ex. - exclude: - (?x)(^src/ndevio/ndev_settings\.yaml$) - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.5 + rev: v0.14.6 hooks: - - id: ruff - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - - repo: https://github.com/tlambert03/napari-plugin-checks + - id: ruff-check + - id: ruff-format + - repo: https://github.com/napari/napari-plugin-checks rev: v0.3.0 hooks: - id: napari-plugin-checks @@ -25,9 +20,3 @@ repos: rev: v0.4.0 hooks: - id: reset-settings-values - # https://mypy.readthedocs.io/en/stable/ - # you may wish to add this as well! - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v1.9.0 - # hooks: - # - id: mypy diff --git a/src/ndevio/__init__.py b/src/ndevio/__init__.py index f91ef71..7a20a20 100644 --- a/src/ndevio/__init__.py +++ b/src/ndevio/__init__.py @@ -3,7 +3,7 @@ try: # noqa: D104 from ._version import version as __version__ except ImportError: - __version__ = "unknown" + __version__ = 'unknown' from . import helpers @@ -14,15 +14,15 @@ def __getattr__(name: str): """Lazily import nImage to speed up package import.""" - if name == "nImage": + if name == 'nImage': from .nimage import nImage return nImage - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + raise AttributeError(f'module {__name__!r} has no attribute {name!r}') __all__ = [ - "__version__", - "helpers", - "nImage", + '__version__', + 'helpers', + 'nImage', ] diff --git a/src/ndevio/_bioio_plugin_utils.py b/src/ndevio/_bioio_plugin_utils.py index 2f9313e..9ce480b 100644 --- a/src/ndevio/_bioio_plugin_utils.py +++ b/src/ndevio/_bioio_plugin_utils.py @@ -45,80 +45,80 @@ # 3. Known issues or limitations BIOIO_PLUGINS = { # Highest priority: OME formats with excellent metadata preservation - "bioio-ome-zarr": { - "extensions": [".zarr"], - "description": "OME-Zarr files", - "repository": "https://github.com/bioio-devs/bioio-ome-zarr", - "core": True, + 'bioio-ome-zarr': { + 'extensions': ['.zarr'], + 'description': 'OME-Zarr files', + 'repository': 'https://github.com/bioio-devs/bioio-ome-zarr', + 'core': True, }, - "bioio-ome-tiff": { - "extensions": [".ome.tif", ".ome.tiff", ".tif", ".tiff"], - "description": "OME-TIFF files with valid OME-XML metadata", - "repository": "https://github.com/bioio-devs/bioio-ome-tiff", - "core": True, + 'bioio-ome-tiff': { + 'extensions': ['.ome.tif', '.ome.tiff', '.tif', '.tiff'], + 'description': 'OME-TIFF files with valid OME-XML metadata', + 'repository': 'https://github.com/bioio-devs/bioio-ome-tiff', + 'core': True, }, - "bioio-ome-tiled-tiff": { - "extensions": [".tiles.ome.tif"], - "description": "Tiled OME-TIFF files", - "repository": "https://github.com/bioio-devs/bioio-ome-tiled-tiff", + 'bioio-ome-tiled-tiff': { + 'extensions': ['.tiles.ome.tif'], + 'description': 'Tiled OME-TIFF files', + 'repository': 'https://github.com/bioio-devs/bioio-ome-tiled-tiff', }, # High priority: Format-specific readers with good metadata support - "bioio-tifffile": { - "extensions": [".tif", ".tiff"], - "description": "TIFF files (including those without OME metadata)", - "repository": "https://github.com/bioio-devs/bioio-tifffile", - "core": True, + 'bioio-tifffile': { + 'extensions': ['.tif', '.tiff'], + 'description': 'TIFF files (including those without OME metadata)', + 'repository': 'https://github.com/bioio-devs/bioio-tifffile', + 'core': True, }, - "bioio-nd2": { - "extensions": [".nd2"], - "description": "Nikon ND2 files", - "repository": "https://github.com/bioio-devs/bioio-nd2", + 'bioio-nd2': { + 'extensions': ['.nd2'], + 'description': 'Nikon ND2 files', + 'repository': 'https://github.com/bioio-devs/bioio-nd2', }, - "bioio-czi": { - "extensions": [".czi"], - "description": "Zeiss CZI files", - "repository": "https://github.com/bioio-devs/bioio-czi", + 'bioio-czi': { + 'extensions': ['.czi'], + 'description': 'Zeiss CZI files', + 'repository': 'https://github.com/bioio-devs/bioio-czi', }, - "bioio-lif": { - "extensions": [".lif"], - "description": "Leica LIF files", - "repository": "https://github.com/bioio-devs/bioio-lif", + 'bioio-lif': { + 'extensions': ['.lif'], + 'description': 'Leica LIF files', + 'repository': 'https://github.com/bioio-devs/bioio-lif', }, - "bioio-dv": { - "extensions": [".dv", ".r3d"], - "description": "DeltaVision files", - "repository": "https://github.com/bioio-devs/bioio-dv", + 'bioio-dv': { + 'extensions': ['.dv', '.r3d'], + 'description': 'DeltaVision files', + 'repository': 'https://github.com/bioio-devs/bioio-dv', }, - "bioio-sldy": { - "extensions": [".sldy", ".dir"], - "description": "3i SlideBook files", - "repository": "https://github.com/bioio-devs/bioio-sldy", + 'bioio-sldy': { + 'extensions': ['.sldy', '.dir'], + 'description': '3i SlideBook files', + 'repository': 'https://github.com/bioio-devs/bioio-sldy', }, # Lower priority: Generic/fallback readers - "bioio-imageio": { - "extensions": [".bmp", ".gif", ".jpg", ".jpeg", ".png"], - "description": "Generic image formats (PNG, JPG, etc.)", - "repository": "https://github.com/bioio-devs/bioio-imageio", - "core": True, + 'bioio-imageio': { + 'extensions': ['.bmp', '.gif', '.jpg', '.jpeg', '.png'], + 'description': 'Generic image formats (PNG, JPG, etc.)', + 'repository': 'https://github.com/bioio-devs/bioio-imageio', + 'core': True, }, - "bioio-tiff-glob": { - "extensions": [".tiff"], - "description": "TIFF sequences (glob patterns)", - "repository": "https://github.com/bioio-devs/bioio-tiff-glob", + 'bioio-tiff-glob': { + 'extensions': ['.tiff'], + 'description': 'TIFF sequences (glob patterns)', + 'repository': 'https://github.com/bioio-devs/bioio-tiff-glob', }, # Lowest priority: Requires external dependencies (Java) - "bioio-bioformats": { - "extensions": [".oib", ".oif", ".vsi", ".ims", ".lsm", ".stk"], - "description": "Proprietary microscopy formats (requires Java)", - "repository": "https://github.com/bioio-devs/bioio-bioformats", - "note": "Requires Java Runtime Environment", + 'bioio-bioformats': { + 'extensions': ['.oib', '.oif', '.vsi', '.ims', '.lsm', '.stk'], + 'description': 'Proprietary microscopy formats (requires Java)', + 'repository': 'https://github.com/bioio-devs/bioio-bioformats', + 'note': 'Requires Java Runtime Environment', }, } # Map extensions to plugin names for quick lookup _EXTENSION_TO_PLUGIN = {} for plugin_name, info in BIOIO_PLUGINS.items(): - for ext in info["extensions"]: + for ext in info['extensions']: if ext not in _EXTENSION_TO_PLUGIN: _EXTENSION_TO_PLUGIN[ext] = [] _EXTENSION_TO_PLUGIN[ext].append(plugin_name) @@ -181,7 +181,7 @@ def format_plugin_installation_message( if not suggested_plugins: return ( f"\n\nNo bioio plugins found for '{filename}'.\n" - "See https://github.com/bioio-devs/bioio for available plugins." + 'See https://github.com/bioio-devs/bioio for available plugins.' ) # Format the plugin list (filters out core plugins automatically) @@ -190,36 +190,36 @@ def format_plugin_installation_message( # Build appropriate message based on what's installed/missing if installed_plugins and installable_plugins and plugin_list: # Case 1: Some plugins installed but failed, suggest alternatives - installed_str = ", ".join(sorted(installed_plugins)) + installed_str = ', '.join(sorted(installed_plugins)) return ( f"\n\nInstalled plugin '{installed_str}' failed to read '{filename}'.\n" - "Try one of these alternatives:\n\n" - f"{plugin_list}" - "\nRestart napari/Python after installing." + 'Try one of these alternatives:\n\n' + f'{plugin_list}' + '\nRestart napari/Python after installing.' ) if installed_plugins and not installable_plugins: # Case 2: All suggested plugins already installed but still failed - installed_str = ", ".join(sorted(installed_plugins)) + installed_str = ', '.join(sorted(installed_plugins)) return ( f"\nFile '{filename}' is supported by: {installed_str}\n" - "However, the plugin failed to read it.\n" - "This may indicate a corrupt file or incompatible format variant." + 'However, the plugin failed to read it.\n' + 'This may indicate a corrupt file or incompatible format variant.' ) if plugin_list: # Case 3: No installed plugins, suggest installing return ( f"\n\nTo read '{filename}', install one of:\n\n" - f"{plugin_list}" - "\nRestart napari/Python after installing." + f'{plugin_list}' + '\nRestart napari/Python after installing.' ) # Case 4: All suggested plugins are core plugins (already should be installed) return ( f"\n\nRequired plugins for '{filename}' should already be installed.\n" "If you're still having issues, check your installation or " - "open an issue at https://github.com/ndev-kit/ndevio." + 'open an issue at https://github.com/ndev-kit/ndevio.' ) @@ -254,11 +254,11 @@ def suggest_plugins_for_path(path: Path | str) -> list[str]: # Check compound extensions first (.ome.tiff, .tiles.ome.tif, etc.) for plugin_name, info in BIOIO_PLUGINS.items(): - for ext in info["extensions"]: + for ext in info['extensions']: # Compound extension: multiple dots and matches filename if ( - ext.startswith(".") - and len(ext.split(".")) > 2 + ext.startswith('.') + and len(ext.split('.')) > 2 and filename.endswith(ext) ): return [plugin_name] @@ -286,7 +286,7 @@ def _format_plugin_list(plugin_names: list[str]) -> str: Formatted installation instructions """ if not plugin_names: - return "" + return '' lines = [] for plugin_name in plugin_names: @@ -296,13 +296,13 @@ def _format_plugin_list(plugin_names: list[str]) -> str: continue # Skip core plugins (already installed with ndevio) - if info.get("core", False): + if info.get('core', False): continue - lines.append(f" • {plugin_name}") + lines.append(f' • {plugin_name}') lines.append(f' {info["description"]}') - if info.get("note"): + if info.get('note'): lines.append(f' Note: {info["note"]}') - lines.append(f" Install: pip install {plugin_name}\n") + lines.append(f' Install: pip install {plugin_name}\n') - return "\n".join(lines) + return '\n'.join(lines) diff --git a/src/ndevio/_colormap_utils.py b/src/ndevio/_colormap_utils.py index 0f66819..19ae8ce 100644 --- a/src/ndevio/_colormap_utils.py +++ b/src/ndevio/_colormap_utils.py @@ -4,13 +4,13 @@ multichannel image display. """ -SINGLE_CHANNEL_COLORMAP = "gray" +SINGLE_CHANNEL_COLORMAP = 'gray' -TWO_CHANNEL_CYCLE = ["magenta", "green"] +TWO_CHANNEL_CYCLE = ['magenta', 'green'] -MULTI_CHANNEL_CYCLE = ["cyan", "magenta", "yellow", "blue", "green", "red"] +MULTI_CHANNEL_CYCLE = ['cyan', 'magenta', 'yellow', 'blue', 'green', 'red'] -RGB = ["red", "green", "blue"] +RGB = ['red', 'green', 'blue'] def get_colormap_for_channel(channel_idx: int, n_channels: int) -> str: @@ -43,9 +43,9 @@ def get_colormap_for_channel(channel_idx: int, n_channels: int) -> str: __all__ = [ - "SINGLE_CHANNEL_COLORMAP", - "TWO_CHANNEL_CYCLE", - "MULTI_CHANNEL_CYCLE", - "RGB", - "get_colormap_for_channel", + 'SINGLE_CHANNEL_COLORMAP', + 'TWO_CHANNEL_CYCLE', + 'MULTI_CHANNEL_CYCLE', + 'RGB', + 'get_colormap_for_channel', ] diff --git a/src/ndevio/_napari_reader.py b/src/ndevio/_napari_reader.py index 975a1c4..6cbb310 100644 --- a/src/ndevio/_napari_reader.py +++ b/src/ndevio/_napari_reader.py @@ -51,17 +51,17 @@ def napari_get_reader( open_first_scene_only = ( open_first_scene_only if open_first_scene_only is not None - else settings.ndevio_reader.scene_handling == "View First Scene Only" # type: ignore + else settings.ndevio_reader.scene_handling == 'View First Scene Only' # type: ignore ) or False open_all_scenes = ( open_all_scenes if open_all_scenes is not None - else settings.ndevio_reader.scene_handling == "View All Scenes" # type: ignore + else settings.ndevio_reader.scene_handling == 'View All Scenes' # type: ignore ) or False if isinstance(path, list): - logger.info("Bioio: Expected a single path, got a list of paths.") + logger.info('Bioio: Expected a single path, got a list of paths.') return None try: @@ -76,7 +76,7 @@ def napari_get_reader( except UnsupportedFileFormatError as e: # determine_reader_plugin() already enhanced the error message - logger.error("ndevio: Unsupported file format: %s", path) + logger.error('ndevio: Unsupported file format: %s', path) # Show plugin installer widget if enabled in settings if settings.ndevio_reader.suggest_reader_plugins: # type: ignore _open_plugin_installer(path, e) @@ -85,7 +85,7 @@ def napari_get_reader( return None except Exception as e: # noqa: BLE001 - logger.warning("ndevio: Error reading file: %s", e) + logger.warning('ndevio: Error reading file: %s', e) return None @@ -121,7 +121,7 @@ def napari_reader_function( """ img = nImage(path, reader=reader) - logger.info("Bioio: Reading file with %d scenes", len(img.scenes)) + logger.info('Bioio: Reading file with %d scenes', len(img.scenes)) # open first scene only if len(img.scenes) == 1 or open_first_scene_only: @@ -140,7 +140,9 @@ def napari_reader_function( return [(None,)] -def _open_scene_container(path: PathLike, img: nImage, in_memory: bool) -> None: +def _open_scene_container( + path: PathLike, img: nImage, in_memory: bool +) -> None: from pathlib import Path import napari @@ -150,12 +152,14 @@ def _open_scene_container(path: PathLike, img: nImage, in_memory: bool) -> None: viewer = napari.current_viewer() viewer.window.add_dock_widget( nImageSceneWidget(viewer, path, img, in_memory), - area="right", - name=f"{Path(path).stem}{DELIMITER}Scenes", + area='right', + name=f'{Path(path).stem}{DELIMITER}Scenes', ) -def _open_plugin_installer(path: PathLike, error: UnsupportedFileFormatError) -> None: +def _open_plugin_installer( + path: PathLike, error: UnsupportedFileFormatError +) -> None: """Open the plugin installer widget for an unsupported file. Parameters @@ -177,7 +181,7 @@ def _open_plugin_installer(path: PathLike, error: UnsupportedFileFormatError) -> # Don't try to open widget if no viewer available (e.g., in tests) if viewer is None: logger.warning( - "Cannot open plugin installer widget: No napari viewer available" + 'Cannot open plugin installer widget: No napari viewer available' ) return @@ -187,6 +191,6 @@ def _open_plugin_installer(path: PathLike, error: UnsupportedFileFormatError) -> widget = PluginInstallerWidget(plugin_manager=manager) viewer.window.add_dock_widget( widget, - area="right", - name="Install BioIO Plugin", + area='right', + name='Install BioIO Plugin', ) diff --git a/src/ndevio/_plugin_installer.py b/src/ndevio/_plugin_installer.py index e5f09ee..98ed43e 100644 --- a/src/ndevio/_plugin_installer.py +++ b/src/ndevio/_plugin_installer.py @@ -58,7 +58,7 @@ def install_plugin(plugin_name: str) -> int: InstallerTools, ) - logger.info("Queueing installation for: %s", plugin_name) + logger.info('Queueing installation for: %s', plugin_name) queue = get_installer_queue() @@ -69,7 +69,7 @@ def install_plugin(plugin_name: str) -> int: # Queue the installation job_id = queue.install(tool=tool, pkgs=[plugin_name]) - logger.info("Installation queued with job ID: %s", job_id) + logger.info('Installation queued with job ID: %s', job_id) return job_id @@ -107,7 +107,7 @@ def verify_plugin_installed(plugin_name: str) -> bool: """ try: # Convert plugin name to module name (bioio-czi -> bioio_czi) - module_name = plugin_name.replace("-", "_") + module_name = plugin_name.replace('-', '_') __import__(module_name) return True except ImportError: diff --git a/src/ndevio/_plugin_manager.py b/src/ndevio/_plugin_manager.py index 68bcaff..ac1fc9d 100644 --- a/src/ndevio/_plugin_manager.py +++ b/src/ndevio/_plugin_manager.py @@ -137,7 +137,7 @@ def feasibility_report(self) -> dict[str, PluginSupport]: if self._feasibility_report is None and self.path: from bioio import plugin_feasibility_report - logger.debug("Generating feasibility report for: %s", self.path) + logger.debug('Generating feasibility report for: %s', self.path) self._feasibility_report = plugin_feasibility_report(self.path) return self._feasibility_report or {} @@ -164,7 +164,7 @@ def installed_plugins(self) -> set[str]: ... print("OME-TIFF reader is available") """ report = self.feasibility_report - return {name for name in report if name != "ArrayLike"} + return {name for name in report if name != 'ArrayLike'} @property def suggested_plugins(self) -> list[str]: @@ -219,11 +219,13 @@ def installable_plugins(self) -> list[str]: return [ plugin_name for plugin_name in suggested - if not BIOIO_PLUGINS.get(plugin_name, {}).get("core", False) + if not BIOIO_PLUGINS.get(plugin_name, {}).get('core', False) and plugin_name not in installed ] - def get_working_reader(self, preferred_reader: str | None = None) -> Reader | None: + def get_working_reader( + self, preferred_reader: str | None = None + ) -> Reader | None: """Get a reader that can actually read this file. Tries readers in priority order: @@ -259,8 +261,8 @@ def get_working_reader(self, preferred_reader: str | None = None) -> Reader | No """ if not self.path: logger.warning( - "Cannot get working reader without a path. " - "Initialize ReaderPluginManager with a file path." + 'Cannot get working reader without a path. ' + 'Initialize ReaderPluginManager with a file path.' ) return None @@ -273,7 +275,7 @@ def get_working_reader(self, preferred_reader: str | None = None) -> Reader | No and report[preferred_reader].supported ): logger.info( - "Using preferred reader: %s for %s", + 'Using preferred reader: %s for %s', preferred_reader, self.path, ) @@ -285,7 +287,7 @@ def get_working_reader(self, preferred_reader: str | None = None) -> Reader | No for reader_name in get_reader_priority(): if reader_name in report and report[reader_name].supported: logger.info( - "Using reader: %s for %s (from priority list)", + 'Using reader: %s for %s (from priority list)', reader_name, self.path, ) @@ -293,15 +295,15 @@ def get_working_reader(self, preferred_reader: str | None = None) -> Reader | No # Try any other installed reader that supports the file for name, support in report.items(): - if name != "ArrayLike" and support.supported: + if name != 'ArrayLike' and support.supported: logger.info( - "Using reader: %s for %s (from installed plugins)", + 'Using reader: %s for %s (from installed plugins)', name, self.path, ) return self._get_reader_module(name) - logger.warning("No working reader found for: %s", self.path) + logger.warning('No working reader found for: %s', self.path) return None def get_installation_message(self) -> str: @@ -323,7 +325,7 @@ def get_installation_message(self) -> str: ... print(manager.get_installation_message()) """ if not self.path: - return "" + return '' from ._bioio_plugin_utils import format_plugin_installation_message @@ -354,7 +356,7 @@ def _get_reader_module(reader_name: str) -> Reader: If the reader module cannot be imported """ # Convert plugin name to module name (bioio-czi -> bioio_czi) - module_name = reader_name.replace("-", "_") - logger.debug("Importing reader module: %s", module_name) + module_name = reader_name.replace('-', '_') + logger.debug('Importing reader module: %s', module_name) module = importlib.import_module(module_name) return module.Reader diff --git a/src/ndevio/helpers.py b/src/ndevio/helpers.py index eb17438..26e8b66 100644 --- a/src/ndevio/helpers.py +++ b/src/ndevio/helpers.py @@ -22,12 +22,12 @@ from ndevio import nImage __all__ = [ - "check_for_missing_files", - "create_id_string", - "elide_string", - "get_channel_names", - "get_directory_and_files", - "get_squeezed_dim_order", + 'check_for_missing_files', + 'create_id_string', + 'elide_string', + 'get_channel_names', + 'get_directory_and_files', + 'get_squeezed_dim_order', ] @@ -57,17 +57,17 @@ def get_directory_and_files( """ if pattern is None: pattern = [ - "tif", - "tiff", - "nd2", - "czi", - "lif", - "oib", - "png", - "jpg", - "jpeg", - "bmp", - "gif", + 'tif', + 'tiff', + 'nd2', + 'czi', + 'lif', + 'oib', + 'png', + 'jpg', + 'jpeg', + 'bmp', + 'gif', ] if dir_path is None: return None, [] @@ -75,16 +75,16 @@ def get_directory_and_files( directory = Path(dir_path) if dir_path is not None and not directory.exists(): - raise FileNotFoundError(f"Directory {dir_path} does not exist.") + raise FileNotFoundError(f'Directory {dir_path} does not exist.') pattern = [pattern] if isinstance(pattern, str) else pattern # add *. to each pattern if it doesn't already have either pattern_glob = [] for pat in pattern: - if "." not in pat: - pat = f"*.{pat}" - if "*" not in pat: - pat = f"*{pat}" + if '.' not in pat: + pat = f'*.{pat}' + if '*' not in pat: + pat = f'*{pat}' pattern_glob.append(pat) files = [] @@ -113,15 +113,15 @@ def get_channel_names(img: nImage | BioImage) -> list[str]: The channel names. """ - if "S" in img.dims.order: - return ["red", "green", "blue"] + if 'S' in img.dims.order: + return ['red', 'green', 'blue'] # Ensure we have plain Python strings, not numpy string types return [str(c) for c in img.channel_names] def get_squeezed_dim_order( img: nImage | BioImage, - skip_dims: tuple[str, ...] | list[str] | str = ("C", "S"), + skip_dims: tuple[str, ...] | list[str] | str = ('C', 'S'), ) -> str: """ Return a string containing the squeezed dimensions of the given BioImage. @@ -141,7 +141,9 @@ def get_squeezed_dim_order( """ if isinstance(skip_dims, str): skip_dims = (skip_dims,) - return "".join({k: v for k, v in img.dims.items() if v > 1 and k not in skip_dims}) + return ''.join( + {k: v for k, v in img.dims.items() if v > 1 and k not in skip_dims} + ) def create_id_string(img: nImage | BioImage, identifier: str) -> str: @@ -175,7 +177,7 @@ def create_id_string(img: nImage | BioImage, identifier: str) -> str: scene = img.ome_metadata.images[scene_idx].name except NotImplementedError: scene = img.current_scene # not useful with OmeTiffReader, atm - id_string = f"{identifier}__{scene_idx}__{scene}" + id_string = f'{identifier}__{scene_idx}__{scene}' return id_string @@ -214,7 +216,7 @@ def check_for_missing_files( def elide_string( - input_string: str, max_length: int = 15, location: str = "middle" + input_string: str, max_length: int = 15, location: str = 'middle' ) -> str: """ Elide a string if it exceeds the specified length. @@ -247,11 +249,11 @@ def elide_string( if max_length <= 5: return input_string[:max_length] # Elide the string based on the location - if location == "start": - return "..." + input_string[-(max_length - 3) :] - if location == "end": - return input_string[: max_length - 3] + "..." - if location == "middle": + if location == 'start': + return '...' + input_string[-(max_length - 3) :] + if location == 'end': + return input_string[: max_length - 3] + '...' + if location == 'middle': half_length = (max_length - 3) // 2 - return input_string[:half_length] + "..." + input_string[-half_length:] + return input_string[:half_length] + '...' + input_string[-half_length:] raise ValueError('Invalid location. Must be "start", "middle", or "end".') diff --git a/src/ndevio/nimage.py b/src/ndevio/nimage.py index dd712ea..1925de5 100644 --- a/src/ndevio/nimage.py +++ b/src/ndevio/nimage.py @@ -17,10 +17,10 @@ logger = logging.getLogger(__name__) -DELIM = " :: " +DELIM = ' :: ' # Keywords that indicate a channel contains labels/segmentation data -LABEL_KEYWORDS = ["label", "mask", "segmentation", "seg", "roi"] +LABEL_KEYWORDS = ['label', 'mask', 'segmentation', 'seg', 'roi'] def determine_reader_plugin( @@ -79,7 +79,7 @@ def determine_reader_plugin( msg_extra = None raise UnsupportedFileFormatError( - reader_name="ndevio", + reader_name='ndevio', path=str(image), msg_extra=msg_extra, ) @@ -205,7 +205,9 @@ def _get_layer_data(self, in_memory: bool | None = None) -> xr.DataArray: if DimensionNames.MosaicTile in self.reader.dims.order: try: if in_memory: - self.napari_layer_data = self.reader.mosaic_xarray_data.squeeze() + self.napari_layer_data = ( + self.reader.mosaic_xarray_data.squeeze() + ) else: self.napari_layer_data = ( self.reader.mosaic_xarray_dask_data.squeeze() @@ -213,7 +215,7 @@ def _get_layer_data(self, in_memory: bool | None = None) -> xr.DataArray: except NotImplementedError: logger.warning( - "Bioio: Mosaic tile switching not supported for this reader" + 'Bioio: Mosaic tile switching not supported for this reader' ) return None else: @@ -228,8 +230,8 @@ def _infer_layer_type(self, channel_name: str) -> str: """Infer layer type from channel name.""" name_lower = channel_name.lower() if any(keyword in name_lower for keyword in LABEL_KEYWORDS): - return "labels" - return "image" + return 'labels' + return 'image' def _get_scale(self) -> tuple | None: """Extract physical pixel scale from image metadata according to number of napari dims.""" @@ -261,10 +263,10 @@ def _build_metadata(self) -> dict: 'ome_metadata'. """ - img_meta = {"bioimage": self, "raw_image_metadata": self.metadata} + img_meta = {'bioimage': self, 'raw_image_metadata': self.metadata} try: - img_meta["ome_metadata"] = self.ome_metadata + img_meta['ome_metadata'] = self.ome_metadata except NotImplementedError: pass # Reader doesn't support OME metadata except (ValueError, TypeError, KeyError) as e: @@ -273,7 +275,7 @@ def _build_metadata(self) -> dict: # As such, when accessing ome_metadata, we may get various exceptions # Log warning but continue - raw metadata is still available logger.warning( - "Could not parse OME metadata: %s. " + 'Could not parse OME metadata: %s. ' "Raw metadata is still available in 'raw_image_metadata'.", e, ) @@ -299,10 +301,12 @@ def _build_layer_name( Formatted layer name. """ - path_stem = Path(self.path).stem if self.path is not None else "unknown path" + path_stem = ( + Path(self.path).stem if self.path is not None else 'unknown path' + ) # Check if scene info is meaningful - no_scene = len(self.scenes) == 1 and self.current_scene == "Image:0" + no_scene = len(self.scenes) == 1 and self.current_scene == 'Image:0' parts = [] if channel_name: @@ -354,22 +358,24 @@ def _build_single_layer_tuple( """ meta = { - "name": self._build_layer_name(channel_name), - "metadata": base_metadata, + 'name': self._build_layer_name(channel_name), + 'metadata': base_metadata, } if scale: - meta["scale"] = scale + meta['scale'] = scale # Add image-specific metadata - if layer_type == "image": + if layer_type == 'image': from ._colormap_utils import get_colormap_for_channel - meta["colormap"] = get_colormap_for_channel(channel_idx, n_channels) - meta["blending"] = ( - "additive" + meta['colormap'] = get_colormap_for_channel( + channel_idx, n_channels + ) + meta['blending'] = ( + 'additive' if channel_idx > 0 and n_channels > 1 - else "translucent_no_depth" + else 'translucent_no_depth' ) # Apply per-channel overrides @@ -477,18 +483,20 @@ def get_layer_data_tuples( # Handle RGB images specially if DimensionNames.Samples in self.reader.dims.order: meta = { - "name": self._build_layer_name(), - "rgb": True, - "metadata": base_metadata, + 'name': self._build_layer_name(), + 'rgb': True, + 'metadata': base_metadata, } if scale: - meta["scale"] = scale - self.layer_data_tuples = [(self.napari_layer_data.data, meta, "image")] + meta['scale'] = scale + self.layer_data_tuples = [ + (self.napari_layer_data.data, meta, 'image') + ] return self.layer_data_tuples # Single channel image (no channel dimension) if channel_dim not in self.napari_layer_data.dims: - effective_type = layer_type or "image" + effective_type = layer_type or 'image' self.layer_data_tuples = [ self._build_single_layer_tuple( data=self.napari_layer_data.data, @@ -503,7 +511,8 @@ def get_layer_data_tuples( # Multichannel image - split into separate layers channel_names = [ - str(c) for c in self.napari_layer_data.coords[channel_dim].data.tolist() + str(c) + for c in self.napari_layer_data.coords[channel_dim].data.tolist() ] channel_axis = self.napari_layer_data.dims.index(channel_dim) n_channels = self.napari_layer_data.shape[channel_axis] @@ -511,7 +520,7 @@ def get_layer_data_tuples( layer_tuples = [] for i in range(n_channels): channel_name = ( - channel_names[i] if i < len(channel_names) else f"channel_{i}" + channel_names[i] if i < len(channel_names) else f'channel_{i}' ) effective_type = self._resolve_layer_type( channel_name, layer_type, channel_types diff --git a/src/ndevio/sampledata/__init__.py b/src/ndevio/sampledata/__init__.py index 4b5f823..654872d 100644 --- a/src/ndevio/sampledata/__init__.py +++ b/src/ndevio/sampledata/__init__.py @@ -10,10 +10,10 @@ ) __all__ = [ - "ndev_logo", - "neocortex", - "neuron_labels", - "neuron_labels_processed", - "neuron_raw", - "scratch_assay", + 'ndev_logo', + 'neocortex', + 'neuron_labels', + 'neuron_labels_processed', + 'neuron_raw', + 'scratch_assay', ] diff --git a/src/ndevio/sampledata/_sample_data.py b/src/ndevio/sampledata/_sample_data.py index ba38257..4cc45a4 100644 --- a/src/ndevio/sampledata/_sample_data.py +++ b/src/ndevio/sampledata/_sample_data.py @@ -19,13 +19,13 @@ if TYPE_CHECKING: from napari.types import LayerDataTuple -SAMPLE_DIR = Path(__file__).parent / "data" +SAMPLE_DIR = Path(__file__).parent / 'data' def ndev_logo() -> list[LayerDataTuple]: """Load the ndev logo image.""" return nImage( - SAMPLE_DIR / "ndev-logo.png", + SAMPLE_DIR / 'ndev-logo.png', reader=ImageIOReader, ).get_layer_data_tuples() @@ -33,9 +33,9 @@ def ndev_logo() -> list[LayerDataTuple]: def scratch_assay() -> list[LayerDataTuple]: """Load scratch assay data with labeled nuclei and cytoplasm.""" scratch_assay_raw_path = pooch.retrieve( - url="doi:10.5281/zenodo.17845346/scratch-assay-labeled-10T-2Ch.tiff", - known_hash="md5:2b98c4ea18cd741a1545e59855348a2f", - fname="scratch-assay-labeled-10T-2Ch.tiff", + url='doi:10.5281/zenodo.17845346/scratch-assay-labeled-10T-2Ch.tiff', + known_hash='md5:2b98c4ea18cd741a1545e59855348a2f', + fname='scratch-assay-labeled-10T-2Ch.tiff', path=SAMPLE_DIR, ) img = nImage( @@ -45,14 +45,14 @@ def scratch_assay() -> list[LayerDataTuple]: return img.get_layer_data_tuples( in_memory=True, channel_types={ - "H3342": "image", - "oblique": "image", - "nuclei": "labels", - "cyto": "labels", + 'H3342': 'image', + 'oblique': 'image', + 'nuclei': 'labels', + 'cyto': 'labels', }, channel_kwargs={ - "H3342": {"colormap": "cyan"}, - "oblique": {"colormap": "gray"}, + 'H3342': {'colormap': 'cyan'}, + 'oblique': {'colormap': 'gray'}, }, ) @@ -60,9 +60,9 @@ def scratch_assay() -> list[LayerDataTuple]: def neocortex() -> list[LayerDataTuple]: """Load neocortex 3-channel image data.""" neocortex_raw_path = pooch.retrieve( - url="doi:10.5281/zenodo.17845346/neocortex-3Ch.tiff", - known_hash="md5:eadc3fac751052461fb2e5f3c6716afa", - fname="neocortex-3Ch.tiff", + url='doi:10.5281/zenodo.17845346/neocortex-3Ch.tiff', + known_hash='md5:eadc3fac751052461fb2e5f3c6716afa', + fname='neocortex-3Ch.tiff', path=SAMPLE_DIR, ) return nImage( @@ -77,9 +77,9 @@ def neuron_raw() -> list[LayerDataTuple]: This sample is downloaded from Zenodo if not present locally. """ neuron_raw_path = pooch.retrieve( - url="doi:10.5281/zenodo.17845346/neuron-4Ch_raw.tiff", - known_hash="md5:5d3e42bca2085e8588b6f23cf89ba87c", - fname="neuron-4Ch_raw.tiff", + url='doi:10.5281/zenodo.17845346/neuron-4Ch_raw.tiff', + known_hash='md5:5d3e42bca2085e8588b6f23cf89ba87c', + fname='neuron-4Ch_raw.tiff', path=SAMPLE_DIR, ) return nImage( @@ -87,9 +87,9 @@ def neuron_raw() -> list[LayerDataTuple]: reader=OmeTiffReader, ).get_layer_data_tuples( in_memory=True, - layer_type="image", + layer_type='image', channel_kwargs={ - "PHALL": {"colormap": "gray"}, + 'PHALL': {'colormap': 'gray'}, }, ) @@ -97,20 +97,20 @@ def neuron_raw() -> list[LayerDataTuple]: def neuron_labels() -> list[LayerDataTuple]: """Load neuron labels data.""" return nImage( - SAMPLE_DIR / "neuron-4Ch_labels.tiff", + SAMPLE_DIR / 'neuron-4Ch_labels.tiff', reader=OmeTiffReader, ).get_layer_data_tuples( in_memory=True, - layer_type="labels", + layer_type='labels', ) def neuron_labels_processed() -> list[LayerDataTuple]: """Load processed neuron labels data.""" return nImage( - SAMPLE_DIR / "neuron-4Ch_labels_processed.tiff", + SAMPLE_DIR / 'neuron-4Ch_labels_processed.tiff', reader=OmeTiffReader, ).get_layer_data_tuples( in_memory=True, - layer_type="labels", + layer_type='labels', ) diff --git a/src/ndevio/widgets/__init__.py b/src/ndevio/widgets/__init__.py index 8e5235f..10ae809 100644 --- a/src/ndevio/widgets/__init__.py +++ b/src/ndevio/widgets/__init__.py @@ -6,9 +6,9 @@ from ._utilities_container import UtilitiesContainer __all__ = [ - "PluginInstallerWidget", - "nImageSceneWidget", - "UtilitiesContainer", - "DELIMITER", - "ReaderPluginManager", + 'PluginInstallerWidget', + 'nImageSceneWidget', + 'UtilitiesContainer', + 'DELIMITER', + 'ReaderPluginManager', ] diff --git a/src/ndevio/widgets/_plugin_install_widget.py b/src/ndevio/widgets/_plugin_install_widget.py index 28d76a2..84c91a2 100644 --- a/src/ndevio/widgets/_plugin_install_widget.py +++ b/src/ndevio/widgets/_plugin_install_widget.py @@ -80,20 +80,24 @@ def _init_widgets(self): if self.manager.path is not None: # Error mode: show file that failed file_name = self.manager.path.name - self._title_label = Label(value=f"Cannot read file: {file_name}") + self._title_label = Label( + value=f'Cannot read file: {file_name}' + ) else: # Standalone mode: general title - self._title_label = Label(value="Install BioIO Reader Plugin") + self._title_label = Label( + value='Install BioIO Reader Plugin' + ) self.append(self._title_label) - self._info_label = Label(value="Select a plugin to install:") + self._info_label = Label(value='Select a plugin to install:') self.append(self._info_label) # Get all available plugin names from manager plugin_names = self.manager.available_plugins self._plugin_select = ComboBox( - label="Plugin", + label='Plugin', choices=plugin_names, value=None, nullable=True, @@ -107,11 +111,11 @@ def _init_widgets(self): self.append(self._plugin_select) # Install button - self._install_button = PushButton(text="Install Plugin") + self._install_button = PushButton(text='Install Plugin') self.append(self._install_button) # Status label - self._status_label = Label(value="") + self._status_label = Label(value='') self.append(self._status_label) def _connect_events(self): @@ -120,16 +124,16 @@ def _connect_events(self): def _on_install_clicked(self): """Handle install button click.""" - self._status_label.value = "Installing..." + self._status_label.value = 'Installing...' # Get selected plugin name plugin_name = self._plugin_select.value if not plugin_name: - self._status_label.value = "No plugin selected" + self._status_label.value = 'No plugin selected' return - logger.info("User requested install of: %s", plugin_name) + logger.info('User requested install of: %s', plugin_name) # Use napari-plugin-manager's InstallerQueue from .._plugin_installer import get_installer_queue, install_plugin @@ -140,8 +144,8 @@ def _on_install_clicked(self): # Connect to the queue's signals to monitor progress def on_process_finished(event): """Handle installation completion.""" - exit_code = event.get("exit_code", 1) - pkgs = event.get("pkgs", []) + exit_code = event.get('exit_code', 1) + pkgs = event.get('pkgs', []) # Check if this event is for our package if plugin_name not in pkgs: @@ -149,17 +153,17 @@ def on_process_finished(event): if exit_code == 0: self._status_label.value = ( - f"✓ Successfully installed {plugin_name}!\n\n" - "⚠ It is recommended to restart napari." + f'✓ Successfully installed {plugin_name}!\n\n' + '⚠ It is recommended to restart napari.' ) - logger.info("Plugin installed successfully: %s", plugin_name) + logger.info('Plugin installed successfully: %s', plugin_name) else: self._status_label.value = ( - f"✗ Installation failed for {plugin_name}\n" - f"Exit code: {exit_code}\n" - "Check the console for details." + f'✗ Installation failed for {plugin_name}\n' + f'Exit code: {exit_code}\n' + 'Check the console for details.' ) - logger.error("Plugin installation failed: %s", plugin_name) + logger.error('Plugin installation failed: %s', plugin_name) # Disconnect after completion (success or failure) if self._queue_connection is not None: @@ -176,4 +180,4 @@ def on_process_finished(event): # Queue the installation (returns job ID) job_id = install_plugin(plugin_name) - logger.info("Installation job %s queued for %s", job_id, plugin_name) + logger.info('Installation job %s queued for %s', job_id, plugin_name) diff --git a/src/ndevio/widgets/_scene_widget.py b/src/ndevio/widgets/_scene_widget.py index 2105825..b4bb3d2 100644 --- a/src/ndevio/widgets/_scene_widget.py +++ b/src/ndevio/widgets/_scene_widget.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) -DELIMITER = " :: " +DELIMITER = ' :: ' class nImageSceneWidget(Container): @@ -92,7 +92,8 @@ def __init__( self.in_memory = in_memory self.settings = get_settings() self.scenes = [ - f"{idx}{DELIMITER}{scene}" for idx, scene in enumerate(self.img.scenes) + f'{idx}{DELIMITER}{scene}' + for idx, scene in enumerate(self.img.scenes) ] self._init_widgets() @@ -128,6 +129,8 @@ def open_scene(self) -> None: # Get layer tuples and add to viewer using napari's Layer.create() from napari.layers import Layer - for ldt in self.img.get_layer_data_tuples(in_memory=self.in_memory): + for ldt in self.img.get_layer_data_tuples( + in_memory=self.in_memory + ): layer = Layer.create(*ldt) self.viewer.add_layer(layer) diff --git a/src/ndevio/widgets/_utilities_container.py b/src/ndevio/widgets/_utilities_container.py index aef65ed..2ebcdf9 100644 --- a/src/ndevio/widgets/_utilities_container.py +++ b/src/ndevio/widgets/_utilities_container.py @@ -39,7 +39,7 @@ def save_ome_tiff( data: np.ndarray, uri: Path, - dim_order: str = "TCZYX", + dim_order: str = 'TCZYX', channel_names: list[str] | None = None, image_name: str | None = None, physical_pixel_sizes: PhysicalPixelSizes | None = None, @@ -76,12 +76,12 @@ def save_ome_tiff( # Validate channel_names count matches data if channel_names is not None and dim_order: - channel_idx = dim_order.upper().find("C") + channel_idx = dim_order.upper().find('C') if channel_idx != -1: if channel_idx >= len(data.shape): logging.warning( - "dim_order %r has C at index %d, but data has only %d " - "dimensions. Ignoring channel_names.", + 'dim_order %r has C at index %d, but data has only %d ' + 'dimensions. Ignoring channel_names.', dim_order, channel_idx, len(data.shape), @@ -139,8 +139,8 @@ def concatenate_and_save_files( array_list = [] for file in files: img = nImage(file) - if "S" in img.dims.order: - img_data = img.get_image_data("TSZYX") + if 'S' in img.dims.order: + img_data = img.get_image_data('TSZYX') else: img_data = img.data @@ -151,18 +151,20 @@ def concatenate_and_save_files( array_list.append(array) if not array_list: - raise ValueError(f"No valid channels found in files: {[str(f) for f in files]}") + raise ValueError( + f'No valid channels found in files: {[str(f) for f in files]}' + ) img_data = np.concatenate(array_list, axis=1) # Save as OME-TIFF save_directory.mkdir(parents=True, exist_ok=True) - save_path = save_directory / f"{save_name}.tiff" + save_path = save_directory / f'{save_name}.tiff' save_ome_tiff( data=img_data, uri=save_path, - dim_order="TCZYX", + dim_order='TCZYX', channel_names=channel_names, image_name=save_name, physical_pixel_sizes=p_sizes, @@ -227,12 +229,12 @@ def extract_and_save_scenes_ome_tiff( # Create ID string for this scene image_id = helpers.create_id_string(img, base_save_name) - save_path = save_directory / f"{image_id}.tiff" + save_path = save_directory / f'{image_id}.tiff' save_ome_tiff( data=img.data, uri=save_path, - dim_order="TCZYX", + dim_order='TCZYX', channel_names=channel_names, image_name=image_id, physical_pixel_sizes=p_sizes, @@ -294,7 +296,7 @@ def _init_batch_runner(self): def _on_batch_start(self, total: int): """Callback when batch starts - initialize progress bar.""" - self._progress_bar.label = f"Processing {total} file sets" + self._progress_bar.label = f'Processing {total} file sets' self._progress_bar.value = 0 self._progress_bar.max = total self._set_batch_button_state(running=True) @@ -304,7 +306,7 @@ def _on_batch_item_complete(self, result, ctx): self._progress_bar.value = ctx.index + 1 # ctx.item is (files, save_name) tuple _, save_name = ctx.item - self._progress_bar.label = f"Processed {save_name}" + self._progress_bar.label = f'Processed {save_name}' def _on_batch_complete(self): """Callback when the entire batch completes.""" @@ -312,36 +314,34 @@ def _on_batch_complete(self): errors = self._batch_runner.error_count if errors > 0: self._progress_bar.label = ( - f"Completed {total - errors} file sets ({errors} Errors)" + f'Completed {total - errors} file sets ({errors} Errors)' ) else: - self._progress_bar.label = f"Completed {total} file sets" + self._progress_bar.label = f'Completed {total} file sets' self._set_batch_button_state(running=False) - self._results.value = ( - f"Batch concatenated files in directory.\nAt {time.strftime('%H:%M:%S')}" - ) + self._results.value = f'Batch concatenated files in directory.\nAt {time.strftime("%H:%M:%S")}' def _on_batch_error(self, ctx, exception): """Callback when a batch item fails.""" _, save_name = ctx.item error_msg = str(exception) if len(error_msg) > 100: - error_msg = error_msg[:100] + "..." - self._progress_bar.label = f"Error on {save_name}: {error_msg}" + error_msg = error_msg[:100] + '...' + self._progress_bar.label = f'Error on {save_name}: {error_msg}' def _set_batch_button_state(self, running: bool): """Update batch button appearance based on running state.""" if running: - self._concatenate_batch_button.text = "Cancel" + self._concatenate_batch_button.text = 'Cancel' self._concatenate_batch_button.tooltip = ( - "Cancel the current batch operation." + 'Cancel the current batch operation.' ) else: - self._concatenate_batch_button.text = "Batch Concat." + self._concatenate_batch_button.text = 'Batch Concat.' self._concatenate_batch_button.tooltip = ( - "Concatenate files in the selected directory by iterating" - " over the remaining files in the directory based on the " - "number of files selected." + 'Concatenate files in the selected directory by iterating' + ' over the remaining files in the directory based on the ' + 'number of files selected.' ) def _on_batch_button_clicked(self): @@ -349,73 +349,75 @@ def _on_batch_button_clicked(self): if self._batch_runner.is_running: self._batch_runner.cancel() self._set_batch_button_state(running=False) - self._progress_bar.label = "Cancelled" + self._progress_bar.label = 'Cancelled' else: self.batch_concatenate_files() def _init_widgets(self): """Initialize widgets.""" self._save_directory_prefix = LineEdit( - label="Save Dir. Prefix", - tooltip="Prefix for the save directories.", + label='Save Dir. Prefix', + tooltip='Prefix for the save directories.', ) self._save_directory = FileEdit( - mode="d", - tooltip="Directory where images will be saved. \n" - "Upon selecting the first file, the save directory will be set \n" - "to the grandparent directory of the first file.", + mode='d', + tooltip='Directory where images will be saved. \n' + 'Upon selecting the first file, the save directory will be set \n' + 'to the grandparent directory of the first file.', ) self._save_directory_container = Container( widgets=[self._save_directory_prefix, self._save_directory], - layout="horizontal", + layout='horizontal', ) self._default_save_directory = self._save_directory.value self._files = FileEdit( - mode="rm", - tooltip="Select file(s) to load.", + mode='rm', + tooltip='Select file(s) to load.', ) - self._progress_bar = ProgressBar(label="Progress") - self._results = TextEdit(label="Info") + self._progress_bar = ProgressBar(label='Progress') + self._results = TextEdit(label='Info') def _init_save_name_container(self): """Initialize the save name container.""" - self._save_name_container = Container(layout="horizontal") + self._save_name_container = Container(layout='horizontal') self._save_name = LineEdit( - label="Save Name", - tooltip="Name of the saved file. " - "Proper extension will be added when saved.", + label='Save Name', + tooltip='Name of the saved file. ' + 'Proper extension will be added when saved.', ) self._append_scene_button = PushButton( - label="Append Scene to Name", + label='Append Scene to Name', + ) + self._save_name_container.extend( + [self._save_name, self._append_scene_button] ) - self._save_name_container.extend([self._save_name, self._append_scene_button]) def _init_open_image_container(self): """Initialize the open image container.""" - self._open_image_container = Container(layout="horizontal") - self._open_image_button = PushButton(label="Open File(s)") + self._open_image_container = Container(layout='horizontal') + self._open_image_button = PushButton(label='Open File(s)') self._select_next_image_button = PushButton( - label="Select Next", - tooltip="Select the next file(s) in the directory. \n" - "Note that the files are sorted alphabetically and numerically.", + label='Select Next', + tooltip='Select the next file(s) in the directory. \n' + 'Note that the files are sorted alphabetically and numerically.', ) self._open_image_container.append(self._open_image_button) self._open_image_container.append(self._select_next_image_button) def _init_concatenate_files_container(self): self._concatenate_files_container = Container( - layout="horizontal", + layout='horizontal', ) - self._concatenate_files_button = PushButton(label="Concat. Files") + self._concatenate_files_button = PushButton(label='Concat. Files') self._concatenate_batch_button = PushButton( - label="Batch Concat.", - tooltip="Concatenate files in the selected directory by iterating" - " over the remaing files in the directory based on the number of" - " files selected. The files are sorted " - "alphabetically and numerically, which may not be consistent " - "with your file viewer. But, opening related consecutive files " - "should work as expected.", + label='Batch Concat.', + tooltip='Concatenate files in the selected directory by iterating' + ' over the remaing files in the directory based on the number of' + ' files selected. The files are sorted ' + 'alphabetically and numerically, which may not be consistent ' + 'with your file viewer. But, opening related consecutive files ' + 'should work as expected.', ) self._concatenate_files_container.extend( [ @@ -427,68 +429,68 @@ def _init_concatenate_files_container(self): def _init_metadata_container(self): self._update_scale = CheckBox( value=True, - label="Scale", - tooltip="Update the scale when files are selected.", + label='Scale', + tooltip='Update the scale when files are selected.', ) self._update_channel_names = CheckBox( value=True, - label="Channel Names", - tooltip="Update the channel names when files are selected.", + label='Channel Names', + tooltip='Update the channel names when files are selected.', ) self._file_options_container = GroupBoxContainer( - layout="horizontal", - name="Update Metadata on File Select", + layout='horizontal', + name='Update Metadata on File Select', labels=False, label=False, widgets=[self._update_scale, self._update_channel_names], ) self._layer_metadata_update_button = PushButton( - label="Update from Selected Layer" + label='Update from Selected Layer' ) self._num_scenes_label = Label( - label="Num. Scenes: ", + label='Num. Scenes: ', ) self._dim_shape = LineEdit( - label="Dims: ", - tooltip="Sanity check for available dimensions.", + label='Dims: ', + tooltip='Sanity check for available dimensions.', ) self._image_info_container = Container( widgets=[self._num_scenes_label, self._dim_shape], - layout="horizontal", + layout='horizontal', ) self._channel_names = LineEdit( - label="Channel Name(s)", - tooltip="Enter channel names as a list. If left blank or the " - "channel names are not the proper length, then default channel " - "names will be used.", + label='Channel Name(s)', + tooltip='Enter channel names as a list. If left blank or the ' + 'channel names are not the proper length, then default channel ' + 'names will be used.', ) self._scale_tuple = TupleEdit( - label="Scale, ZYX", - tooltip="Pixel size, usually in μm", + label='Scale, ZYX', + tooltip='Pixel size, usually in μm', value=(0.0000, 1.0000, 1.0000), - options={"step": 0.0001}, + options={'step': 0.0001}, ) self._channel_scale_container = Container( widgets=[self._channel_names, self._scale_tuple], ) self._scale_layers_button = PushButton( - label="Scale Layer(s)", - tooltip="Scale the selected layer(s) based on the given scale.", + label='Scale Layer(s)', + tooltip='Scale the selected layer(s) based on the given scale.', ) self._metadata_button_container = Container( widgets=[ self._layer_metadata_update_button, self._scale_layers_button, ], - layout="horizontal", + layout='horizontal', ) self._metadata_container = GroupBoxContainer( - layout="vertical", - name="Metadata", + layout='vertical', + name='Metadata', widgets=[ self._file_options_container, self._image_info_container, @@ -501,16 +503,16 @@ def _init_metadata_container(self): def _init_scene_container(self): """Initialize the scene container, allowing scene saving.""" self._scene_container = Container( - layout="horizontal", - tooltip="Must be in list index format. Ex: [0, 1, 2] or [5:10]", + layout='horizontal', + tooltip='Must be in list index format. Ex: [0, 1, 2] or [5:10]', ) self._scenes_to_extract = LineEdit( - tooltip="Enter the scenes to extract as a list. If left blank " - "then all scenes will be extracted.", + tooltip='Enter the scenes to extract as a list. If left blank ' + 'then all scenes will be extracted.', ) self._extract_scenes = PushButton( - label="Extract and Save Scenes", - tooltip="Extract scenes from a single selected file.", + label='Extract and Save Scenes', + tooltip='Extract scenes from a single selected file.', ) self._scene_container.append(self._scenes_to_extract) self._scene_container.append(self._extract_scenes) @@ -518,31 +520,31 @@ def _init_scene_container(self): def _init_save_layers_container(self): """Initialize the container to save images, labels, and shapes.""" self._save_layers_button = PushButton( - text="Selected Layers (TIFF)", - tooltip="Concatenate and save all selected layers as OME-TIFF. " - "Layers will save to corresponding directories based on the layer " - "type, e.g. Images, Labels, ShapesAsLabels. Shapes are saved as " - "labels based on the selected image layer dimensions. If multiple " - "layer types are selected, then the image will save to Layers.", + text='Selected Layers (TIFF)', + tooltip='Concatenate and save all selected layers as OME-TIFF. ' + 'Layers will save to corresponding directories based on the layer ' + 'type, e.g. Images, Labels, ShapesAsLabels. Shapes are saved as ' + 'labels based on the selected image layer dimensions. If multiple ' + 'layer types are selected, then the image will save to Layers.', ) self._export_figure_button = PushButton( - text="Figure (PNG)", - tooltip="Export the current canvas figure as a PNG to the Figure " - "directory. Only works in 2D mode. Use Screenshot for 3D figures. " - "Crops the figure to the extent of the data, attempting to remove " - "margins. Increase or decrease scaling in the settings", + text='Figure (PNG)', + tooltip='Export the current canvas figure as a PNG to the Figure ' + 'directory. Only works in 2D mode. Use Screenshot for 3D figures. ' + 'Crops the figure to the extent of the data, attempting to remove ' + 'margins. Increase or decrease scaling in the settings', ) self._export_screenshot_button = PushButton( - text="Canvas (PNG)", - tooltip="Export the current canvas screenshot as a PNG to the " - "Figure directory. Works in 2D and 3D mode. Uses the full canvas " - "size, including margins. Increase or decrease scaling in the " - "settings, and also it is possible to override the canvas size.", + text='Canvas (PNG)', + tooltip='Export the current canvas screenshot as a PNG to the ' + 'Figure directory. Works in 2D and 3D mode. Uses the full canvas ' + 'size, including margins. Increase or decrease scaling in the ' + 'settings, and also it is possible to override the canvas size.', ) self._save_layers_container = GroupBoxContainer( - layout="horizontal", - name="Export", + layout='horizontal', + name='Export', labels=None, ) @@ -561,7 +563,7 @@ def _init_layout(self): self._files, self._open_image_container, ], - name="Opening", + name='Opening', labels=False, ) self._save_group = GroupBoxContainer( @@ -573,7 +575,7 @@ def _init_layout(self): self._save_layers_container, self._progress_bar, ], - name="Saving", + name='Saving', labels=False, ) @@ -599,8 +601,12 @@ def _connect_events(self): ) self._scale_layers_button.clicked.connect(self.rescale_by) - self._concatenate_files_button.clicked.connect(self.save_files_as_ome_tiff) - self._concatenate_batch_button.clicked.connect(self._on_batch_button_clicked) + self._concatenate_files_button.clicked.connect( + self.save_files_as_ome_tiff + ) + self._concatenate_batch_button.clicked.connect( + self._on_batch_button_clicked + ) self._extract_scenes.clicked.connect(self.save_scenes_ome_tiff) self._save_layers_button.clicked.connect(self.save_layers_as_ome_tiff) self._export_figure_button.clicked.connect(self.canvas_export_figure) @@ -630,7 +636,7 @@ def _update_metadata_from_Image( update_scale: bool = True, ): """Update the metadata based on the given image.""" - dims = re.search(r"\[(.*?)\]", str(img.dims)).group(1) + dims = re.search(r'\[(.*?)\]', str(img.dims)).group(1) self._dim_shape.value = dims self._num_scenes_label.value = str(len(img.scenes)) @@ -664,35 +670,35 @@ def append_scene_to_name(self): """Append the scene to the save name.""" if self._viewer.layers.selection.active is not None: try: - img = self._viewer.layers.selection.active.metadata["bioimage"] - scene = re.sub(r"[^\w\s]", "-", img.current_scene) - self._save_name.value = f"{self._save_name.value}_{scene}" + img = self._viewer.layers.selection.active.metadata['bioimage'] + scene = re.sub(r'[^\w\s]', '-', img.current_scene) + self._save_name.value = f'{self._save_name.value}_{scene}' except AttributeError: self._results.value = ( - "Tried to append scene to name, but layer not opened with" - " ndevio reader." + 'Tried to append scene to name, but layer not opened with' + ' ndevio reader.' ) else: self._results.value = ( - "Tried to append scene to name, but no layer selected." - " So the first scene from the first file will be appended." + 'Tried to append scene to name, but no layer selected.' + ' So the first scene from the first file will be appended.' ) from ndevio import nImage img = nImage(self._files.value[0]) - scene = re.sub(r"[^\w\s]", "-", img.current_scene) - self._save_name.value = f"{self._save_name.value}_{scene}" + scene = re.sub(r'[^\w\s]', '-', img.current_scene) + self._save_name.value = f'{self._save_name.value}_{scene}' def update_metadata_from_layer(self): """Update metadata from the selected layer.""" selected_layer = self._viewer.layers.selection.active try: - img = selected_layer.metadata["bioimage"] + img = selected_layer.metadata['bioimage'] self._update_metadata_from_Image(img) except AttributeError: self._results.value = ( - "Tried to update metadata, but no layer selected." + 'Tried to update metadata, but no layer selected.' f'\nAt {time.strftime("%H:%M:%S")}' ) except KeyError: @@ -703,14 +709,14 @@ def update_metadata_from_layer(self): scale[-1], ) self._results.value = ( - "Tried to update metadata, but could only update scale" - " because layer not opened with ndevio reader." + 'Tried to update metadata, but could only update scale' + ' because layer not opened with ndevio reader.' f'\nAt {time.strftime("%H:%M:%S")}' ) def open_images(self): """Open the selected images in the napari viewer with ndevio.""" - self._viewer.open(self._files.value, plugin="ndevio") + self._viewer.open(self._files.value, plugin='ndevio') def select_next_images(self): """Open the next set of images in the directory.""" @@ -721,21 +727,23 @@ def select_next_images(self): first_file = self._files.value[0] parent_dir = first_file.parent - files = list(parent_dir.glob(f"*{first_file.suffix}")) + files = list(parent_dir.glob(f'*{first_file.suffix}')) files = os_sorted(files) idx = files.index(first_file) next_files = files[idx + num_files : idx + num_files + num_files] if not next_files: - self._results.value = "No more file sets to select." + self._results.value = 'No more file sets to select.' return from ndevio import nImage img = nImage(next_files[0]) - self._save_name.value = helpers.create_id_string(img, next_files[0].stem) + self._save_name.value = helpers.create_id_string( + img, next_files[0].stem + ) self._files.value = next_files self.update_metadata_on_file_select() @@ -788,37 +796,41 @@ def _get_dims_for_shape_layer(self) -> tuple[int, ...]: None, ) if dim_layer is None: - raise ValueError("No image or labels present to convert shapes layer.") + raise ValueError( + 'No image or labels present to convert shapes layer.' + ) label_dim = dim_layer.data.shape label_dim = label_dim[:-1] if label_dim[-1] == 3 else label_dim return label_dim - def _get_save_loc(self, root_dir: Path, parent: str, file_name: str) -> Path: + def _get_save_loc( + self, root_dir: Path, parent: str, file_name: str + ) -> Path: """Get the save location based on the parent directory.""" save_directory = root_dir / parent save_directory.mkdir(parents=False, exist_ok=True) return save_directory / file_name def _determine_save_directory(self, save_dir: str | None = None) -> str: - if self._save_directory_prefix.value != "": - save_dir = f"{self._save_directory_prefix.value}_{save_dir}" + if self._save_directory_prefix.value != '': + save_dir = f'{self._save_directory_prefix.value}_{save_dir}' else: - save_dir = f"{save_dir}" + save_dir = f'{save_dir}' return save_dir def save_files_as_ome_tiff(self) -> None: """Save the selected files as OME-TIFF with threading.""" from napari.qt import create_worker - save_dir = self._determine_save_directory("ConcatenatedImages") + save_dir = self._determine_save_directory('ConcatenatedImages') save_directory = self._save_directory.value / save_dir save_name = self._save_name.value cnames = self._channel_names.value channel_names = ast.literal_eval(cnames) if cnames else None - self._progress_bar.label = "Concatenating files..." + self._progress_bar.label = 'Concatenating files...' self._progress_bar.value = 0 self._progress_bar.max = 0 @@ -835,21 +847,21 @@ def save_files_as_ome_tiff(self) -> None: def _on_concat_complete(self, save_path: Path) -> None: """Handle completion of file concatenation.""" - self._progress_bar.label = "" + self._progress_bar.label = '' self._progress_bar.max = 1 self._progress_bar.value = 1 self._results.value = ( - f"Saved Concatenated Image: {save_path.name}" + f'Saved Concatenated Image: {save_path.name}' f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_concat_error(self, exception: Exception) -> None: """Handle error during file concatenation.""" - self._progress_bar.label = "Error" + self._progress_bar.label = 'Error' self._progress_bar.max = 1 self._progress_bar.value = 0 self._results.value = ( - f"Error concatenating files: {exception}" + f'Error concatenating files: {exception}' f'\nAt {time.strftime("%H:%M:%S")}' ) @@ -864,7 +876,7 @@ def _build_file_sets(self) -> list[tuple[list[Path], str]]: suffix = self._files.value[0].suffix num_files = len(self._files.value) - all_files = os_sorted(list(parent_dir.glob(f"*{suffix}"))) + all_files = os_sorted(list(parent_dir.glob(f'*{suffix}'))) from ndevio import nImage @@ -891,10 +903,10 @@ def batch_concatenate_files(self) -> None: cnames = self._channel_names.value channel_names = ast.literal_eval(cnames) if cnames else None - save_dir = self._determine_save_directory("ConcatenatedImages") + save_dir = self._determine_save_directory('ConcatenatedImages') save_directory = self._save_directory.value / save_dir - self._progress_bar.label = "Starting batch..." + self._progress_bar.label = 'Starting batch...' self._set_batch_button_state(running=True) self._batch_runner.run( @@ -903,12 +915,12 @@ def batch_concatenate_files(self) -> None: save_directory=save_directory, channel_names=channel_names, p_sizes=self.p_sizes, - log_file=save_directory / "batch_concatenate.log.txt", + log_file=save_directory / 'batch_concatenate.log.txt', log_header={ - "Source Directory": str(self._files.value[0].parent), - "Save Directory": str(save_directory), - "Files per Set": len(self._files.value), - "Total Sets": len(file_sets), + 'Source Directory': str(self._files.value[0].parent), + 'Save Directory': str(save_directory), + 'Files per Set': len(self._files.value), + 'Total Sets': len(file_sets), }, threaded=True, ) @@ -925,15 +937,15 @@ def save_scenes_ome_tiff(self) -> None: scenes = self._scenes_to_extract.value scenes_list = ast.literal_eval(scenes) if scenes else list(img.scenes) - save_dir = self._determine_save_directory("ExtractedScenes") + save_dir = self._determine_save_directory('ExtractedScenes') save_directory = self._save_directory.value / save_dir - base_save_name = self._save_name.value.split(".")[0] + base_save_name = self._save_name.value.split('.')[0] cnames = self._channel_names.value channel_names = ast.literal_eval(cnames) if cnames else None - self._progress_bar.label = "Extracting Scenes" + self._progress_bar.label = 'Extracting Scenes' self._progress_bar.value = 0 self._progress_bar.max = len(scenes_list) @@ -958,20 +970,18 @@ def _on_scene_extracted(self, result: tuple[int, str]) -> None: scene_idx, scene_name = result self._progress_bar.value = self._progress_bar.value + 1 self._results.value = ( - f"Extracted scene {scene_idx}: {scene_name}" + f'Extracted scene {scene_idx}: {scene_name}' f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_scenes_complete(self, scenes_list: list, _=None) -> None: """Handle completion of all scene extractions.""" - self._progress_bar.label = "" - self._results.value = ( - f"Saved extracted scenes: {scenes_list}\nAt {time.strftime('%H:%M:%S')}" - ) + self._progress_bar.label = '' + self._results.value = f'Saved extracted scenes: {scenes_list}\nAt {time.strftime("%H:%M:%S")}' def _on_scene_error(self, exc: Exception) -> None: """Handle error during scene extraction.""" - self._progress_bar.label = "Error" + self._progress_bar.label = 'Error' self._progress_bar.max = 1 self._progress_bar.value = 0 self._results.value = ( @@ -982,16 +992,16 @@ def canvas_export_figure(self) -> None: """Export the current canvas figure to the save directory.""" if self._viewer.dims.ndisplay != 2: self._results.value = ( - "Exporting Figure only works in 2D mode." - "\nUse Screenshot for 3D figures." + 'Exporting Figure only works in 2D mode.' + '\nUse Screenshot for 3D figures.' f'\nAt {time.strftime("%H:%M:%S")}' ) return - save_name = f"{self._save_name.value}_figure.png" + save_name = f'{self._save_name.value}_figure.png' save_path = self._get_save_loc( self._save_directory.value, - "Figures", + 'Figures', save_name, ) @@ -1003,17 +1013,19 @@ def canvas_export_figure(self) -> None: ) self._results.value = ( - f"Exported canvas figure to Figures directory." - f"\nSaved as {save_name}" - f"\nWith scale factor of {scale}" + f'Exported canvas figure to Figures directory.' + f'\nSaved as {save_name}' + f'\nWith scale factor of {scale}' f'\nAt {time.strftime("%H:%M:%S")}' ) return def canvas_screenshot(self) -> None: """Export the current canvas screenshot to the save directory.""" - save_name = f"{self._save_name.value}_canvas.png" - save_path = self._get_save_loc(self._save_directory.value, "Figures", save_name) + save_name = f'{self._save_name.value}_canvas.png' + save_path = self._get_save_loc( + self._save_directory.value, 'Figures', save_name + ) scale = self._settings.ndevio_export.canvas_scale if self._settings.ndevio_export.override_canvas_size: @@ -1029,10 +1041,10 @@ def canvas_screenshot(self) -> None: ) self._results.value = ( - f"Exported screenshot of canvas to Figures directory." - f"\nSaved as {save_name}" - f"\nWith canvas dimensions of {canvas_size}" - f"\nWith scale factor of {scale}" + f'Exported screenshot of canvas to Figures directory.' + f'\nSaved as {save_name}' + f'\nWith canvas dimensions of {canvas_size}' + f'\nWith scale factor of {scale}' f'\nAt {time.strftime("%H:%M:%S")}' ) return @@ -1041,36 +1053,44 @@ def save_layers_as_ome_tiff(self) -> None: """Save the selected layers as OME-TIFF.""" from napari.qt import create_worker - layer_data = self.concatenate_layers(list(self._viewer.layers.selection)) - layer_types = [type(layer).__name__ for layer in self._viewer.layers.selection] + layer_data = self.concatenate_layers( + list(self._viewer.layers.selection) + ) + layer_types = [ + type(layer).__name__ for layer in self._viewer.layers.selection + ] - layer_save_type = "Layers" if len(set(layer_types)) > 1 else layer_types[0] + layer_save_type = ( + 'Layers' if len(set(layer_types)) > 1 else layer_types[0] + ) layer_save_dir = self._determine_save_directory(layer_save_type) - layer_save_name = f"{self._save_name.value}.tiff" + layer_save_name = f'{self._save_name.value}.tiff' layer_save_loc = self._get_save_loc( self._save_directory.value, layer_save_dir, layer_save_name ) - if layer_save_type not in ["Shapes", "Labels"]: + if layer_save_type not in ['Shapes', 'Labels']: cnames = self._channel_names.value channel_names = ast.literal_eval(cnames) if cnames else None else: channel_names = [layer_save_type] - if layer_save_type == "Shapes": + if layer_save_type == 'Shapes': layer_data = layer_data.astype(np.int16) - elif layer_save_type == "Labels": + elif layer_save_type == 'Labels': if layer_data.max() > 65535: layer_data = layer_data.astype(np.int32) else: layer_data = layer_data.astype(np.int16) if self._squeezed_dims_order: - dim_order = "C" + self._squeezed_dims_order + dim_order = 'C' + self._squeezed_dims_order else: num_dims = len(layer_data.shape) - dim_order = "C" + "".join([str(d) for d in "TZYX"[-(num_dims - 1) :]]) + dim_order = 'C' + ''.join( + [str(d) for d in 'TZYX'[-(num_dims - 1) :]] + ) self._layer_save_type = layer_save_type @@ -1090,14 +1110,14 @@ def save_layers_as_ome_tiff(self) -> None: def _on_layer_save_complete(self, result: None = None) -> None: """Handle successful layer save completion.""" self._results.value = ( - f"Saved {self._layer_save_type}: " + f'Saved {self._layer_save_type}: ' + str(self._save_name.value) + f'\nAt {time.strftime("%H:%M:%S")}' ) def _on_layer_save_error(self, exc: Exception) -> None: """Handle layer save error.""" - self._progress_bar.label = "Error" + self._progress_bar.label = 'Error' self._progress_bar.max = 1 self._progress_bar.value = 0 self._results.value = ( diff --git a/tests/conftest.py b/tests/conftest.py index 3ab50d6..733661c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,4 +8,4 @@ @pytest.fixture def resources_dir() -> Path: """Return path to test resources directory.""" - return Path(__file__).parent / "resources" + return Path(__file__).parent / 'resources' diff --git a/tests/test_bioio_plugin_utils.py b/tests/test_bioio_plugin_utils.py index c42f198..61bb072 100644 --- a/tests/test_bioio_plugin_utils.py +++ b/tests/test_bioio_plugin_utils.py @@ -8,36 +8,36 @@ def test_czi_file(self): """Test that CZI file suggests bioio-czi.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path("test.czi") + plugins = suggest_plugins_for_path('test.czi') assert len(plugins) == 1 - assert plugins[0] == "bioio-czi" + assert plugins[0] == 'bioio-czi' def test_lif_file(self): """Test that LIF file suggests bioio-lif.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path("test.lif") + plugins = suggest_plugins_for_path('test.lif') assert len(plugins) == 1 - assert plugins[0] == "bioio-lif" + assert plugins[0] == 'bioio-lif' def test_tiff_file_suggests_all(self): """Test that TIFF files suggest all TIFF-compatible plugins.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path("test.tiff") + plugins = suggest_plugins_for_path('test.tiff') # Should get bioio-ome-tiff, bioio-tifffile, bioio-tiff-glob - assert "bioio-ome-tiff" in plugins - assert "bioio-tifffile" in plugins - assert "bioio-tiff-glob" in plugins + assert 'bioio-ome-tiff' in plugins + assert 'bioio-tifffile' in plugins + assert 'bioio-tiff-glob' in plugins def test_unsupported_extension(self): """Test that unsupported extensions return empty list.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path("test.xyz") + plugins = suggest_plugins_for_path('test.xyz') assert len(plugins) == 0 @@ -52,19 +52,19 @@ def test_manager_filters_installed_plugins(self): from ndevio._plugin_manager import ReaderPluginManager # Mock feasibility report showing bioio-czi as installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-czi": Mock(supported=False), - "ArrayLike": Mock(supported=False), + 'bioio-czi': Mock(supported=False), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.czi") + manager = ReaderPluginManager('test.czi') # bioio-czi should be in installed_plugins - assert "bioio-czi" in manager.installed_plugins + assert 'bioio-czi' in manager.installed_plugins # bioio-czi should NOT be in installable_plugins (already installed) - assert "bioio-czi" not in manager.installable_plugins + assert 'bioio-czi' not in manager.installable_plugins def test_manager_suggests_uninstalled_plugins(self): """Test that manager suggests uninstalled plugins.""" @@ -73,19 +73,19 @@ def test_manager_suggests_uninstalled_plugins(self): from ndevio._plugin_manager import ReaderPluginManager # Mock feasibility report with no bioio-lif installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-ome-tiff": Mock(supported=False), - "ArrayLike": Mock(supported=False), + 'bioio-ome-tiff': Mock(supported=False), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.lif") + manager = ReaderPluginManager('test.lif') # bioio-lif should be in suggested_plugins - assert "bioio-lif" in manager.suggested_plugins + assert 'bioio-lif' in manager.suggested_plugins # bioio-lif should also be in installable_plugins - assert "bioio-lif" in manager.installable_plugins + assert 'bioio-lif' in manager.installable_plugins def test_manager_excludes_core_plugins_from_installable(self): """Test that core plugins are excluded from installable list.""" @@ -94,28 +94,28 @@ def test_manager_excludes_core_plugins_from_installable(self): from ndevio._plugin_manager import ReaderPluginManager # Mock report showing no plugins installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "ArrayLike": Mock(supported=False), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.tiff") + manager = ReaderPluginManager('test.tiff') # Core plugins should not be in installable installable_plugins = manager.installable_plugins # These are core plugins, shouldn't need installation core_plugins = [ - "bioio-ome-tiff", - "bioio-imageio", - "bioio-ome-zarr", - "bioio-tifffile", + 'bioio-ome-tiff', + 'bioio-imageio', + 'bioio-ome-zarr', + 'bioio-tifffile', ] for core in core_plugins: assert core not in installable_plugins # bioio-tiff-glob is not core, should be installable - assert "bioio-tiff-glob" in installable_plugins + assert 'bioio-tiff-glob' in installable_plugins class TestFormatPluginInstallationMessage: @@ -128,16 +128,16 @@ def test_czi_message_basic(self): suggest_plugins_for_path, ) - suggested = suggest_plugins_for_path("test.czi") + suggested = suggest_plugins_for_path('test.czi') message = format_plugin_installation_message( - filename="test.czi", + filename='test.czi', suggested_plugins=suggested, installed_plugins=set(), installable_plugins=suggested, ) - assert "bioio-czi" in message - assert "pip install" in message or "conda install" in message + assert 'bioio-czi' in message + assert 'pip install' in message or 'conda install' in message def test_unsupported_extension_message(self): """Test message for completely unsupported extension.""" @@ -146,10 +146,10 @@ def test_unsupported_extension_message(self): ) message = format_plugin_installation_message( - filename="test.xyz", + filename='test.xyz', suggested_plugins=[], installed_plugins=set(), installable_plugins=[], ) - assert "No bioio plugins found" in message or ".xyz" in message + assert 'No bioio plugins found' in message or '.xyz' in message diff --git a/tests/test_helpers.py b/tests/test_helpers.py index cab0685..3347e8d 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -20,36 +20,38 @@ class TestCheckForMissingFiles: def test_no_missing_files_path(self, tmp_path): """Test when all files exist using Path objects.""" - directory = tmp_path / "test_dir" + directory = tmp_path / 'test_dir' directory.mkdir() - file1 = directory / "file1.txt" - file1.write_text("Test file 1") - file2 = directory / "file2.txt" - file2.write_text("Test file 2") + file1 = directory / 'file1.txt' + file1.write_text('Test file 1') + file2 = directory / 'file2.txt' + file2.write_text('Test file 2') missing = check_for_missing_files([file1, file2], directory) assert missing == [] def test_missing_file_path(self, tmp_path): """Test detecting missing files using Path objects.""" - directory = tmp_path / "test_dir" + directory = tmp_path / 'test_dir' directory.mkdir() - file1 = directory / "file1.txt" - file1.write_text("Test file 1") - file3 = directory / "file3.txt" # Does not exist + file1 = directory / 'file1.txt' + file1.write_text('Test file 1') + file3 = directory / 'file3.txt' # Does not exist missing = check_for_missing_files([file1, file3], directory) - assert missing == [("file3.txt", "test_dir")] + assert missing == [('file3.txt', 'test_dir')] def test_missing_file_str(self, tmp_path): """Test with string file names.""" - directory = tmp_path / "test_dir" + directory = tmp_path / 'test_dir' directory.mkdir() - file1 = directory / "file1.txt" - file1.write_text("Test file 1") + file1 = directory / 'file1.txt' + file1.write_text('Test file 1') - missing = check_for_missing_files(["file1.txt", "file3.txt"], directory) - assert missing == [("file3.txt", "test_dir")] + missing = check_for_missing_files( + ['file1.txt', 'file3.txt'], directory + ) + assert missing == [('file3.txt', 'test_dir')] class TestCreateIdString: @@ -58,28 +60,28 @@ class TestCreateIdString: def test_basic_id_string(self): """Test basic ID string creation from numpy array.""" img = nImage(np.random.random((2, 2))) - id_string = create_id_string(img, "test_id") - assert id_string == "test_id__0__Image:0" + id_string = create_id_string(img, 'test_id') + assert id_string == 'test_id__0__Image:0' def test_none_identifier(self): """Test with None identifier.""" img = nImage(np.random.random((2, 2))) id_string = create_id_string(img, None) - assert id_string == "None__0__Image:0" + assert id_string == 'None__0__Image:0' def test_with_ome_metadata_name(self, tmp_path): """Test that OmeTiffWriter image_name is used in ID string.""" OmeTiffWriter.save( data=np.random.random((2, 2)), - uri=tmp_path / "test.tiff", - image_name="test_image", + uri=tmp_path / 'test.tiff', + image_name='test_image', ) - img = nImage(tmp_path / "test.tiff") - id_string = create_id_string(img, "test_id") + img = nImage(tmp_path / 'test.tiff') + id_string = create_id_string(img, 'test_id') - assert img.current_scene == "Image:0" - assert id_string == "test_id__0__test_image" + assert img.current_scene == 'Image:0' + assert id_string == 'test_id__0__test_image' class TestGetChannelNames: @@ -88,7 +90,7 @@ class TestGetChannelNames: def test_multichannel_image(self, resources_dir): """Test getting channel names from multichannel image.""" # Use the legacy tiff which has channel names - file = resources_dir / "cells3d2ch_legacy.tiff" + file = resources_dir / 'cells3d2ch_legacy.tiff' if file.exists(): img = nImage(file) names = get_channel_names(img) @@ -97,12 +99,12 @@ def test_multichannel_image(self, resources_dir): def test_rgb_image(self, resources_dir): """Test that RGB images return red, green, blue.""" - file = resources_dir / "RGB_bad_metadata.tiff" + file = resources_dir / 'RGB_bad_metadata.tiff' if file.exists(): img = nImage(file) - if "S" in img.dims.order: + if 'S' in img.dims.order: names = get_channel_names(img) - assert names == ["red", "green", "blue"] + assert names == ['red', 'green', 'blue'] class TestGetDirectoryAndFiles: @@ -111,28 +113,28 @@ class TestGetDirectoryAndFiles: def test_default_pattern(self, tmp_path): """Test with default pattern finding image files.""" # Create test files - (tmp_path / "image1.tif").write_bytes(b"fake") - (tmp_path / "image2.tiff").write_bytes(b"fake") - (tmp_path / "data.csv").write_text("a,b,c") + (tmp_path / 'image1.tif').write_bytes(b'fake') + (tmp_path / 'image2.tiff').write_bytes(b'fake') + (tmp_path / 'data.csv').write_text('a,b,c') directory, files = get_directory_and_files(tmp_path) assert directory == tmp_path # Should find tif/tiff but not csv file_names = [f.name for f in files] - assert "image1.tif" in file_names - assert "image2.tiff" in file_names - assert "data.csv" not in file_names + assert 'image1.tif' in file_names + assert 'image2.tiff' in file_names + assert 'data.csv' not in file_names def test_custom_pattern(self, tmp_path): """Test with custom file pattern.""" - (tmp_path / "data1.csv").write_text("a,b") - (tmp_path / "data2.csv").write_text("c,d") - (tmp_path / "image.tif").write_bytes(b"fake") + (tmp_path / 'data1.csv').write_text('a,b') + (tmp_path / 'data2.csv').write_text('c,d') + (tmp_path / 'image.tif').write_bytes(b'fake') - directory, files = get_directory_and_files(tmp_path, pattern="csv") + directory, files = get_directory_and_files(tmp_path, pattern='csv') assert directory == tmp_path assert len(files) == 2 - assert all(f.suffix == ".csv" for f in files) + assert all(f.suffix == '.csv' for f in files) def test_none_directory(self): """Test with None directory returns empty results.""" @@ -142,7 +144,7 @@ def test_none_directory(self): def test_nonexistent_directory(self, tmp_path): """Test that nonexistent directory raises FileNotFoundError.""" - nonexistent = tmp_path / "does_not_exist" + nonexistent = tmp_path / 'does_not_exist' with pytest.raises(FileNotFoundError): get_directory_and_files(nonexistent) @@ -157,19 +159,19 @@ def test_3d_image(self): img = nImage(data) dims = get_squeezed_dim_order(img) # Should return ZYX (C is skipped by default, T=1 is squeezed) - assert "Z" in dims - assert "Y" in dims - assert "X" in dims - assert "C" not in dims - assert "T" not in dims + assert 'Z' in dims + assert 'Y' in dims + assert 'X' in dims + assert 'C' not in dims + assert 'T' not in dims def test_2d_image(self): """Test squeezed dims for 2D image.""" data = np.random.random((10, 10)) img = nImage(data) dims = get_squeezed_dim_order(img) - assert "Y" in dims - assert "X" in dims + assert 'Y' in dims + assert 'X' in dims class TestElideString: @@ -177,30 +179,32 @@ class TestElideString: def test_short_string_unchanged(self): """Test that short strings are not modified.""" - assert elide_string("short", 10) == "short" - assert elide_string("short", 6) == "short" + assert elide_string('short', 10) == 'short' + assert elide_string('short', 6) == 'short' def test_exact_length_unchanged(self): """Test that strings at max length are not modified.""" - assert elide_string("exactly15chars", 15) == "exactly15chars" + assert elide_string('exactly15chars', 15) == 'exactly15chars' def test_middle_elision(self): """Test middle elision (default).""" - assert elide_string("thisisaverylongstring", 10) == "thi...ing" - assert elide_string("thisisaverylongstring", 15) == "thisis...string" + assert elide_string('thisisaverylongstring', 10) == 'thi...ing' + assert elide_string('thisisaverylongstring', 15) == 'thisis...string' def test_start_elision(self): """Test start elision.""" - assert elide_string("thisisaverylongstring", 10, "start") == "...gstring" + assert ( + elide_string('thisisaverylongstring', 10, 'start') == '...gstring' + ) def test_end_elision(self): """Test end elision.""" - assert elide_string("thisisaverylongstring", 10, "end") == "thisisa..." + assert elide_string('thisisaverylongstring', 10, 'end') == 'thisisa...' def test_very_small_max_length(self): """Test with max_length <= 5 truncates without ellipsis.""" - assert elide_string("thisisaverylongstring", 3) == "thi" - assert elide_string("thisisaverylongstring", 5) == "thisi" + assert elide_string('thisisaverylongstring', 3) == 'thi' + assert elide_string('thisisaverylongstring', 5) == 'thisi' def test_invalid_location(self): """Test that invalid location raises ValueError.""" @@ -208,10 +212,10 @@ def test_invalid_location(self): ValueError, match='Invalid location. Must be "start", "middle", or "end".', ): - elide_string("thisisaverylongstring", 10, "invalid") + elide_string('thisisaverylongstring', 10, 'invalid') def test_edge_cases(self): """Test edge cases.""" - assert elide_string("", 10) == "" - assert elide_string("a", 1) == "a" - assert elide_string("ab", 1) == "a" + assert elide_string('', 10) == '' + assert elide_string('a', 1) == 'a' + assert elide_string('ab', 1) == 'a' diff --git a/tests/test_napari_reader.py b/tests/test_napari_reader.py index 9a400ec..f2611e3 100644 --- a/tests/test_napari_reader.py +++ b/tests/test_napari_reader.py @@ -13,11 +13,11 @@ ############################################################################### -RGB_TIFF = "RGB_bad_metadata.tiff" # has two scenes -MULTISCENE_CZI = r"0T-4C-0Z-7pos.czi" -PNG_FILE = "nDev-logo-small.png" -ND2_FILE = "ND2_dims_rgb.nd2" -OME_TIFF = "cells3d2ch_legacy.tiff" +RGB_TIFF = 'RGB_bad_metadata.tiff' # has two scenes +MULTISCENE_CZI = r'0T-4C-0Z-7pos.czi' +PNG_FILE = 'nDev-logo-small.png' +ND2_FILE = 'ND2_dims_rgb.nd2' +OME_TIFF = 'cells3d2ch_legacy.tiff' ############################################################################### @@ -32,7 +32,7 @@ def test_napari_viewer_open(resources_dir: Path, make_napari_viewer) -> None: is shimmed to DirectoryStore with a compatibility patch in nImage. """ viewer = make_napari_viewer() - viewer.open(str(resources_dir / OME_TIFF), plugin="ndevio") + viewer.open(str(resources_dir / OME_TIFF), plugin='ndevio') # Now channels are split into separate layers, so we should have 2 layers assert len(viewer.layers) == 2 @@ -41,7 +41,7 @@ def test_napari_viewer_open(resources_dir: Path, make_napari_viewer) -> None: @pytest.mark.parametrize( - ("in_memory", "expected_dtype"), + ('in_memory', 'expected_dtype'), [ (True, np.ndarray), (False, da.core.Array), @@ -49,10 +49,10 @@ def test_napari_viewer_open(resources_dir: Path, make_napari_viewer) -> None: ) @pytest.mark.parametrize( ( - "filename", - "expected_shape", - "expected_has_scale", - "expected_num_layers", + 'filename', + 'expected_shape', + 'expected_has_scale', + 'expected_num_layers', ), [ # PNG shape is (106, 243, 4) - actual dimensions of nDev-logo-small.png @@ -101,20 +101,20 @@ def test_reader_supported_formats( assert data.shape == expected_shape # Check meta has expected keys - assert "name" in meta + assert 'name' in meta if expected_has_scale: - assert "scale" in meta + assert 'scale' in meta @pytest.mark.parametrize( - ("in_memory", "expected_dtype"), + ('in_memory', 'expected_dtype'), [ (True, np.ndarray), (False, da.core.Array), ], ) @pytest.mark.parametrize( - ("filename", "expected_shape", "should_work"), + ('filename', 'expected_shape', 'should_work'), [ # RGB_TIFF should work now that bioio-tifffile is a core dependency (RGB_TIFF, (1440, 1920, 3), True), @@ -158,7 +158,7 @@ def test_for_multiscene_widget( if len(viewer.window._dock_widgets) != 0: # Get the second scene scene_widget = ( - viewer.window._dock_widgets[f"{Path(filename).stem} :: Scenes"] + viewer.window._dock_widgets[f'{Path(filename).stem} :: Scenes'] .widget() ._magic_widget ) @@ -202,9 +202,9 @@ def test_napari_get_reader_ome_override(resources_dir: Path) -> None: def test_napari_get_reader_unsupported(resources_dir: Path): """Test that unsupported file extension returns None per napari reader spec.""" # Mock the widget opener since we don't have a viewer in this test - with patch("ndevio._napari_reader._open_plugin_installer") as mock_opener: + with patch('ndevio._napari_reader._open_plugin_installer') as mock_opener: reader = napari_get_reader( - str(resources_dir / "measure_props_Labels.abcdefg"), + str(resources_dir / 'measure_props_Labels.abcdefg'), ) # Should return None for unsupported formats (per napari spec) @@ -215,22 +215,22 @@ def test_napari_get_reader_unsupported(resources_dir: Path): # Check the error message contains the extension error_arg = mock_opener.call_args[0][1] error_msg = str(error_arg) - assert ".abcdefg" in error_msg or "abcdefg" in error_msg + assert '.abcdefg' in error_msg or 'abcdefg' in error_msg def test_napari_get_reader_general_exception(caplog): """Test that general exceptions in determine_reader_plugin are handled correctly.""" - test_path = "non_existent_file.xyz" + test_path = 'non_existent_file.xyz' # Mock determine_reader_plugin to raise an exception - with patch("ndevio._napari_reader.determine_reader_plugin") as mock_reader: - mock_reader.side_effect = Exception("Test exception") + with patch('ndevio._napari_reader.determine_reader_plugin') as mock_reader: + mock_reader.side_effect = Exception('Test exception') reader = napari_get_reader(test_path) assert reader is None - assert "ndevio: Error reading file" in caplog.text - assert "Test exception" in caplog.text + assert 'ndevio: Error reading file' in caplog.text + assert 'Test exception' in caplog.text def test_napari_get_reader_png(resources_dir: Path) -> None: @@ -262,9 +262,9 @@ def test_napari_get_reader_supported_formats_work(resources_dir: Path): @pytest.mark.parametrize( - ("filename", "expected_plugin_in_error"), + ('filename', 'expected_plugin_in_error'), [ - (ND2_FILE, "bioio-nd2"), # ND2 needs bioio-nd2 + (ND2_FILE, 'bioio-nd2'), # ND2 needs bioio-nd2 ], ) def test_napari_get_reader_unsupported_formats_helpful_errors( @@ -276,7 +276,7 @@ def test_napari_get_reader_unsupported_formats_helpful_errors( The plugin installer widget should be shown via settings if enabled. """ # Mock the widget opener since we don't have a viewer in this test - with patch("ndevio._napari_reader._open_plugin_installer") as mock_opener: + with patch('ndevio._napari_reader._open_plugin_installer') as mock_opener: reader = napari_get_reader(str(resources_dir / filename)) # Should return None for unsupported formats (per napari spec) @@ -289,4 +289,4 @@ def test_napari_get_reader_unsupported_formats_helpful_errors( error_arg = call_args[0][1] # Second argument is the exception error_msg = str(error_arg) assert expected_plugin_in_error in error_msg - assert "pip install" in error_msg or "conda install" in error_msg + assert 'pip install' in error_msg or 'conda install' in error_msg diff --git a/tests/test_nimage.py b/tests/test_nimage.py index 1050bf1..b4b5a46 100644 --- a/tests/test_nimage.py +++ b/tests/test_nimage.py @@ -16,13 +16,13 @@ from ndevio import nImage from ndevio.nimage import determine_reader_plugin -RGB_TIFF = "RGB_bad_metadata.tiff" # has two scenes, with really difficult metadata -CELLS3D2CH_OME_TIFF = ( - "cells3d2ch_legacy.tiff" # 2 channel, 3D OME-TIFF, from old napari-ndev saving +RGB_TIFF = ( + 'RGB_bad_metadata.tiff' # has two scenes, with really difficult metadata ) -LOGO_PNG = "nDev-logo-small.png" # small PNG file (fix typo) -CZI_FILE = "0T-4C-0Z-7pos.czi" # multi-scene CZI file -ND2_FILE = "ND2_dims_rgb.nd2" # ND2 file requiring bioio-nd2 +CELLS3D2CH_OME_TIFF = 'cells3d2ch_legacy.tiff' # 2 channel, 3D OME-TIFF, from old napari-ndev saving +LOGO_PNG = 'nDev-logo-small.png' # small PNG file (fix typo) +CZI_FILE = '0T-4C-0Z-7pos.czi' # multi-scene CZI file +ND2_FILE = 'ND2_dims_rgb.nd2' # ND2 file requiring bioio-nd2 def test_nImage_init(resources_dir: Path): @@ -52,22 +52,24 @@ def test_nImage_ome_reader(resources_dir: Path): img_path = resources_dir / CELLS3D2CH_OME_TIFF nimg = nImage(img_path) - assert nimg.settings.ndevio_reader.preferred_reader == "bioio-ome-tiff" + assert nimg.settings.ndevio_reader.preferred_reader == 'bioio-ome-tiff' # the below only exists if 'bioio-ome-tiff' is used - assert hasattr(nimg, "ome_metadata") - assert nimg.channel_names == ["membrane", "nuclei"] + assert hasattr(nimg, 'ome_metadata') + assert nimg.channel_names == ['membrane', 'nuclei'] # Additional check that the reader override works when bioio_tifffile is # available. The project does not require bioio_tifffile as a test # dependency, so skip this part when it's missing. if bioio_tifffile is None: # pragma: no cover - optional - pytest.skip("bioio_tifffile not installed; skipping reader-override checks") + pytest.skip( + 'bioio_tifffile not installed; skipping reader-override checks' + ) nimg = nImage(img_path, reader=bioio_tifffile.Reader) # check that despite preferred reader, the reader is still bioio_tifffile # because there is no ome_metadata - assert nimg.settings.ndevio_reader.preferred_reader == "bioio-ome-tiff" + assert nimg.settings.ndevio_reader.preferred_reader == 'bioio-ome-tiff' # check that calling nimg.ome_metadata raises NotImplementedError with pytest.raises(NotImplementedError): _ = nimg.ome_metadata @@ -88,23 +90,23 @@ def test_nImage_save_read(resources_dir: Path, tmp_path: Path): img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) assert img.physical_pixel_sizes.X == 1 - img_data = img.get_image_data("CZYX") + img_data = img.get_image_data('CZYX') OmeTiffWriter.save( img_data, - tmp_path / "test_save_read.tiff", - dim_order="CZYX", + tmp_path / 'test_save_read.tiff', + dim_order='CZYX', physical_pixel_sizes=PhysicalPixelSizes(1, 2, 3), # ZYX - channel_names=["test1", "test2"], + channel_names=['test1', 'test2'], ) - assert (tmp_path / "test_save_read.tiff").exists() + assert (tmp_path / 'test_save_read.tiff').exists() - new_img = nImage(tmp_path / "test_save_read.tiff") + new_img = nImage(tmp_path / 'test_save_read.tiff') # having the below features means it is properly read as OME-TIFF assert new_img.physical_pixel_sizes.Z == 1 assert new_img.physical_pixel_sizes.Y == 2 assert new_img.physical_pixel_sizes.X == 3 - assert new_img.channel_names == ["test1", "test2"] + assert new_img.channel_names == ['test1', 'test2'] def test_determine_in_memory(resources_dir: Path): @@ -117,10 +119,12 @@ def test_nImage_determine_in_memory_large_file(resources_dir: Path): """Test in-memory determination for large files.""" img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) with ( - mock.patch("psutil.virtual_memory", return_value=mock.Mock(available=1e9)), mock.patch( - "bioio_base.io.pathlike_to_fs", - return_value=(mock.Mock(size=lambda x: 5e9), ""), + 'psutil.virtual_memory', return_value=mock.Mock(available=1e9) + ), + mock.patch( + 'bioio_base.io.pathlike_to_fs', + return_value=(mock.Mock(size=lambda x: 5e9), ''), ), ): assert img._determine_in_memory() is False @@ -133,7 +137,7 @@ def test_get_layer_data(resources_dir: Path): # napari_layer_data will be squeezed # Original shape (1, 2, 60, 66, 85) -> (2, 60, 66, 85) assert img.napari_layer_data.shape == (2, 60, 66, 85) - assert img.napari_layer_data.dims == ("C", "Z", "Y", "X") + assert img.napari_layer_data.dims == ('C', 'Z', 'Y', 'X') def test_get_layer_data_not_in_memory(resources_dir: Path): @@ -154,9 +158,9 @@ def test_get_layer_data_tuples_basic(resources_dir: Path): # With 2 channels, should get 2 tuples (one per channel) assert len(layer_tuples) == 2 for _data, meta, layer_type in layer_tuples: - assert "cells3d2ch_legacy" in meta["name"] - assert meta["scale"] is not None - assert layer_type == "image" # default layer type + assert 'cells3d2ch_legacy' in meta['name'] + assert meta['scale'] is not None + assert layer_type == 'image' # default layer type def test_get_layer_data_tuples_ome_validation_error_logged( @@ -175,9 +179,9 @@ def test_get_layer_data_tuples_ome_validation_error_logged( # Mock ome_metadata to raise a ValidationError (which inherits from ValueError) with mock.patch.object( type(img), - "ome_metadata", + 'ome_metadata', new_callable=mock.PropertyMock, - side_effect=ValueError("Invalid acquisition_mode: LatticeLightsheet"), + side_effect=ValueError('Invalid acquisition_mode: LatticeLightsheet'), ): caplog.clear() layer_tuples = img.get_layer_data_tuples() @@ -188,22 +192,22 @@ def test_get_layer_data_tuples_ome_validation_error_logged( # Check that metadata dict exists in each tuple for _, meta, _ in layer_tuples: - assert "name" in meta - assert "metadata" in meta + assert 'name' in meta + assert 'metadata' in meta # ome_metadata should NOT be in the nested metadata dict - assert "ome_metadata" not in meta["metadata"] + assert 'ome_metadata' not in meta['metadata'] # raw_image_metadata should still be available - assert "raw_image_metadata" in meta["metadata"] + assert 'raw_image_metadata' in meta['metadata'] # Warning should be logged assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" - assert "Could not parse OME metadata" in caplog.records[0].message - assert "LatticeLightsheet" in caplog.records[0].message + assert caplog.records[0].levelname == 'WARNING' + assert 'Could not parse OME metadata' in caplog.records[0].message + assert 'LatticeLightsheet' in caplog.records[0].message assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" - assert "Could not parse OME metadata" in caplog.records[0].message - assert "LatticeLightsheet" in caplog.records[0].message + assert caplog.records[0].levelname == 'WARNING' + assert 'Could not parse OME metadata' in caplog.records[0].message + assert 'LatticeLightsheet' in caplog.records[0].message def test_get_layer_data_tuples_ome_not_implemented_silent( @@ -220,9 +224,11 @@ def test_get_layer_data_tuples_ome_not_implemented_silent( # Mock ome_metadata to raise NotImplementedError with mock.patch.object( type(img), - "ome_metadata", + 'ome_metadata', new_callable=mock.PropertyMock, - side_effect=NotImplementedError("Reader does not support OME metadata"), + side_effect=NotImplementedError( + 'Reader does not support OME metadata' + ), ): caplog.clear() layer_tuples = img.get_layer_data_tuples() @@ -232,7 +238,7 @@ def test_get_layer_data_tuples_ome_not_implemented_silent( assert len(layer_tuples) > 0 for _, meta, _ in layer_tuples: - assert "ome_metadata" not in meta["metadata"] + assert 'ome_metadata' not in meta['metadata'] # No warning should be logged for NotImplementedError assert len(caplog.records) == 0 @@ -243,9 +249,11 @@ def test_get_layer_data_mosaic_tile_in_memory(resources_dir: Path): import xarray as xr from bioio_base.dimensions import DimensionNames - with mock.patch.object(nImage, "reader", create=True) as mock_reader: + with mock.patch.object(nImage, 'reader', create=True) as mock_reader: mock_reader.dims.order = [DimensionNames.MosaicTile] - mock_reader.mosaic_xarray_data.squeeze.return_value = xr.DataArray([1, 2, 3]) + mock_reader.mosaic_xarray_data.squeeze.return_value = xr.DataArray( + [1, 2, 3] + ) img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) img._get_layer_data(in_memory=True) assert img.napari_layer_data is not None @@ -259,10 +267,10 @@ def test_get_layer_data_mosaic_tile_not_in_memory( import xarray as xr from bioio_base.dimensions import DimensionNames - with mock.patch.object(nImage, "reader", create=True) as mock_reader: + with mock.patch.object(nImage, 'reader', create=True) as mock_reader: mock_reader.dims.order = [DimensionNames.MosaicTile] - mock_reader.mosaic_xarray_dask_data.squeeze.return_value = xr.DataArray( - [1, 2, 3] + mock_reader.mosaic_xarray_dask_data.squeeze.return_value = ( + xr.DataArray([1, 2, 3]) ) img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) img._get_layer_data(in_memory=False) @@ -271,7 +279,7 @@ def test_get_layer_data_mosaic_tile_not_in_memory( @pytest.mark.parametrize( - ("filename", "should_work", "expected_plugin_suggestion"), + ('filename', 'should_work', 'expected_plugin_suggestion'), [ (LOGO_PNG, True, None), # PNG works with bioio-imageio (core) ( @@ -280,7 +288,7 @@ def test_get_layer_data_mosaic_tile_not_in_memory( None, ), # OME-TIFF works with bioio-ome-tiff (core) (CZI_FILE, True, None), - (ND2_FILE, False, "bioio-nd2"), # ND2 needs bioio-nd2 + (ND2_FILE, False, 'bioio-nd2'), # ND2 needs bioio-nd2 (RGB_TIFF, True, None), ], ) @@ -314,7 +322,7 @@ def test_determine_reader_plugin_behavior( assert filename in error_msg if expected_plugin_suggestion: assert expected_plugin_suggestion in error_msg - assert "pip install" in error_msg + assert 'pip install' in error_msg else: # "maybe" # Can succeed or fail; if fails, check for helpful message try: @@ -324,16 +332,16 @@ def test_determine_reader_plugin_behavior( error_msg = str(e) if expected_plugin_suggestion: assert expected_plugin_suggestion in error_msg - assert "pip install" in error_msg + assert 'pip install' in error_msg @pytest.mark.parametrize( - ("filename", "should_work", "expected_error_contains"), + ('filename', 'should_work', 'expected_error_contains'), [ (LOGO_PNG, True, None), (CELLS3D2CH_OME_TIFF, True, None), (CZI_FILE, True, None), - (ND2_FILE, False, ["bioio-nd2", "pip install"]), + (ND2_FILE, False, ['bioio-nd2', 'pip install']), (RGB_TIFF, True, None), ], ) @@ -370,7 +378,9 @@ def test_nimage_init_with_various_formats( error_msg = str(e) # Should contain at least one of the expected error texts if expected_error_contains: - assert any(text in error_msg for text in expected_error_contains) + assert any( + text in error_msg for text in expected_error_contains + ) # ============================================================================= @@ -394,16 +404,16 @@ def test_multichannel_returns_tuple_per_channel(self, resources_dir: Path): for data, meta, layer_type in layer_tuples: # channel_axis should NOT be in metadata (we split ourselves) - assert "channel_axis" not in meta + assert 'channel_axis' not in meta # name should be a string (not a list) - assert isinstance(meta["name"], str) + assert isinstance(meta['name'], str) # Data shape should NOT include channel dimension assert data.shape == (60, 66, 85) # ZYX only # Default layer type is "image" (channel names don't match label keywords) - assert layer_type == "image" + assert layer_type == 'image' def test_layer_names_include_channel_names(self, resources_dir: Path): """Test that layer names include channel names from the file.""" @@ -411,13 +421,15 @@ def test_layer_names_include_channel_names(self, resources_dir: Path): layer_tuples = img.get_layer_data_tuples() # Extract names from the tuples - names = [meta["name"] for _, meta, _ in layer_tuples] + names = [meta['name'] for _, meta, _ in layer_tuples] # Channel names from the file are "membrane" and "nuclei" - assert "membrane" in names[0] - assert "nuclei" in names[1] + assert 'membrane' in names[0] + assert 'nuclei' in names[1] - def test_single_channel_image_returns_single_tuple(self, resources_dir: Path): + def test_single_channel_image_returns_single_tuple( + self, resources_dir: Path + ): """Test that single channel images return single tuple.""" # PNG is single channel (or RGB treated as single layer) img = nImage(resources_dir / LOGO_PNG) @@ -427,8 +439,8 @@ def test_single_channel_image_returns_single_tuple(self, resources_dir: Path): assert len(layer_tuples) == 1 data, meta, layer_type = layer_tuples[0] - assert "channel_axis" not in meta - assert layer_type == "image" + assert 'channel_axis' not in meta + assert layer_type == 'image' def test_scale_preserved_in_tuples(self, resources_dir: Path): """Test that scale metadata is preserved in each tuple.""" @@ -437,9 +449,9 @@ def test_scale_preserved_in_tuples(self, resources_dir: Path): for _, meta, _ in layer_tuples: # Scale should be preserved in each layer - assert "scale" in meta + assert 'scale' in meta # Original has physical pixel sizes, so scale should have values - assert len(meta["scale"]) > 0 + assert len(meta['scale']) > 0 def test_in_memory_parameter_respected(self, resources_dir: Path): """Test that in_memory parameter is passed through correctly.""" @@ -466,11 +478,11 @@ def test_colormap_cycling_for_images(self, resources_dir: Path): layer_tuples = img.get_layer_data_tuples() # Extract colormaps from the tuples - colormaps = [meta.get("colormap") for _, meta, _ in layer_tuples] + colormaps = [meta.get('colormap') for _, meta, _ in layer_tuples] # 2 channels should use MAGENTA_GREEN - assert colormaps[0] == "magenta" - assert colormaps[1] == "green" + assert colormaps[0] == 'magenta' + assert colormaps[1] == 'green' def test_colormap_single_channel_is_gray(self, resources_dir: Path): """Test that single channel images get gray colormap.""" @@ -482,13 +494,13 @@ def test_colormap_single_channel_is_gray(self, resources_dir: Path): # Mock single channel data (no Channel dimension) mock_data = xr.DataArray( np.zeros((10, 10)), - dims=["Y", "X"], + dims=['Y', 'X'], ) img.napari_layer_data = mock_data layer_tuples = img.get_layer_data_tuples() assert len(layer_tuples) == 1 - assert layer_tuples[0][1]["colormap"] == "gray" + assert layer_tuples[0][1]['colormap'] == 'gray' def test_colormap_three_plus_channels_uses_multi_channel_cycle( self, resources_dir: Path @@ -505,13 +517,13 @@ def test_colormap_three_plus_channels_uses_multi_channel_cycle( # Mock 4 channel data mock_data = xr.DataArray( np.zeros((4, 10, 10)), - dims=[DimensionNames.Channel, "Y", "X"], - coords={DimensionNames.Channel: ["ch0", "ch1", "ch2", "ch3"]}, + dims=[DimensionNames.Channel, 'Y', 'X'], + coords={DimensionNames.Channel: ['ch0', 'ch1', 'ch2', 'ch3']}, ) img.napari_layer_data = mock_data layer_tuples = img.get_layer_data_tuples() - colormaps = [meta.get("colormap") for _, meta, _ in layer_tuples] + colormaps = [meta.get('colormap') for _, meta, _ in layer_tuples] # Should cycle through MULTI_CHANNEL_CYCLE (CMYBGR) assert colormaps[0] == MULTI_CHANNEL_CYCLE[0] # cyan @@ -530,8 +542,8 @@ def test_auto_detect_labels_from_channel_name(self, resources_dir: Path): # Mock napari_layer_data with a channel named "mask" mock_data = xr.DataArray( np.zeros((2, 10, 10)), - dims=[DimensionNames.Channel, "Y", "X"], - coords={DimensionNames.Channel: ["intensity", "mask"]}, + dims=[DimensionNames.Channel, 'Y', 'X'], + coords={DimensionNames.Channel: ['intensity', 'mask']}, ) img.napari_layer_data = mock_data @@ -539,9 +551,9 @@ def test_auto_detect_labels_from_channel_name(self, resources_dir: Path): layer_tuples = img.get_layer_data_tuples() # First channel "intensity" should be image - assert layer_tuples[0][2] == "image" + assert layer_tuples[0][2] == 'image' # Second channel "mask" should be labels (keyword match) - assert layer_tuples[1][2] == "labels" + assert layer_tuples[1][2] == 'labels' def test_channel_types_override_auto_detection(self, resources_dir: Path): """Test that channel_types parameter overrides auto-detection.""" @@ -554,19 +566,19 @@ def test_channel_types_override_auto_detection(self, resources_dir: Path): # Set up mock data mock_data = xr.DataArray( np.zeros((2, 10, 10)), - dims=[DimensionNames.Channel, "Y", "X"], - coords={DimensionNames.Channel: ["intensity", "mask"]}, + dims=[DimensionNames.Channel, 'Y', 'X'], + coords={DimensionNames.Channel: ['intensity', 'mask']}, ) img.napari_layer_data = mock_data # Override: set both channels to labels layer_tuples = img.get_layer_data_tuples( - channel_types={"intensity": "labels", "mask": "labels"} + channel_types={'intensity': 'labels', 'mask': 'labels'} ) # Both should be labels due to override - assert layer_tuples[0][2] == "labels" - assert layer_tuples[1][2] == "labels" + assert layer_tuples[0][2] == 'labels' + assert layer_tuples[1][2] == 'labels' def test_labels_do_not_get_colormap(self, resources_dir: Path): """Test that labels layers don't get colormap metadata.""" @@ -579,29 +591,29 @@ def test_labels_do_not_get_colormap(self, resources_dir: Path): # Mock data with a labels channel mock_data = xr.DataArray( np.zeros((1, 10, 10)), - dims=[DimensionNames.Channel, "Y", "X"], - coords={DimensionNames.Channel: ["segmentation"]}, + dims=[DimensionNames.Channel, 'Y', 'X'], + coords={DimensionNames.Channel: ['segmentation']}, ) img.napari_layer_data = mock_data layer_tuples = img.get_layer_data_tuples() # "segmentation" matches label keyword - assert layer_tuples[0][2] == "labels" + assert layer_tuples[0][2] == 'labels' # Labels should not have colormap - assert "colormap" not in layer_tuples[0][1] + assert 'colormap' not in layer_tuples[0][1] def test_layer_type_override_all_channels(self, resources_dir: Path): """Test that layer_type parameter overrides all channels.""" img = nImage(resources_dir / CELLS3D2CH_OME_TIFF) - layer_tuples = img.get_layer_data_tuples(layer_type="labels") + layer_tuples = img.get_layer_data_tuples(layer_type='labels') # All channels should be labels due to override assert len(layer_tuples) == 2 for _, meta, layer_type in layer_tuples: - assert layer_type == "labels" + assert layer_type == 'labels' # Labels should not have colormap - assert "colormap" not in meta + assert 'colormap' not in meta def test_layer_type_overrides_channel_types(self, resources_dir: Path): """Test that layer_type takes precedence over channel_types.""" @@ -613,20 +625,20 @@ def test_layer_type_overrides_channel_types(self, resources_dir: Path): mock_data = xr.DataArray( np.zeros((2, 10, 10)), - dims=[DimensionNames.Channel, "Y", "X"], - coords={DimensionNames.Channel: ["intensity", "mask"]}, + dims=[DimensionNames.Channel, 'Y', 'X'], + coords={DimensionNames.Channel: ['intensity', 'mask']}, ) img.napari_layer_data = mock_data # Even though channel_types says "intensity" should be image, # layer_type="labels" should override everything layer_tuples = img.get_layer_data_tuples( - layer_type="labels", - channel_types={"intensity": "image", "mask": "image"}, + layer_type='labels', + channel_types={'intensity': 'image', 'mask': 'image'}, ) # Both should be labels due to layer_type override - assert layer_tuples[0][2] == "labels" + assert layer_tuples[0][2] == 'labels' def test_channel_kwargs_override_metadata(self, resources_dir: Path): """Test that channel_kwargs overrides default metadata.""" @@ -634,19 +646,21 @@ def test_channel_kwargs_override_metadata(self, resources_dir: Path): layer_tuples = img.get_layer_data_tuples( channel_kwargs={ img.channel_names[0]: { - "colormap": "blue", - "contrast_limits": (0, 1000), + 'colormap': 'blue', + 'contrast_limits': (0, 1000), }, img.channel_names[1]: { - "opacity": 0.5, + 'opacity': 0.5, }, } ) assert len(layer_tuples) == 2 # First channel should have overridden colormap and contrast_limits - assert layer_tuples[0][1]["colormap"] == "blue" - assert layer_tuples[0][1]["contrast_limits"] == (0, 1000) + assert layer_tuples[0][1]['colormap'] == 'blue' + assert layer_tuples[0][1]['contrast_limits'] == (0, 1000) # Second channel should have opacity override but default colormap - assert layer_tuples[1][1]["opacity"] == 0.5 - assert layer_tuples[1][1]["colormap"] == "green" # default for 2-channel + assert layer_tuples[1][1]['opacity'] == 0.5 + assert ( + layer_tuples[1][1]['colormap'] == 'green' + ) # default for 2-channel diff --git a/tests/test_plugin_installer.py b/tests/test_plugin_installer.py index d4b4c64..6a31ad4 100644 --- a/tests/test_plugin_installer.py +++ b/tests/test_plugin_installer.py @@ -10,52 +10,52 @@ def test_czi_file_no_plugins_installed(self): """Test that CZI file suggests bioio-czi plugin when nothing installed.""" from ndevio._plugin_manager import ReaderPluginManager - with patch("bioio.plugin_feasibility_report") as mock_report: - mock_report.return_value = {"ArrayLike": Mock(supported=False)} + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager("test.czi") + manager = ReaderPluginManager('test.czi') plugins = manager.installable_plugins assert len(plugins) == 1 - assert plugins[0] == "bioio-czi" + assert plugins[0] == 'bioio-czi' def test_lif_file_no_plugins_installed(self): """Test that LIF file suggests bioio-lif plugin.""" from ndevio._plugin_manager import ReaderPluginManager - with patch("bioio.plugin_feasibility_report") as mock_report: - mock_report.return_value = {"ArrayLike": Mock(supported=False)} + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager("test.lif") + manager = ReaderPluginManager('test.lif') plugins = manager.installable_plugins assert len(plugins) == 1 - assert plugins[0] == "bioio-lif" + assert plugins[0] == 'bioio-lif' def test_tiff_file_suggests_non_core_only(self): """Test that TIFF files only suggest non-core plugins.""" from ndevio._plugin_manager import ReaderPluginManager - with patch("bioio.plugin_feasibility_report") as mock_report: - mock_report.return_value = {"ArrayLike": Mock(supported=False)} + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager("test.tiff") + manager = ReaderPluginManager('test.tiff') plugins = manager.installable_plugins # Should only get bioio-tiff-glob (non-core) # bioio-ome-tiff and bioio-tifffile are core and shouldn't be suggested - assert "bioio-tiff-glob" in plugins - assert "bioio-ome-tiff" not in plugins - assert "bioio-tifffile" not in plugins + assert 'bioio-tiff-glob' in plugins + assert 'bioio-ome-tiff' not in plugins + assert 'bioio-tifffile' not in plugins def test_no_plugins_for_unsupported_extension(self): """Test that unsupported extensions return empty list.""" from ndevio._plugin_manager import ReaderPluginManager - with patch("bioio.plugin_feasibility_report") as mock_report: - mock_report.return_value = {"ArrayLike": Mock(supported=False)} + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager("test.xyz") + manager = ReaderPluginManager('test.xyz') plugins = manager.installable_plugins assert len(plugins) == 0 @@ -65,13 +65,13 @@ def test_filters_installed_plugins(self): from ndevio._plugin_manager import ReaderPluginManager # Mock feasibility report showing bioio-czi as installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-czi": Mock(supported=True), - "ArrayLike": Mock(supported=False), + 'bioio-czi': Mock(supported=True), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.czi") + manager = ReaderPluginManager('test.czi') plugins = manager.installable_plugins # bioio-czi should be filtered out since it's "installed" @@ -94,7 +94,7 @@ def test_standalone_mode(self, make_napari_viewer): assert widget.manager.path is None # Title should be standalone message - assert "Install BioIO Reader Plugin" in widget._title_label.value + assert 'Install BioIO Reader Plugin' in widget._title_label.value # No path, so no pre-selection assert ( @@ -107,11 +107,11 @@ def test_error_mode_with_installable_plugins(self, make_napari_viewer): from ndevio._plugin_manager import ReaderPluginManager from ndevio.widgets import PluginInstallerWidget - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: # Mock report showing no plugins installed - mock_report.return_value = {"ArrayLike": Mock(supported=False)} + mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager("test.czi") + manager = ReaderPluginManager('test.czi') widget = PluginInstallerWidget(plugin_manager=manager) # Should have ALL plugins available @@ -120,31 +120,31 @@ def test_error_mode_with_installable_plugins(self, make_napari_viewer): # Should have installable plugins installable = widget.manager.installable_plugins assert len(installable) > 0 - assert "bioio-czi" in installable + assert 'bioio-czi' in installable # First installable plugin should be pre-selected assert widget._plugin_select.value == installable[0] # Should have path assert widget.manager.path is not None - assert widget.manager.path.name == "test.czi" + assert widget.manager.path.name == 'test.czi' # Title should show filename - assert "test.czi" in widget._title_label.value + assert 'test.czi' in widget._title_label.value def test_error_mode_no_installable_plugins(self, make_napari_viewer): """Test widget in error mode without installable plugins.""" from ndevio._plugin_manager import ReaderPluginManager from ndevio.widgets import PluginInstallerWidget - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: # Mock report showing all suggested plugins already installed mock_report.return_value = { - "bioio-imageio": Mock(supported=False), # for .xyz files - "ArrayLike": Mock(supported=False), + 'bioio-imageio': Mock(supported=False), # for .xyz files + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.png") + manager = ReaderPluginManager('test.png') widget = PluginInstallerWidget(plugin_manager=manager) # Should still have ALL plugins available @@ -173,7 +173,7 @@ def test_returns_job_id(self): from ndevio._plugin_installer import install_plugin # This will queue the installation but not actually run it - job_id = install_plugin("bioio-imageio") + job_id = install_plugin('bioio-imageio') # Job ID should be an integer assert isinstance(job_id, int) @@ -199,7 +199,7 @@ def on_finished(event): # Check that we got a completion event assert len(completed) > 0 - assert "bioio-imageio" in completed[0].get("pkgs", []) + assert 'bioio-imageio' in completed[0].get('pkgs', []) class TestVerifyPluginInstalled: @@ -210,14 +210,14 @@ def test_verify_installed_plugin(self): from ndevio._plugin_installer import verify_plugin_installed # bioio should be installed since it's a dependency - assert verify_plugin_installed("bioio") + assert verify_plugin_installed('bioio') def test_verify_not_installed_plugin(self): """Test verification of a plugin that isn't installed.""" from ndevio._plugin_installer import verify_plugin_installed # Use a plugin that definitely won't be installed - assert not verify_plugin_installed("bioio-nonexistent-plugin-12345") + assert not verify_plugin_installed('bioio-nonexistent-plugin-12345') def test_verify_converts_name_format(self): """Test that plugin name is correctly converted to module name.""" @@ -225,7 +225,7 @@ def test_verify_converts_name_format(self): # Test with installed package (bioio-base should be installed) # The function should convert bioio-base -> bioio_base - result = verify_plugin_installed("bioio-base") + result = verify_plugin_installed('bioio-base') assert isinstance(result, bool) diff --git a/tests/test_plugin_installer_integration.py b/tests/test_plugin_installer_integration.py index 2e6f758..daec073 100644 --- a/tests/test_plugin_installer_integration.py +++ b/tests/test_plugin_installer_integration.py @@ -15,16 +15,16 @@ def test_opens_widget_with_viewer(self, make_napari_viewer): import ndevio._napari_reader as reader_module viewer = make_napari_viewer() - test_path = "test.czi" + test_path = 'test.czi' error = UnsupportedFileFormatError( - reader_name="test", path=test_path, msg_extra="" + reader_name='test', path=test_path, msg_extra='' ) # Mock plugin_feasibility_report from bioio (not ndevio) - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-ome-tiff": Mock(supported=False), - "ArrayLike": Mock(supported=False), + 'bioio-ome-tiff': Mock(supported=False), + 'ArrayLike': Mock(supported=False), } # Call the function @@ -36,7 +36,7 @@ def test_opens_widget_with_viewer(self, make_napari_viewer): # Find the plugin installer widget plugin_widget = None for name, widget in viewer.window.dock_widgets.items(): - if "Install BioIO Plugin" in name: + if 'Install BioIO Plugin' in name: plugin_widget = widget break @@ -49,12 +49,12 @@ def test_widget_has_correct_path(self, make_napari_viewer): import ndevio._napari_reader as reader_module viewer = make_napari_viewer() - test_path = Path("path/to/test.czi") + test_path = Path('path/to/test.czi') error = UnsupportedFileFormatError( - reader_name="test", path=str(test_path), msg_extra="" + reader_name='test', path=str(test_path), msg_extra='' ) - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = {} reader_module._open_plugin_installer(test_path, error) @@ -63,7 +63,7 @@ def test_widget_has_correct_path(self, make_napari_viewer): # Find the plugin installer widget by name widget = None for name, docked_widget in viewer.window.dock_widgets.items(): - if "Install BioIO Plugin" in name: + if 'Install BioIO Plugin' in name: widget = docked_widget break @@ -80,18 +80,18 @@ def test_filters_installed_plugins(self, make_napari_viewer): import ndevio._napari_reader as reader_module viewer = make_napari_viewer() - test_path = "test.czi" + test_path = 'test.czi' error = UnsupportedFileFormatError( - reader_name="test", path=test_path, msg_extra="" + reader_name='test', path=test_path, msg_extra='' ) # Mock feasibility report showing bioio-czi as installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-czi": Mock( + 'bioio-czi': Mock( supported=False ), # Installed but can't read this file - "ArrayLike": Mock(supported=False), + 'ArrayLike': Mock(supported=False), } reader_module._open_plugin_installer(test_path, error) @@ -100,7 +100,7 @@ def test_filters_installed_plugins(self, make_napari_viewer): # Find the plugin installer widget by name widget = None for name, docked_widget in viewer.window.dock_widgets.items(): - if "Install BioIO Plugin" in name: + if 'Install BioIO Plugin' in name: widget = docked_widget break @@ -109,7 +109,7 @@ def test_filters_installed_plugins(self, make_napari_viewer): # bioio-czi should be filtered out from installable_plugins installable = widget.manager.installable_plugins if installable: - assert "bioio-czi" not in installable + assert 'bioio-czi' not in installable def test_suggests_uninstalled_plugins(self, make_napari_viewer): """Test that uninstalled plugins are suggested.""" @@ -118,16 +118,16 @@ def test_suggests_uninstalled_plugins(self, make_napari_viewer): import ndevio._napari_reader as reader_module viewer = make_napari_viewer() - test_path = "test.lif" # LIF files need bioio-lif + test_path = 'test.lif' # LIF files need bioio-lif error = UnsupportedFileFormatError( - reader_name="test", path=test_path, msg_extra="" + reader_name='test', path=test_path, msg_extra='' ) # Mock feasibility report with no bioio-lif installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "bioio-ome-tiff": Mock(supported=False), - "ArrayLike": Mock(supported=False), + 'bioio-ome-tiff': Mock(supported=False), + 'ArrayLike': Mock(supported=False), } reader_module._open_plugin_installer(test_path, error) @@ -136,7 +136,7 @@ def test_suggests_uninstalled_plugins(self, make_napari_viewer): # Find the plugin installer widget by name widget = None for name, docked_widget in viewer.window.dock_widgets.items(): - if "Install BioIO Plugin" in name: + if 'Install BioIO Plugin' in name: widget = docked_widget break @@ -145,7 +145,7 @@ def test_suggests_uninstalled_plugins(self, make_napari_viewer): # bioio-lif should be in installable_plugins installable = widget.manager.installable_plugins assert installable is not None - assert "bioio-lif" in installable + assert 'bioio-lif' in installable class TestPluginInstallerWidgetIntegration: @@ -159,24 +159,24 @@ def test_widget_created_in_error_mode(self, make_napari_viewer): make_napari_viewer() # Create viewer context # Create manager for a CZI file - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: # Mock report showing no plugins installed mock_report.return_value = { - "ArrayLike": Mock(supported=False), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.czi") + manager = ReaderPluginManager('test.czi') widget = PluginInstallerWidget(plugin_manager=manager) # Verify widget state assert widget.manager.path is not None - assert widget.manager.path.name == "test.czi" - assert "test.czi" in widget._title_label.value + assert widget.manager.path.name == 'test.czi' + assert 'test.czi' in widget._title_label.value # bioio-czi should be in installable plugins and pre-selected installable = widget.manager.installable_plugins - assert "bioio-czi" in installable - assert widget._plugin_select.value == "bioio-czi" + assert 'bioio-czi' in installable + assert widget._plugin_select.value == 'bioio-czi' def test_install_button_queues_installation(self, make_napari_viewer): """Test that clicking install button queues installation.""" @@ -187,17 +187,17 @@ def test_install_button_queues_installation(self, make_napari_viewer): widget = PluginInstallerWidget() # Select a plugin - widget._plugin_select.value = "bioio-imageio" + widget._plugin_select.value = 'bioio-imageio' # Mock the install_plugin function at the point of import - with patch("ndevio._plugin_installer.install_plugin") as mock_install: + with patch('ndevio._plugin_installer.install_plugin') as mock_install: mock_install.return_value = 123 # Mock job ID # Click install button widget._on_install_clicked() # Verify install was called with correct plugin - mock_install.assert_called_once_with("bioio-imageio") + mock_install.assert_called_once_with('bioio-imageio') def test_widget_shows_all_plugins(self, make_napari_viewer): """Test that widget shows all available plugins.""" @@ -222,12 +222,12 @@ def test_widget_preselects_first_installable(self, make_napari_viewer): make_napari_viewer() # Mock report showing no plugins installed - with patch("bioio.plugin_feasibility_report") as mock_report: + with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { - "ArrayLike": Mock(supported=False), + 'ArrayLike': Mock(supported=False), } - manager = ReaderPluginManager("test.lif") + manager = ReaderPluginManager('test.lif') widget = PluginInstallerWidget(plugin_manager=manager) # bioio-lif should be pre-selected (first installable) @@ -242,14 +242,14 @@ def test_install_updates_status_label(self, make_napari_viewer): make_napari_viewer() widget = PluginInstallerWidget() - widget._plugin_select.value = "bioio-imageio" + widget._plugin_select.value = 'bioio-imageio' - with patch("ndevio._plugin_installer.install_plugin") as mock_install: + with patch('ndevio._plugin_installer.install_plugin') as mock_install: mock_install.return_value = 123 # Status should update to "Installing..." widget._on_install_clicked() - assert "Installing" in widget._status_label.value + assert 'Installing' in widget._status_label.value def test_no_plugin_selected_shows_error(self, make_napari_viewer): """Test that clicking install with no selection shows error.""" @@ -262,4 +262,4 @@ def test_no_plugin_selected_shows_error(self, make_napari_viewer): widget._on_install_clicked() - assert "No plugin selected" in widget._status_label.value + assert 'No plugin selected' in widget._status_label.value diff --git a/tests/test_sampledata.py b/tests/test_sampledata.py index 4d74926..2fde2fb 100644 --- a/tests/test_sampledata.py +++ b/tests/test_sampledata.py @@ -14,7 +14,9 @@ ) -def _validate_layer_data_tuples(result: list, expected_layer_type: str | None = None): +def _validate_layer_data_tuples( + result: list, expected_layer_type: str | None = None +): """Helper to validate LayerDataTuple structure. Parameters @@ -35,7 +37,7 @@ def _validate_layer_data_tuples(result: list, expected_layer_type: str | None = data, kwargs, layer_type = layer_tuple # Data should be array-like with shape - assert hasattr(data, "shape") + assert hasattr(data, 'shape') assert len(data.shape) >= 2 # At minimum 2D # kwargs should be a dict @@ -43,7 +45,7 @@ def _validate_layer_data_tuples(result: list, expected_layer_type: str | None = # layer_type should be a string assert isinstance(layer_type, str) - assert layer_type in ("image", "labels") + assert layer_type in ('image', 'labels') if expected_layer_type: assert layer_type == expected_layer_type @@ -55,21 +57,21 @@ class TestLocalSampleData: def test_ndev_logo(self): """Test loading ndev logo returns valid LayerDataTuples.""" result = ndev_logo() - _validate_layer_data_tuples(result, expected_layer_type="image") + _validate_layer_data_tuples(result, expected_layer_type='image') # Logo should be a single image layer assert len(result) == 1 def test_neuron_labels(self): """Test loading neuron labels returns valid LayerDataTuples.""" result = neuron_labels() - _validate_layer_data_tuples(result, expected_layer_type="labels") + _validate_layer_data_tuples(result, expected_layer_type='labels') # Should have 4 channels as separate label layers assert len(result) == 4 def test_neuron_labels_processed(self): """Test loading processed neuron labels returns valid LayerDataTuples.""" result = neuron_labels_processed() - _validate_layer_data_tuples(result, expected_layer_type="labels") + _validate_layer_data_tuples(result, expected_layer_type='labels') # Should have 4 channels as separate label layers assert len(result) == 4 @@ -91,19 +93,19 @@ def test_scratch_assay(self): assert len(result) == 4 # Check we have both image and labels types layer_types = [t[2] for t in result] - assert "image" in layer_types - assert "labels" in layer_types + assert 'image' in layer_types + assert 'labels' in layer_types def test_neocortex(self): """Test loading neocortex returns valid LayerDataTuples.""" result = neocortex() - _validate_layer_data_tuples(result, expected_layer_type="image") + _validate_layer_data_tuples(result, expected_layer_type='image') # Should have 3 channels as separate image layers assert len(result) == 3 def test_neuron_raw(self): """Test loading neuron raw returns valid LayerDataTuples.""" result = neuron_raw() - _validate_layer_data_tuples(result, expected_layer_type="image") + _validate_layer_data_tuples(result, expected_layer_type='image') # Should have 4 channels as separate image layers assert len(result) == 4 diff --git a/tests/test_utilities_container.py b/tests/test_utilities_container.py index 5acea81..d27bfb3 100644 --- a/tests/test_utilities_container.py +++ b/tests/test_utilities_container.py @@ -9,7 +9,9 @@ image_2d = np.asarray([[0, 0, 1, 1], [0, 0, 1, 1], [2, 2, 1, 1], [2, 2, 1, 1]]) shapes_2d = np.array([[0.25, 0.25], [0.25, 2.75], [2.75, 2.75], [2.75, 0.25]]) -labels_2d = np.asarray([[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]]) +labels_2d = np.asarray( + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]] +) image_4d = np.random.random((1, 1, 10, 10)) shapes_4d = [ @@ -38,8 +40,8 @@ @pytest.fixture( params=[ - (image_2d, shapes_2d, labels_2d, "YX"), - (image_4d, shapes_4d, labels_4d, "TZYX"), + (image_2d, shapes_2d, labels_2d, 'YX'), + (image_4d, shapes_4d, labels_4d, 'TZYX'), ] ) def test_data(request: pytest.FixtureRequest): @@ -59,11 +61,11 @@ def test_save_shapes_as_labels( viewer.add_shapes(test_shape) container = UtilitiesContainer(viewer) - container._viewer.layers.selection.active = viewer.layers["test_shape"] + container._viewer.layers.selection.active = viewer.layers['test_shape'] container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' - expected_save_loc = tmp_path / "Shapes" / "test.tiff" + expected_save_loc = tmp_path / 'Shapes' / 'test.tiff' container.save_layers_as_ome_tiff() # Wait for file to exist (avoids race condition with signal timing) @@ -72,7 +74,7 @@ def test_save_shapes_as_labels( assert expected_save_loc.exists() saved_img = nImage(expected_save_loc) assert saved_img.shape[1] == 1 # single channel (C dimension is index 1) - assert saved_img.channel_names == ["Shapes"] + assert saved_img.channel_names == ['Shapes'] def test_save_labels(qtbot, make_napari_viewer, tmp_path: Path, test_data): @@ -82,13 +84,13 @@ def test_save_labels(qtbot, make_napari_viewer, tmp_path: Path, test_data): viewer.add_labels( test_labels ) # <- should add a way to specify this is the selected layer in the viewer - viewer.layers.selection.active = viewer.layers["test_labels"] + viewer.layers.selection.active = viewer.layers['test_labels'] container = UtilitiesContainer(viewer) container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' - expected_save_loc = tmp_path / "Labels" / "test.tiff" + expected_save_loc = tmp_path / 'Labels' / 'test.tiff' container.save_layers_as_ome_tiff() # Wait for file to exist (avoids race condition with signal timing) @@ -97,21 +99,23 @@ def test_save_labels(qtbot, make_napari_viewer, tmp_path: Path, test_data): assert expected_save_loc.exists() saved_img = nImage(expected_save_loc) assert saved_img.shape[1] == 1 # single channel (C dimension is index 1) - assert saved_img.channel_names == ["Labels"] + assert saved_img.channel_names == ['Labels'] -def test_save_image_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): +def test_save_image_layer( + qtbot, make_napari_viewer, test_data, tmp_path: Path +): test_image, _, _, squeezed_dims = test_data viewer = make_napari_viewer() viewer.add_image(test_image) container = UtilitiesContainer(viewer) - container._viewer.layers.selection.active = viewer.layers["test_image"] - container._channel_names.value = ["0"] + container._viewer.layers.selection.active = viewer.layers['test_image'] + container._channel_names.value = ['0'] container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' - expected_save_loc = tmp_path / "Image" / "test.tiff" + expected_save_loc = tmp_path / 'Image' / 'test.tiff' container.save_layers_as_ome_tiff() # Wait for file to exist (avoids race condition with signal timing) @@ -120,10 +124,12 @@ def test_save_image_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): assert expected_save_loc.exists() saved_img = nImage(expected_save_loc) assert saved_img.shape[1] == 1 # single channel (C dimension is index 1) - assert saved_img.channel_names == ["0"] + assert saved_img.channel_names == ['0'] -def test_save_multi_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): +def test_save_multi_layer( + qtbot, make_napari_viewer, test_data, tmp_path: Path +): test_image, _, test_labels, squeezed_dims = test_data viewer = make_napari_viewer() viewer.add_image(test_image) @@ -131,13 +137,13 @@ def test_save_multi_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): container = UtilitiesContainer(viewer) container._viewer.layers.selection = [ - viewer.layers["test_labels"], - viewer.layers["test_image"], + viewer.layers['test_labels'], + viewer.layers['test_image'], ] container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' - expected_save_loc = tmp_path / "Layers" / "test.tiff" + expected_save_loc = tmp_path / 'Layers' / 'test.tiff' container.save_layers_as_ome_tiff() # Wait for file to exist (avoids race condition with signal timing) @@ -150,7 +156,7 @@ def test_save_multi_layer(qtbot, make_napari_viewer, test_data, tmp_path: Path): @pytest.fixture def test_rgb_image(resources_dir: Path): - path = resources_dir / "RGB_bad_metadata.tiff" + path = resources_dir / 'RGB_bad_metadata.tiff' img = nImage(path) return path, img @@ -163,9 +169,12 @@ def test_update_metadata_from_file(make_napari_viewer, test_rgb_image): container._files.value = path container.update_metadata_on_file_select() - assert container._save_name.value == "RGB_bad_metadata" - assert container._dim_shape.value == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" - assert container._squeezed_dims_order == "YX" + assert container._save_name.value == 'RGB_bad_metadata' + assert ( + container._dim_shape.value + == 'T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3' + ) + assert container._squeezed_dims_order == 'YX' assert container._channel_names.value == "['red', 'green', 'blue']" @@ -175,18 +184,18 @@ def test_update_metadata_from_layer(make_napari_viewer, test_data): viewer.add_image(test_image, scale=(2, 3)) container = UtilitiesContainer(viewer) - container._viewer.layers.selection.active = viewer.layers["test_image"] + container._viewer.layers.selection.active = viewer.layers['test_image'] container.update_metadata_from_layer() assert ( - "Tried to update metadata, but could only update scale" + 'Tried to update metadata, but could only update scale' ) in container._results.value assert container._scale_tuple.value == (1, 2, 3) @pytest.fixture def test_czi_image(resources_dir: Path): - path = resources_dir / "0T-4C-0Z-7pos.czi" + path = resources_dir / '0T-4C-0Z-7pos.czi' img = nImage(path) return path, img @@ -196,8 +205,8 @@ def test_save_files_as_ome_tiff(test_czi_image, tmp_path: Path, qtbot): container = UtilitiesContainer() container._files.value = path container._save_directory.value = tmp_path - save_dir = tmp_path / "ConcatenatedImages" - expected_file = save_dir / "0T-4C-0Z-7pos.tiff" + save_dir = tmp_path / 'ConcatenatedImages' + expected_file = save_dir / '0T-4C-0Z-7pos.tiff' container.save_files_as_ome_tiff() @@ -210,11 +219,11 @@ def test_save_files_as_ome_tiff(test_czi_image, tmp_path: Path, qtbot): assert expected_file.exists() -@pytest.mark.parametrize("num_files", [1, 2]) +@pytest.mark.parametrize('num_files', [1, 2]) def test_select_next_images(resources_dir: Path, num_files: int): container = UtilitiesContainer() - image_dir = resources_dir / "test_czis" + image_dir = resources_dir / 'test_czis' # get all the files in the directory all_image_files = list(image_dir.iterdir()) # sort the files @@ -236,7 +245,7 @@ def test_select_next_images(resources_dir: Path, num_files: int): def test_batch_concatenate_files(tmp_path: Path, resources_dir: Path, qtbot): container = UtilitiesContainer() - image_dir = resources_dir / "test_czis" + image_dir = resources_dir / 'test_czis' all_image_files = list(image_dir.iterdir()) all_image_files = natsort.os_sorted(all_image_files) @@ -244,11 +253,11 @@ def test_batch_concatenate_files(tmp_path: Path, resources_dir: Path, qtbot): container._files.value = all_image_files[:1] container._save_directory.value = tmp_path - container._save_directory_prefix.value = "test" + container._save_directory_prefix.value = 'test' container.batch_concatenate_files() # Wait for threaded batch to complete - expected_output_dir = tmp_path / "test_ConcatenatedImages" + expected_output_dir = tmp_path / 'test_ConcatenatedImages' # 4 tiff files + 1 log file = 5 total qtbot.waitUntil( @@ -260,21 +269,21 @@ def test_batch_concatenate_files(tmp_path: Path, resources_dir: Path, qtbot): assert expected_output_dir.exists() output_files = list(expected_output_dir.iterdir()) - tiff_files = [f for f in output_files if f.suffix == ".tiff"] + tiff_files = [f for f in output_files if f.suffix == '.tiff'] assert len(tiff_files) == 4 - assert (expected_output_dir / "batch_concatenate.log.txt").exists() + assert (expected_output_dir / 'batch_concatenate.log.txt').exists() def test_batch_cancel_button(tmp_path: Path, resources_dir: Path, qtbot): """Test that cancel button stops the batch runner.""" container = UtilitiesContainer() - image_dir = resources_dir / "test_czis" + image_dir = resources_dir / 'test_czis' all_image_files = list(image_dir.iterdir()) all_image_files = natsort.os_sorted(all_image_files) container._files.value = tuple(all_image_files[:1]) container._save_directory.value = tmp_path - container._save_directory_prefix.value = "test" + container._save_directory_prefix.value = 'test' # Start the batch operation container.batch_concatenate_files() @@ -286,7 +295,9 @@ def test_batch_cancel_button(tmp_path: Path, resources_dir: Path, qtbot): container._concatenate_batch_button.clicked() # Wait for cancellation to complete - qtbot.waitUntil(lambda: not container._batch_runner.is_running, timeout=10000) + qtbot.waitUntil( + lambda: not container._batch_runner.is_running, timeout=10000 + ) # Verify it stopped assert not container._batch_runner.is_running @@ -301,17 +312,17 @@ def test_batch_error_callback(qtbot): container = UtilitiesContainer() # Create a mock context with a file_set item (files, save_name) - mock_files = [Path("file1.tiff"), Path("file2.tiff")] + mock_files = [Path('file1.tiff'), Path('file2.tiff')] ctx = MagicMock(spec=BatchContext) - ctx.item = (mock_files, "bad_file") + ctx.item = (mock_files, 'bad_file') - test_exception = ValueError("Test error message") + test_exception = ValueError('Test error message') container._on_batch_error(ctx, test_exception) # Verify progress bar label was updated with error info - assert "Error on bad_file" in container._progress_bar.label - assert "Test error message" in container._progress_bar.label + assert 'Error on bad_file' in container._progress_bar.label + assert 'Test error message' in container._progress_bar.label def test_batch_button_state_toggle(qtbot): @@ -319,15 +330,15 @@ def test_batch_button_state_toggle(qtbot): container = UtilitiesContainer() # Initial state should be 'Batch Concat.' - assert container._concatenate_batch_button.text == "Batch Concat." + assert container._concatenate_batch_button.text == 'Batch Concat.' # Set to running state container._set_batch_button_state(running=True) - assert container._concatenate_batch_button.text == "Cancel" + assert container._concatenate_batch_button.text == 'Cancel' # Set back to not running container._set_batch_button_state(running=False) - assert container._concatenate_batch_button.text == "Batch Concat." + assert container._concatenate_batch_button.text == 'Batch Concat.' def test_save_scenes_ome_tiff(test_czi_image, tmp_path: Path, qtbot): @@ -335,7 +346,7 @@ def test_save_scenes_ome_tiff(test_czi_image, tmp_path: Path, qtbot): container = UtilitiesContainer() container._files.value = path container._save_directory.value = tmp_path - save_dir = tmp_path / "ExtractedScenes" + save_dir = tmp_path / 'ExtractedScenes' container.save_scenes_ome_tiff() @@ -356,7 +367,7 @@ def test_extract_and_save_scenes_ome_tiff(test_czi_image, tmp_path: Path): ) path, _ = test_czi_image - save_dir = tmp_path / "ExtractedScenes" + save_dir = tmp_path / 'ExtractedScenes' # Collect all yielded results results = list(extract_and_save_scenes_ome_tiff(path, save_dir)) @@ -378,10 +389,12 @@ def test_extract_and_save_scenes_ome_tiff_specific_scenes( ) path, _ = test_czi_image - save_dir = tmp_path / "ExtractedScenes" + save_dir = tmp_path / 'ExtractedScenes' # Extract only scenes 0 and 2 - results = list(extract_and_save_scenes_ome_tiff(path, save_dir, scenes=[0, 2])) + results = list( + extract_and_save_scenes_ome_tiff(path, save_dir, scenes=[0, 2]) + ) assert len(results) == 2 assert len(list(save_dir.iterdir())) == 2 @@ -395,8 +408,11 @@ def test_open_images(make_napari_viewer, test_rgb_image): container._files.value = path container.open_images() - assert container._dim_shape.value == "T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3" - assert container._squeezed_dims_order == "YX" + assert ( + container._dim_shape.value + == 'T: 1, C: 1, Z: 1, Y: 1440, X: 1920, S: 3' + ) + assert container._squeezed_dims_order == 'YX' assert container._channel_names.value == "['red', 'green', 'blue']" @@ -406,20 +422,20 @@ def test_canvas_export_figure(make_napari_viewer, tmp_path: Path): container = UtilitiesContainer(viewer) container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' container.canvas_export_figure() - expected_save_loc = tmp_path / "Figures" / "test_figure.png" + expected_save_loc = tmp_path / 'Figures' / 'test_figure.png' - assert "Exported canvas" in container._results.value + assert 'Exported canvas' in container._results.value assert expected_save_loc.exists() assert expected_save_loc.stat().st_size > 0 # make sure properly detects 3D mode doesn't work viewer.dims.ndisplay = 3 container.canvas_export_figure() - assert "Exporting Figure only works in 2D mode" in container._results.value + assert 'Exporting Figure only works in 2D mode' in container._results.value def test_canvas_screenshot(make_napari_viewer, tmp_path: Path): @@ -428,13 +444,13 @@ def test_canvas_screenshot(make_napari_viewer, tmp_path: Path): container = UtilitiesContainer(viewer) container._save_directory.value = tmp_path - container._save_name.value = "test" + container._save_name.value = 'test' container.canvas_screenshot() - expected_save_loc = tmp_path / "Figures" / "test_canvas.png" + expected_save_loc = tmp_path / 'Figures' / 'test_canvas.png' - assert "Exported screenshot of canvas" in container._results.value + assert 'Exported screenshot of canvas' in container._results.value assert expected_save_loc.exists() assert expected_save_loc.stat().st_size > 0 @@ -463,7 +479,7 @@ def test_rescale_by(make_napari_viewer): def test_get_dims_for_shape_layer(): container = UtilitiesContainer() - container._squeezed_dims_order = "YX" + container._squeezed_dims_order = 'YX' container._squeezed_dims = (20, 30) dims = container._get_dims_for_shape_layer() From 9430136ce206c5e6daa05182eeb5cfa3d186c44f Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Sun, 7 Dec 2025 22:30:55 -0600 Subject: [PATCH 3/3] update precommit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8abd962..9099864 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: check-docstring-first - id: end-of-file-fixer @@ -8,7 +8,7 @@ repos: exclude: ^\.napari-hub/.* - id: check-yaml # checks for correct yaml syntax for github actions ex. - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.6 + rev: v0.14.8 hooks: - id: ruff-check - id: ruff-format