Skip to content

Commit 808e5d5

Browse files
Merge pull request #162 from bioio-devs/feature/plugin-priority
feature/plugin-priority
2 parents 9dc5c2c + b333927 commit 808e5d5

File tree

18 files changed

+1243
-741
lines changed

18 files changed

+1243
-741
lines changed

bioio/bio_image.py

Lines changed: 133 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33

4-
import datetime
54
import logging
65
from pathlib import Path
7-
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args
6+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union, get_args
87

98
import bioio_base as biob
109
import dask.array as da
@@ -15,7 +14,7 @@
1514
from ome_types import OME
1615

1716
from .ome_utils import generate_ome_channel_id
18-
from .plugins import PluginEntry, check_type, get_array_like_plugin, get_plugins
17+
from .plugins import PluginEntry, get_array_like_plugin, get_plugins
1918

2019
###############################################################################
2120

@@ -35,9 +34,28 @@ class BioImage(biob.image_container.ImageContainer):
3534
----------
3635
image: biob.types.ImageLike
3736
A string, Path, fsspec supported URI, or arraylike to read.
38-
reader: Optional[Type[Reader]]
39-
The Reader class to specifically use for reading the provided image.
40-
Default: None (find matching reader)
37+
reader: Optional[
38+
Union[
39+
Type[biob.reader.Reader],
40+
Sequence[Type[biob.reader.Reader]],
41+
]
42+
]
43+
Controls how BioImage selects the underlying Reader:
44+
45+
* If a **single Reader subclass** is provided
46+
(e.g. ``reader=TiffReader``), that reader is used directly.
47+
Plugin discovery, extension matching, and default ordering are
48+
bypassed entirely.
49+
50+
* If a **sequence of Reader subclasses** is provided
51+
(e.g. ``reader=[TiffReader, OmeTiffReader]``), the sequence is
52+
treated as an explicit *try-order*. BioImage attempts to
53+
construct each reader in order and uses the **first one that
54+
successfully constructs**.
55+
56+
* If ``None`` (default), BioImage uses its default plugin discovery
57+
and ordering logic based on the input image. For a detailed
58+
description of this behavior, see ``bioio.plugins.get_plugins``.
4159
reconstruct_mosaic: bool
4260
Boolean for setting that data for this object to the reconstructed / stitched
4361
mosaic image.
@@ -101,7 +119,16 @@ class BioImage(biob.image_container.ImageContainer):
101119
102120
>>> img = BioImage("malformed_metadata.ome.tiff", reader=readers.TiffReader)
103121
104-
Data for a mosaic file is returned pre-stitched (if the base reader supports it).
122+
Initialize an image and override plugin selection order when multiple readers
123+
support the same extension.
124+
125+
>>> from bioio_tifffile import Reader as TiffReader
126+
>>> from bioio_ome_tiff import Reader as OmeTiffReader
127+
>>> img = BioImage(
128+
... "multi_plugin_file.ome.tiff",
129+
... reader=[TiffReader, OmeTiffReader],
130+
... )
131+
... # TiffReader will be tried before OmeTiffReader for this instance.
105132
106133
>>> img = BioImage("big_mosaic.czi")
107134
... img.dims # <Dimensions [T: 40, C: 3, Z: 1, Y: 30000, X: 45000]>
@@ -138,9 +165,11 @@ def determine_plugin(
138165
Determine the appropriate plugin to read a given image.
139166
140167
This function identifies the most suitable plugin to read the provided image
141-
based on its type or file extension. It leverages the installed plugins for
142-
`bioio`, each of which supports a subset of image formats. If a suitable
143-
plugin is found, it is returned; otherwise, an error is raised.
168+
based on its type or file extension. It consults the installed `bioio` reader
169+
plugins (discovered via :func:`bioio.plugins.get_plugins`) and, when the image
170+
is path-like, probes candidate readers in priority order using
171+
``ReaderClass.is_supported_image(...)``. If a suitable plugin is found, it is
172+
returned; otherwise, an error is raised.
144173
145174
Parameters
146175
----------
@@ -168,39 +197,36 @@ def determine_plugin(
168197
Notes
169198
-----
170199
This function performs the following steps:
200+
171201
1. Fetches an updated mapping of available plugins,
172202
optionally using a cached version.
173203
2. If the `image` is a file path (str or Path), it checks for a matching
174204
plugin based on the file extension.
175-
3. If the `image` is an array-like object, it attempts to use the
176-
built-in `ArrayLikeReader`.
177-
4. If no suitable plugin is found, raises an `UnsupportedFileFormatError`.
205+
3. For matching extensions, tries candidate readers in the order provided by
206+
the plugin mapping, returning the first constructed plugin whose reader.
207+
4. If `image` is array-like (or a list of array-like), returns the built-in
208+
ArrayLike reader plugin.
209+
5. If no suitable plugin is found, raises an `UnsupportedFileFormatError`.
210+
211+
Extension ordering and plugin ordering are defined by
212+
:func:`bioio.plugins.get_plugins`. See that function for the
213+
description of BioIO’s default selection policy.
178214
179215
Examples
180216
--------
181217
To determine the appropriate plugin for a given image file:
182218
183219
>>> image_path = "example_image.tif"
184-
>>> plugin = determine_plugin(image_path)
220+
>>> plugin = BioImage.determine_plugin(image_path)
185221
>>> print(plugin)
186222
187223
To determine the appropriate plugin for an array-like image:
188224
189225
>>> import numpy as np
190226
>>> image_array = np.random.random((5, 5, 5))
191-
>>> plugin = determine_plugin(image_array)
227+
>>> plugin = BioImage.determine_plugin(image_array)
192228
>>> print(plugin)
193229
194-
Implementation Details
195-
----------------------
196-
- The function first converts the image to a string representation.
197-
- If the image is a file path, it verifies the path and checks the file
198-
extension against the known plugins.
199-
- For each matching plugin, it tries to instantiate a reader and checks
200-
if it supports the image.
201-
- If the image is array-like, it uses a built-in reader designed for
202-
such objects.
203-
- Detailed logging is provided for troubleshooting purposes.
204230
"""
205231
# Fetch updated mapping of plugins
206232
plugins_by_ext = get_plugins(use_cache=use_plugin_cache)
@@ -223,17 +249,15 @@ def determine_plugin(
223249
ReaderClass = plugin.metadata.get_reader()
224250
try:
225251
if ReaderClass.is_supported_image(
226-
image,
227-
fs_kwargs=fs_kwargs,
252+
image, fs_kwargs=fs_kwargs
228253
):
229254
return plugin
230-
231255
except FileNotFoundError as fe:
232256
raise fe
233257
except Exception as e:
234258
log.warning(
235-
f"Attempted file ({path}) load with "
236-
f"reader: {ReaderClass} failed with error: {e}"
259+
f"Attempted file ({path}) load with reader: "
260+
f"{ReaderClass} failed with error: {e}"
237261
)
238262

239263
# Use built-in ArrayLikeReader if type MetaArrayLike
@@ -281,41 +305,86 @@ def ends_with_ext(string: str) -> bool:
281305
@staticmethod
282306
def _get_reader(
283307
image: biob.types.ImageLike,
284-
reader: biob.reader.Reader,
308+
readers: Optional[Sequence[Type[biob.reader.Reader]]],
285309
use_plugin_cache: bool,
286310
fs_kwargs: Dict[str, Any],
287311
**kwargs: Any,
288312
) -> Tuple[biob.reader.Reader, Optional[PluginEntry]]:
289313
"""
290-
Initializes and returns the reader (and plugin if relevant) for the provided
291-
image based on provided args and/or the available bioio supported plugins
314+
Select and initialize a reader for the provided image.
315+
316+
Behavior
317+
--------
318+
* If ``reader`` is ``None``, BioImage selects a reader using the default
319+
plugin discovery and ordering logic and returns both the reader instance
320+
and its corresponding ``PluginEntry``.
321+
322+
* If ``reader`` is a sequence of Reader subclasses, BioImage attempts to
323+
initialize each reader in order and returns the first one that successfully
324+
constructs. No other readers or plugins are considered.
292325
"""
293-
if reader is not None:
294-
# Check specific reader image types in a situation where a specified reader
295-
# only supports some of the ImageLike types.
296-
if not check_type(image, reader):
297-
raise biob.exceptions.UnsupportedFileFormatError(
298-
reader.__name__, str(type(image))
299-
)
300326

301-
return reader(image, fs_kwargs=fs_kwargs, **kwargs), None
327+
# Case 1: Default Priority
328+
if readers is None:
329+
plugin = BioImage.determine_plugin(
330+
image,
331+
fs_kwargs=fs_kwargs,
332+
use_plugin_cache=use_plugin_cache,
333+
**kwargs,
334+
)
335+
ReaderClass = plugin.metadata.get_reader()
336+
return ReaderClass(image, fs_kwargs=fs_kwargs, **kwargs), plugin
337+
338+
# Case 2: User Selection of Readers
339+
if not readers or not all(
340+
isinstance(r, type) and issubclass(r, biob.reader.Reader) for r in readers
341+
):
342+
raise TypeError(
343+
"BioImage(reader=...) must be a Reader subclass, "
344+
"a non-empty sequence of Reader subclasses, or None."
345+
)
302346

303-
# Determine reader class based on available plugins
304-
plugin = BioImage.determine_plugin(
305-
image, fs_kwargs=fs_kwargs, use_plugin_cache=use_plugin_cache, **kwargs
347+
failures: list[str] = []
348+
for ReaderClass in readers:
349+
name = getattr(ReaderClass, "__name__", repr(ReaderClass))
350+
try:
351+
return ReaderClass(image, fs_kwargs=fs_kwargs, **kwargs), None
352+
except Exception as e:
353+
log.warning(
354+
"Exclusive reader attempt failed. reader=%s image=%r error=%s",
355+
name,
356+
image,
357+
e,
358+
)
359+
failures.append(f"{name}: {e}")
360+
361+
raise biob.exceptions.UnsupportedFileFormatError(
362+
"BioImage",
363+
str(image) if isinstance(image, (str, Path)) else str(type(image)),
364+
msg_extra=(
365+
"None of the specified readers were able to read this image.\n"
366+
"Readers attempted:\n - " + "\n - ".join(failures)
367+
),
306368
)
307-
ReaderClass = plugin.metadata.get_reader()
308-
return ReaderClass(image, fs_kwargs=fs_kwargs, **kwargs), plugin
309369

310370
def __init__(
311371
self,
312372
image: biob.types.ImageLike,
313-
reader: Optional[Type[biob.reader.Reader]] = None,
373+
reader: Optional[
374+
Union[Type[biob.reader.Reader], Sequence[Type[biob.reader.Reader]]]
375+
] = None,
314376
reconstruct_mosaic: bool = True,
315377
use_plugin_cache: bool = False,
316378
fs_kwargs: Dict[str, Any] = {},
317379
**kwargs: Any,
318380
):
381+
self._reader: Optional[biob.reader.Reader] = None
382+
self._plugin: Optional[PluginEntry] = None
383+
384+
# Normalize Single Reader
385+
if reader is not None and isinstance(reader, type):
386+
reader = [reader]
387+
319388
try:
320389
self._reader, self._plugin = self._get_reader(
321390
image,
@@ -1214,16 +1283,29 @@ def save(
12141283
)
12151284

12161285
def __str__(self) -> str:
1286+
"""
1287+
Summary of this BioImage.
1288+
1289+
Reports the plugin entrypoint name if the reader was selected via the
1290+
plugin system; otherwise reports the explicit reader name in use.
1291+
"""
1292+
in_memory = self._xarray_data is not None
1293+
12171294
if self._plugin is not None:
12181295
return (
12191296
f"<BioImage ["
1220-
f"plugin: {self._plugin.entrypoint.name} installed "
1221-
f"at {datetime.datetime.fromtimestamp(self._plugin.timestamp)}, "
1222-
f"Image-is-in-Memory: {self._xarray_data is not None}"
1297+
f"plugin: {self._plugin.entrypoint.name}, "
1298+
f"image-in-memory: {in_memory}"
12231299
f"]>"
12241300
)
12251301

1226-
return f"<BioImage [Image-is-in-Memory: {self._xarray_data is not None}]>"
1302+
reader_name = self._reader.name if self._reader is not None else "None"
1303+
return (
1304+
f"<BioImage ["
1305+
f"reader: {reader_name}, "
1306+
f"image-in-memory: {in_memory}"
1307+
f"]>"
1308+
)
12271309

12281310
def __repr__(self) -> str:
12291311
return str(self)

0 commit comments

Comments
 (0)