Skip to content

Commit a0e2838

Browse files
authored
Add plugin priority and refactor for compatibility with napari-ndev legacy behavior (#24)
# References and relevant issues This PR is needed to maintain behavior for using `ndevio` in `napari-ndev` in ndev-kit/napari-ndev#194 # Description 1. Adds `bioio-tifffile` to core dependencies, because, at least for now, this is needed for some awkward tiff files that _can_ be read by `bioio-imageio` but result in bad metadata reading. Since there are currently no `tifffile` pins in any of the dependencies, this behavior will remain for now. It's worth considering in the future if this is really the way I want core dependencies going forward. 2. This orders the plugin list to be used for infering the reader, because despite the installation order of plugins, some get installed before others (perhaps this is a `uv` thing, compared to `pip`). As such, I need a way to opinionatedly pick my preferred plugins, but the preferred plugin setting still gives the user an easy way to override if they can only find their desired behavior with a specific plugin. It will always fall back to the setting. 3. Refactors `determine_reader_plugin` to mostly just rely on the plugin manager
1 parent 12a4775 commit a0e2838

File tree

10 files changed

+191
-140
lines changed

10 files changed

+191
-140
lines changed

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ dependencies = [
3333
"xarray",
3434
"bioio-base",
3535
"bioio",
36-
"bioio-imageio",
37-
"bioio-ome-tiff",
3836
"bioio-ome-zarr",
39-
"napari-plugin-manager", # For plugin installation widget
37+
"bioio-ome-tiff",
38+
"bioio-tifffile", # a backup tifffile reader with better performance than imageio
39+
"bioio-imageio",
40+
"napari-plugin-manager>=0.1.7", # Renamed to PYPI from PIP for tool selection https://github.com/napari/napari-plugin-manager/pull/176
4041
]
4142

4243
[project.optional-dependencies]

src/ndevio/_bioio_plugin_utils.py

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Public API:
77
BIOIO_PLUGINS - Dict of all bioio plugins and their file extensions
88
suggest_plugins_for_path() - Get list of suggested plugins by file extension
9+
get_reader_priority() - Get reader priority list from BIOIO_PLUGINS order
910
1011
Internal API (used by ReaderPluginManager):
1112
format_plugin_installation_message() - Generate installation message
@@ -35,33 +36,21 @@
3536

3637
# Bioio plugins and their supported extensions
3738
# Source: https://github.com/bioio-devs/bioio
39+
#
40+
# ORDERING MATTERS: Plugins are listed in priority order (highest priority first).
41+
# This order is used by ReaderPluginManager when selecting readers.
42+
# Priority is based on:
43+
# 1. Metadata preservation quality (OME formats preserve most metadata)
44+
# 2. Reliability and performance for specific formats
45+
# 3. Known issues or limitations
3846
BIOIO_PLUGINS = {
39-
"bioio-czi": {
40-
"extensions": [".czi"],
41-
"description": "Zeiss CZI files",
42-
"repository": "https://github.com/bioio-devs/bioio-czi",
43-
},
44-
"bioio-dv": {
45-
"extensions": [".dv", ".r3d"],
46-
"description": "DeltaVision files",
47-
"repository": "https://github.com/bioio-devs/bioio-dv",
48-
},
49-
"bioio-imageio": {
50-
"extensions": [".bmp", ".gif", ".jpg", ".jpeg", ".png"],
51-
"description": "Generic image formats (PNG, JPG, etc.)",
52-
"repository": "https://github.com/bioio-devs/bioio-imageio",
47+
# Highest priority: OME formats with excellent metadata preservation
48+
"bioio-ome-zarr": {
49+
"extensions": [".zarr"],
50+
"description": "OME-Zarr files",
51+
"repository": "https://github.com/bioio-devs/bioio-ome-zarr",
5352
"core": True,
5453
},
55-
"bioio-lif": {
56-
"extensions": [".lif"],
57-
"description": "Leica LIF files",
58-
"repository": "https://github.com/bioio-devs/bioio-lif",
59-
},
60-
"bioio-nd2": {
61-
"extensions": [".nd2"],
62-
"description": "Nikon ND2 files",
63-
"repository": "https://github.com/bioio-devs/bioio-nd2",
64-
},
6554
"bioio-ome-tiff": {
6655
"extensions": [".ome.tif", ".ome.tiff", ".tif", ".tiff"],
6756
"description": "OME-TIFF files with valid OME-XML metadata",
@@ -73,27 +62,51 @@
7362
"description": "Tiled OME-TIFF files",
7463
"repository": "https://github.com/bioio-devs/bioio-ome-tiled-tiff",
7564
},
76-
"bioio-ome-zarr": {
77-
"extensions": [".zarr"],
78-
"description": "OME-Zarr files",
79-
"repository": "https://github.com/bioio-devs/bioio-ome-zarr",
65+
# High priority: Format-specific readers with good metadata support
66+
"bioio-tifffile": {
67+
"extensions": [".tif", ".tiff"],
68+
"description": "TIFF files (including those without OME metadata)",
69+
"repository": "https://github.com/bioio-devs/bioio-tifffile",
8070
"core": True,
8171
},
72+
"bioio-nd2": {
73+
"extensions": [".nd2"],
74+
"description": "Nikon ND2 files",
75+
"repository": "https://github.com/bioio-devs/bioio-nd2",
76+
},
77+
"bioio-czi": {
78+
"extensions": [".czi"],
79+
"description": "Zeiss CZI files",
80+
"repository": "https://github.com/bioio-devs/bioio-czi",
81+
},
82+
"bioio-lif": {
83+
"extensions": [".lif"],
84+
"description": "Leica LIF files",
85+
"repository": "https://github.com/bioio-devs/bioio-lif",
86+
},
87+
"bioio-dv": {
88+
"extensions": [".dv", ".r3d"],
89+
"description": "DeltaVision files",
90+
"repository": "https://github.com/bioio-devs/bioio-dv",
91+
},
8292
"bioio-sldy": {
8393
"extensions": [".sldy", ".dir"],
8494
"description": "3i SlideBook files",
8595
"repository": "https://github.com/bioio-devs/bioio-sldy",
8696
},
87-
"bioio-tifffile": {
88-
"extensions": [".tif", ".tiff"],
89-
"description": "TIFF files (including those without OME metadata)",
90-
"repository": "https://github.com/bioio-devs/bioio-tifffile",
97+
# Lower priority: Generic/fallback readers
98+
"bioio-imageio": {
99+
"extensions": [".bmp", ".gif", ".jpg", ".jpeg", ".png"],
100+
"description": "Generic image formats (PNG, JPG, etc.)",
101+
"repository": "https://github.com/bioio-devs/bioio-imageio",
102+
"core": True,
91103
},
92104
"bioio-tiff-glob": {
93105
"extensions": [".tiff"],
94106
"description": "TIFF sequences (glob patterns)",
95107
"repository": "https://github.com/bioio-devs/bioio-tiff-glob",
96108
},
109+
# Lowest priority: Requires external dependencies (Java)
97110
"bioio-bioformats": {
98111
"extensions": [".oib", ".oif", ".vsi", ".ims", ".lsm", ".stk"],
99112
"description": "Proprietary microscopy formats (requires Java)",
@@ -111,6 +124,27 @@
111124
_EXTENSION_TO_PLUGIN[ext].append(plugin_name)
112125

113126

127+
def get_reader_priority() -> list[str]:
128+
"""Get reader priority list from BIOIO_PLUGINS dictionary order.
129+
130+
Returns plugin names in priority order (highest priority first).
131+
This order is used by ReaderPluginManager when selecting readers.
132+
133+
Returns
134+
-------
135+
list of str
136+
Plugin names in priority order
137+
138+
Examples
139+
--------
140+
>>> from ndevio._bioio_plugin_utils import get_reader_priority
141+
>>> priority = get_reader_priority()
142+
>>> print(priority[0]) # Highest priority reader
143+
'bioio-ome-zarr'
144+
"""
145+
return list(BIOIO_PLUGINS.keys())
146+
147+
114148
def format_plugin_installation_message(
115149
filename: str,
116150
suggested_plugins: list[str],

src/ndevio/_napari_reader.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,17 @@ def napari_get_reader(
7373
open_first_scene_only=open_first_scene_only,
7474
open_all_scenes=open_all_scenes,
7575
)
76+
7677
except UnsupportedFileFormatError as e:
7778
# determine_reader_plugin() already enhanced the error message
7879
logger.error("ndevio: Unsupported file format: %s", path)
7980
# Show plugin installer widget if enabled in settings
8081
if settings.ndevio_Reader.suggest_reader_plugins: # type: ignore
8182
_open_plugin_installer(path, e)
8283

83-
raise
84+
# Return None per napari reader spec - don't raise exception
85+
return None
86+
8487
except Exception as e: # noqa: BLE001
8588
logger.warning("ndevio: Error reading file: %s", e)
8689
return None
@@ -196,6 +199,13 @@ def _open_plugin_installer(
196199
# Get viewer, handle case where no viewer available
197200
viewer = napari.current_viewer()
198201

202+
# Don't try to open widget if no viewer available (e.g., in tests)
203+
if viewer is None:
204+
logger.warning(
205+
"Cannot open plugin installer widget: No napari viewer available"
206+
)
207+
return
208+
199209
# Create plugin manager for this file
200210
manager = ReaderPluginManager(path)
201211

src/ndevio/_plugin_installer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def install_plugin(plugin_name: str) -> int:
6464

6565
# Determine which tool to use (pip vs conda)
6666
# napari-plugin-manager handles this automatically based on environment
67-
tool = InstallerTools.PIP # Default to pip for bioio packages
67+
tool = InstallerTools.PYPI # Default to PYPI for bioio packages
6868

6969
# Queue the installation
7070
job_id = queue.install(tool=tool, pkgs=[plugin_name])

src/ndevio/_plugin_manager.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ def get_working_reader(
230230
231231
Tries readers in priority order:
232232
1. Preferred reader if it's installed and supported
233-
2. Any installed reader that supports the file
234-
3. None if no working reader found
233+
2. Readers from BIOIO_PLUGINS dict order (highest priority first)
234+
3. Any other installed reader that supports the file
235+
4. None if no working reader found
235236
236237
Parameters
237238
----------
@@ -244,6 +245,12 @@ def get_working_reader(
244245
Reader class that can read the file, or None if no suitable
245246
reader is installed.
246247
248+
Notes
249+
-----
250+
The priority order is determined by the ordering of BIOIO_PLUGINS
251+
in _bioio_plugin_utils.py, which prioritizes readers based on
252+
metadata preservation quality, reliability, and known issues.
253+
247254
Examples
248255
--------
249256
>>> manager = ReaderPluginManager("image.tif")
@@ -274,11 +281,23 @@ def get_working_reader(
274281
)
275282
return self._get_reader_module(preferred_reader)
276283

277-
# Try any installed reader that supports the file
284+
# Try readers in priority order from BIOIO_PLUGINS
285+
from ._bioio_plugin_utils import get_reader_priority
286+
287+
for reader_name in get_reader_priority():
288+
if reader_name in report and report[reader_name].supported:
289+
logger.info(
290+
"Using reader: %s for %s (from priority list)",
291+
reader_name,
292+
self.path,
293+
)
294+
return self._get_reader_module(reader_name)
295+
296+
# Try any other installed reader that supports the file
278297
for name, support in report.items():
279298
if name != "ArrayLike" and support.supported:
280299
logger.info(
281-
"Using reader: %s for %s (preferred not available)",
300+
"Using reader: %s for %s (from installed plugins)",
282301
name,
283302
self.path,
284303
)

src/ndevio/nimage.py

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,24 @@ def determine_reader_plugin(
4040
image: ImageLike, preferred_reader: str | None = None
4141
) -> Reader:
4242
"""
43-
Determine the reader for the plugin to be used to load the image.
44-
In order of priority:
45-
1. If preferred_reader from settings is suitable, use that.
46-
2. Otherwise, use bioio.BioImage.determine_plugin() to find a suitable reader.
47-
3. If all else fails, raise an error with suggestions for missing plugins.
43+
Determine the reader plugin to use for loading an image.
44+
45+
Convenience wrapper that integrates ReaderPluginManager with ndevio settings.
46+
For file paths, uses the priority system (DEFAULT_READER_PRIORITY).
47+
For arrays, uses bioio's default plugin detection.
4848
4949
Parameters
5050
----------
5151
image : ImageLike
52-
Image to be loaded. Can be a path to an image file, a numpy array,
53-
or an xarray DataArray.
52+
Image to be loaded (file path, numpy array, or xarray DataArray).
5453
preferred_reader : str, optional
55-
Preferred reader to be used to load the image. If not provided,
56-
it will fallback to the preference in settings. Finally, if this
57-
preferred reader is not suitable, a reader will be attempted to be
58-
determined based on the image type.
54+
Preferred reader name. If None, uses ndevio_Reader.preferred_reader
55+
from settings.
5956
6057
Returns
6158
-------
6259
Reader
63-
Reader to be used to load the image.
60+
Reader class to use for loading the image.
6461
6562
Raises
6663
------
@@ -73,40 +70,34 @@ def determine_reader_plugin(
7370

7471
from ._plugin_manager import ReaderPluginManager
7572

76-
settings = get_settings()
73+
# For file paths: use ReaderPluginManager
74+
if isinstance(image, str | Path):
75+
settings = get_settings()
7776

78-
preferred_reader = (
79-
preferred_reader or settings.ndevio_Reader.preferred_reader # type: ignore
80-
)
77+
# Get preferred reader from settings if not provided
78+
if preferred_reader is None:
79+
preferred_reader = settings.ndevio_Reader.preferred_reader # type: ignore
8180

82-
# Only use ReaderPluginManager for file paths, not arrays
83-
if isinstance(image, str | Path):
8481
manager = ReaderPluginManager(image)
8582
reader = manager.get_working_reader(preferred_reader)
8683

8784
if reader:
8885
return reader
8986

90-
# No reader found - raise with helpful message if enabled
87+
# No reader found - raise error with installation suggestions
9188
if settings.ndevio_Reader.suggest_reader_plugins: # type: ignore
92-
raise UnsupportedFileFormatError(
93-
reader_name="ndevio",
94-
path=str(image),
95-
msg_extra=manager.get_installation_message(),
96-
)
89+
msg_extra = manager.get_installation_message()
9790
else:
98-
# Re-raise without suggestions
99-
raise UnsupportedFileFormatError(
100-
reader_name="ndevio",
101-
path=str(image),
102-
)
103-
else:
104-
# For arrays, use bioio's built-in plugin determination
105-
try:
106-
return nImage.determine_plugin(image).metadata.get_reader()
107-
except UnsupportedFileFormatError:
108-
# Re-raise for non-file inputs
109-
raise
91+
msg_extra = None
92+
93+
raise UnsupportedFileFormatError(
94+
reader_name="ndevio",
95+
path=str(image),
96+
msg_extra=msg_extra,
97+
)
98+
99+
# For arrays: use bioio's built-in plugin determination
100+
return nImage.determine_plugin(image).metadata.get_reader()
110101

111102

112103
class nImage(BioImage):

tests/test_bioio_plugin_utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,20 +101,21 @@ def test_manager_excludes_core_plugins_from_installable(self):
101101

102102
manager = ReaderPluginManager("test.tiff")
103103

104-
# Core plugins (like bioio-ome-tiff) should not be in installable
104+
# Core plugins should not be in installable
105105
installable_plugins = manager.installable_plugins
106106

107-
# bioio-ome-tiff is a core plugin, shouldn't need installation
107+
# These are core plugins, shouldn't need installation
108108
core_plugins = [
109109
"bioio-ome-tiff",
110110
"bioio-imageio",
111111
"bioio-ome-zarr",
112+
"bioio-tifffile",
112113
]
113114
for core in core_plugins:
114115
assert core not in installable_plugins
115116

116-
# bioio-tifffile is not core, should be installable
117-
assert "bioio-tifffile" in installable_plugins
117+
# bioio-tiff-glob is not core, should be installable
118+
assert "bioio-tiff-glob" in installable_plugins
118119

119120

120121
class TestFormatPluginInstallationMessage:

0 commit comments

Comments
 (0)