11#!/usr/bin/env python
22# -*- coding: utf-8 -*-
33
4- import datetime
54import logging
65from 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
98import bioio_base as biob
109import dask .array as da
1514from ome_types import OME
1615
1716from .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