Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Normalize all text files to LF on commit
* text=auto eol=lf

# SCM syntax highlighting & preventing 3-way merges
pixi.lock merge=binary linguist-language=YAML linguist-generated=true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ target/
.idea/
venv/
.vscode/
.venv*/

# IPython Notebook
.ipynb_checkpoints
Expand Down
12 changes: 7 additions & 5 deletions src/ndevio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
except ImportError:
__version__ = 'unknown'

from .utils import helpers

# Type stub for lazy import - lets type checkers know nImage exists
if TYPE_CHECKING:
from .nimage import nImage as nImage
from .utils import helpers as helpers


def __getattr__(name: str):
"""Lazily import nImage to speed up package import."""
def __getattr__(name: str) -> object:
"""Lazily import heavy submodules to speed up package import."""
if name == 'nImage':
from .nimage import nImage

return nImage
if name == 'helpers':
from .utils import helpers

return helpers
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')


Expand Down
14 changes: 8 additions & 6 deletions src/ndevio/_napari_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
from functools import partial
from typing import TYPE_CHECKING

from ndev_settings import get_settings

from .nimage import nImage

if TYPE_CHECKING:
from napari.types import LayerDataTuple, PathLike, ReaderFunction

from .nimage import nImage

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -40,6 +38,8 @@ def napari_get_reader(
The reader function for the given path
"""

from ndev_settings import get_settings

settings = get_settings()

open_first_scene_only = (
Expand Down Expand Up @@ -93,6 +93,8 @@ def napari_reader_function(
"""
from bioio_base.exceptions import UnsupportedFileFormatError

from .nimage import nImage

try:
img = nImage(path) # nImage handles preferred reader and fallback
except UnsupportedFileFormatError:
Expand Down Expand Up @@ -125,7 +127,7 @@ def _open_scene_container(path: PathLike, img: nImage) -> None:

import napari

from .widgets import DELIMITER, nImageSceneWidget
from .widgets._scene_widget import DELIMITER, nImageSceneWidget

viewer = napari.current_viewer()
viewer.window.add_dock_widget(
Expand Down Expand Up @@ -155,7 +157,7 @@ def _open_plugin_installer(path: PathLike) -> None:
from bioio_base.exceptions import UnsupportedFileFormatError

from .bioio_plugins._manager import ReaderPluginManager
from .widgets import PluginInstallerWidget
from .widgets._plugin_install_widget import PluginInstallerWidget

# Get viewer, handle case where no viewer available
viewer = napari.current_viewer()
Expand Down
16 changes: 8 additions & 8 deletions src/ndevio/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ contributions:
python_name: ndevio._napari_reader:napari_get_reader
title: Open file with ndevio
- id: ndevio.make_plugin_installer_widget
python_name: ndevio.widgets:PluginInstallerWidget
python_name: ndevio.widgets._plugin_install_widget:PluginInstallerWidget
title: Install BioIO Reader Plugins
- id: ndevio.make_utilities_widget
python_name: ndevio.widgets:UtilitiesContainer
python_name: ndevio.widgets._utilities_container:UtilitiesContainer
title: I/O Utilities
- id: ndevio.make_ndev_logo
python_name: ndevio.sampledata:ndev_logo
python_name: ndevio.sampledata._sample_data:ndev_logo
title: Load ndev logo
- id: ndevio.make_scratch_assay
python_name: ndevio.sampledata:scratch_assay
python_name: ndevio.sampledata._sample_data:scratch_assay
title: Load scratch assay data
- id: ndevio.make_neocortex
python_name: ndevio.sampledata:neocortex
python_name: ndevio.sampledata._sample_data:neocortex
title: Load neocortex data
- id: ndevio.make_neuron_raw
python_name: ndevio.sampledata:neuron_raw
python_name: ndevio.sampledata._sample_data:neuron_raw
title: Load raw neuron data
- id: ndevio.make_neuron_labels
python_name: ndevio.sampledata:neuron_labels
python_name: ndevio.sampledata._sample_data:neuron_labels
title: Load neuron labels data
- id: ndevio.make_neuron_labels_processed
python_name: ndevio.sampledata:neuron_labels_processed
python_name: ndevio.sampledata._sample_data:neuron_labels_processed
title: Load processed neuron labels data
readers:
- command: ndevio.get_reader
Expand Down
2 changes: 0 additions & 2 deletions src/ndevio/nimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from typing import TYPE_CHECKING

from bioio import BioImage
from bioio_base.reader import Reader
from bioio_base.types import ImageLike

from .bioio_plugins._manager import raise_unsupported_with_suggestions
from .utils._layer_utils import (
Expand Down
18 changes: 0 additions & 18 deletions src/ndevio/sampledata/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
"""Sample data for ndevio and the ndev-kit ecosystem."""

from ndevio.sampledata._sample_data import (
ndev_logo,
neocortex,
neuron_labels,
neuron_labels_processed,
neuron_raw,
scratch_assay,
)

__all__ = [
'ndev_logo',
'neocortex',
'neuron_labels',
'neuron_labels_processed',
'neuron_raw',
'scratch_assay',
]
33 changes: 27 additions & 6 deletions src/ndevio/sampledata/_sample_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@
from pathlib import Path
from typing import TYPE_CHECKING

import pooch
from bioio_imageio import Reader as ImageIOReader
from bioio_ome_tiff import Reader as OmeTiffReader

from ndevio import nImage

if TYPE_CHECKING:
from napari.types import LayerDataTuple

Expand All @@ -24,6 +18,10 @@

def ndev_logo() -> list[LayerDataTuple]:
"""Load the ndev logo image."""
from bioio_imageio import Reader as ImageIOReader

from ..nimage import nImage

return nImage(
SAMPLE_DIR / 'ndev-logo.png',
reader=ImageIOReader,
Expand All @@ -32,6 +30,11 @@ def ndev_logo() -> list[LayerDataTuple]:

def scratch_assay() -> list[LayerDataTuple]:
"""Load scratch assay data with labeled nuclei and cytoplasm."""
import pooch
from bioio_ome_tiff import Reader as OmeTiffReader

from ..nimage import nImage

scratch_assay_raw_path = pooch.retrieve(
url='doi:10.5281/zenodo.17845346/scratch-assay-labeled-10T-2Ch.tiff',
known_hash='md5:2b98c4ea18cd741a1545e59855348a2f',
Expand All @@ -58,6 +61,11 @@ def scratch_assay() -> list[LayerDataTuple]:

def neocortex() -> list[LayerDataTuple]:
"""Load neocortex 3-channel image data."""
import pooch
from bioio_ome_tiff import Reader as OmeTiffReader

from ..nimage import nImage

neocortex_raw_path = pooch.retrieve(
url='doi:10.5281/zenodo.17845346/neocortex-3Ch.tiff',
known_hash='md5:eadc3fac751052461fb2e5f3c6716afa',
Expand All @@ -75,6 +83,11 @@ def neuron_raw() -> list[LayerDataTuple]:

This sample is downloaded from Zenodo if not present locally.
"""
import pooch
from bioio_ome_tiff import Reader as OmeTiffReader

from ..nimage import nImage

neuron_raw_path = pooch.retrieve(
url='doi:10.5281/zenodo.17845346/neuron-4Ch_raw.tiff',
known_hash='md5:5d3e42bca2085e8588b6f23cf89ba87c',
Expand All @@ -94,6 +107,10 @@ def neuron_raw() -> list[LayerDataTuple]:

def neuron_labels() -> list[LayerDataTuple]:
"""Load neuron labels data."""
from bioio_ome_tiff import Reader as OmeTiffReader

from ..nimage import nImage

return nImage(
SAMPLE_DIR / 'neuron-4Ch_labels.tiff',
reader=OmeTiffReader,
Expand All @@ -104,6 +121,10 @@ def neuron_labels() -> list[LayerDataTuple]:

def neuron_labels_processed() -> list[LayerDataTuple]:
"""Load processed neuron labels data."""
from bioio_ome_tiff import Reader as OmeTiffReader

from ..nimage import nImage

return nImage(
SAMPLE_DIR / 'neuron-4Ch_labels_processed.tiff',
reader=OmeTiffReader,
Expand Down
13 changes: 0 additions & 13 deletions src/ndevio/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1 @@
"""Widgets for ndevio package."""

from ..bioio_plugins._manager import ReaderPluginManager
from ._plugin_install_widget import PluginInstallerWidget
from ._scene_widget import DELIMITER, nImageSceneWidget
from ._utilities_container import UtilitiesContainer

__all__ = [
'PluginInstallerWidget',
'nImageSceneWidget',
'UtilitiesContainer',
'DELIMITER',
'ReaderPluginManager',
]
34 changes: 34 additions & 0 deletions tests/test_nimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ def test_nImage_zarr(resources_dir: Path):
assert img.data.shape == (1, 1, 2, 4, 4)


def test_nImage_zarr_trailing_slash(resources_dir: Path):
"""Test that a string path with a trailing slash is handled correctly.

Regression test: bioio's extension-based reader detection fails when the
path ends with '/', e.g. 'store.zarr/'. nImage strips the slash on init.
See https://github.com/ndev-kit/ndevio/issues/XX
"""
path_with_slash = str(resources_dir / ZARR) + '/'
img = nImage(path_with_slash)
assert img.data is not None
# path stored without the trailing slash
assert not img.path.endswith('/')
assert img.path == str(resources_dir / ZARR)
assert img.data.shape == (1, 1, 2, 4, 4)


@pytest.mark.network
def test_nImage_remote_zarr_trailing_slash():
"""Test that a remote Zarr URL with a trailing slash is read correctly.

Regression test: 'https://...9846152.zarr/' crashed with
'Reader ndevio returned no data' because the trailing slash prevented
bioio from matching the '*.zarr' extension pattern.
"""
remote_zarr = (
'https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.4/idr0048A/9846152.zarr/'
)
img = nImage(remote_zarr)
assert img._is_remote
assert not img.path.endswith('/')
assert img.path == remote_zarr.rstrip('/')
assert img.xarray_dask_data is not None


@pytest.mark.network
def test_nImage_remote_zarr():
"""Test that nImage can read a remote Zarr file."""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_sampledata/test_sampledata.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from ndevio.sampledata import (
from ndevio.sampledata._sample_data import (
ndev_logo,
neocortex,
neuron_labels,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_widgets/test_plugin_installer_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TestPluginInstallerWidget:

def test_standalone_mode(self):
"""Test widget in standalone mode - no path, shows generic title."""
from ndevio.widgets import PluginInstallerWidget
from ndevio.widgets._plugin_install_widget import PluginInstallerWidget

widget = PluginInstallerWidget()

Expand All @@ -27,7 +27,7 @@ def test_standalone_mode(self):
def test_error_mode_with_path(self):
"""Test widget in error mode - has path, preselects suggested plugin."""
from ndevio.bioio_plugins._manager import ReaderPluginManager
from ndevio.widgets import PluginInstallerWidget
from ndevio.widgets._plugin_install_widget import PluginInstallerWidget

# Mock installed plugins to NOT include bioio-czi
# This simulates the error case where file can't be read
Expand All @@ -48,7 +48,7 @@ def test_error_mode_with_path(self):

def test_install_button_behavior(self):
"""Test install button: queues installation and updates status."""
from ndevio.widgets import PluginInstallerWidget
from ndevio.widgets._plugin_install_widget import PluginInstallerWidget

widget = PluginInstallerWidget()
widget._plugin_select.value = 'bioio-imageio'
Expand All @@ -64,7 +64,7 @@ def test_install_button_behavior(self):

def test_install_without_selection_shows_error(self):
"""Test that clicking install with no selection shows error."""
from ndevio.widgets import PluginInstallerWidget
from ndevio.widgets._plugin_install_widget import PluginInstallerWidget

widget = PluginInstallerWidget()
widget._plugin_select.value = None
Expand Down