Skip to content

Commit e49f04c

Browse files
author
its-serah
committed
Implement raise_on_missing_reader flag for LoadImage transforms
This commit addresses CodeRabbit review feedback and adds a new flag to control exception behavior when readers are not available: - Add raise_on_missing_reader parameter to LoadImage and LoadImaged - Fix guarding for class-based readers to respect the flag - Add proper stacklevel and category to all warning messages for better UX - Update LoadImage docstring with Raises section and accepted reader types - Add comprehensive test coverage for the new functionality - Apply code formatting with black, isort, and ruff - Maintain backward compatibility (flag defaults to False) The flag allows users to choose between strict error handling or fallback behavior when specified readers cannot be found or their dependencies are missing. Signed-off-by: Sarah <[email protected]>
1 parent f315bcb commit e49f04c

File tree

2 files changed

+97
-8
lines changed

2 files changed

+97
-8
lines changed

monai/transforms/io/array.py

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
from monai.transforms.utility.array import EnsureChannelFirst
4646
from monai.utils import (
4747
GridSamplePadMode,
48-
ImageMetaKey,
48+
)
49+
from monai.utils import ImageMetaKey
50+
from monai.utils import ImageMetaKey as Key
51+
from monai.utils import (
4952
MetaKeys,
5053
OptionalImportError,
5154
convert_to_dst_type,
@@ -138,6 +141,7 @@ def __init__(
138141
prune_meta_pattern: str | None = None,
139142
prune_meta_sep: str = ".",
140143
expanduser: bool = True,
144+
raise_on_missing_reader: bool = False,
141145
*args,
142146
**kwargs,
143147
) -> None:
@@ -161,9 +165,21 @@ def __init__(
161165
in the metadata (nested dictionary). default is ".", see also :py:class:`monai.transforms.DeleteItemsd`.
162166
e.g. ``prune_meta_pattern=".*_code$", prune_meta_sep=" "`` removes meta keys that ends with ``"_code"``.
163167
expanduser: if True cast filename to Path and call .expanduser on it, otherwise keep filename as is.
168+
raise_on_missing_reader: if True, raise `OptionalImportError` when a specified reader is not available;
169+
otherwise attempt to use fallback readers. Defaults to False (backward compatibility).
164170
args: additional parameters for reader if providing a reader name.
165171
kwargs: additional parameters for reader if providing a reader name.
166172
173+
Raises:
174+
OptionalImportError: If `raise_on_missing_reader=True` and the specified reader
175+
cannot be found or its optional dependency is not installed.
176+
177+
Accepted reader types:
178+
- str: name of a registered reader (e.g., `"ITKReader"`)
179+
- class: e.g., `ITKReader` or a custom reader class
180+
- instance: e.g., `ITKReader(pixel_type=itk.UC)`
181+
- list/tuple: multiple reader names or classes to try in order
182+
167183
Note:
168184
169185
- The transform returns a MetaTensor, unless `set_track_meta(False)` has been used, in which case, a
@@ -183,6 +199,7 @@ def __init__(
183199
self.pattern = prune_meta_pattern
184200
self.sep = prune_meta_sep
185201
self.expanduser = expanduser
202+
self.raise_on_missing_reader = raise_on_missing_reader
186203

187204
self.readers: list[ImageReader] = []
188205
for r in SUPPORTED_READERS: # set predefined readers as default
@@ -206,18 +223,61 @@ def __init__(
206223
if not has_built_in:
207224
the_reader = locate(f"{_r}") # search dotted path
208225
if the_reader is None:
209-
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
226+
try:
227+
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
228+
except ValueError:
229+
# If the reader name is not recognized at all, raise OptionalImportError
230+
msg = f"Cannot find reader '{_r}'. It may not be installed or recognized."
231+
if self.raise_on_missing_reader:
232+
raise OptionalImportError(msg)
233+
else:
234+
warnings.warn(
235+
f"{msg} Will use fallback readers if available.",
236+
category=UserWarning,
237+
stacklevel=2,
238+
)
239+
continue
210240
try:
211241
self.register(the_reader(*args, **kwargs))
212-
except OptionalImportError:
213-
warnings.warn(
214-
f"required package for reader {_r} is not installed, or the version doesn't match requirement."
242+
except OptionalImportError as e:
243+
msg = (
244+
f"Required package for reader {_r} is not installed, or the version doesn't match requirement."
215245
)
246+
if self.raise_on_missing_reader:
247+
raise OptionalImportError(msg) from e
248+
else:
249+
warnings.warn(
250+
f"{msg} Will use fallback readers if available.",
251+
category=UserWarning,
252+
stacklevel=2,
253+
)
216254
except TypeError: # the reader doesn't have the corresponding args/kwargs
217-
warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.")
255+
warnings.warn(
256+
f"{_r} is not supported with the given parameters {args} {kwargs}.",
257+
category=UserWarning,
258+
stacklevel=2,
259+
)
218260
self.register(the_reader())
219261
elif inspect.isclass(_r):
220-
self.register(_r(*args, **kwargs))
262+
try:
263+
self.register(_r(*args, **kwargs))
264+
except OptionalImportError as e:
265+
msg = f"Required package for reader {_r.__name__} is not installed, or the version doesn't match requirement."
266+
if self.raise_on_missing_reader:
267+
raise OptionalImportError(msg) from e
268+
else:
269+
warnings.warn(
270+
f"{msg} Will use fallback readers if available.",
271+
category=UserWarning,
272+
stacklevel=2,
273+
)
274+
except TypeError:
275+
warnings.warn(
276+
f"{_r.__name__} is not supported with the given parameters {args} {kwargs}.",
277+
category=UserWarning,
278+
stacklevel=2,
279+
)
280+
self.register(_r())
221281
else:
222282
self.register(_r) # reader instance, ignoring the constructor args/kwargs
223283
return

tests/transforms/test_load_image.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import shutil
1616
import tempfile
1717
import unittest
18+
import warnings
1819
from pathlib import Path
1920

2021
import nibabel as nib
@@ -28,7 +29,7 @@
2829
from monai.data.meta_obj import set_track_meta
2930
from monai.data.meta_tensor import MetaTensor
3031
from monai.transforms import LoadImage
31-
from monai.utils import optional_import
32+
from monai.utils import OptionalImportError, optional_import
3233
from tests.test_utils import SkipIfNoModule, assert_allclose, skip_if_downloading_fails, testing_data_config
3334

3435
itk, has_itk = optional_import("itk", allow_namespace_pkg=True)
@@ -436,12 +437,40 @@ def test_my_reader(self):
436437
self.assertEqual(out.meta["name"], "my test")
437438
out = LoadImage(image_only=True, reader=_MiniReader, is_compatible=False)("test")
438439
self.assertEqual(out.meta["name"], "my test")
440+
441+
def test_reader_not_installed_exception(self):
442+
"""test if an exception is raised when a specified reader is not installed"""
443+
with self.assertRaises(OptionalImportError):
444+
LoadImage(image_only=True, reader="NonExistentReader")("test")
439445
for item in (_MiniReader, _MiniReader(is_compatible=False)):
440446
out = LoadImage(image_only=True, reader=item)("test")
441447
self.assertEqual(out.meta["name"], "my test")
442448
out = LoadImage(image_only=True)("test", reader=_MiniReader(is_compatible=False))
443449
self.assertEqual(out.meta["name"], "my test")
444450

451+
def test_raise_on_missing_reader_flag(self):
452+
"""test raise_on_missing_reader flag behavior"""
453+
# Test with flag enabled - should raise exception for unknown reader name
454+
with self.assertRaises(OptionalImportError):
455+
LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=True)
456+
457+
# Test with flag disabled - should warn but not raise exception for unknown reader name
458+
# This should succeed and create the loader with fallback behavior
459+
with warnings.catch_warnings(record=True) as w:
460+
warnings.simplefilter("always")
461+
loader_with_fallback = LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=False)
462+
self.assertIsInstance(loader_with_fallback, LoadImage)
463+
# Should have produced a warning about the unknown reader
464+
self.assertTrue(any("Cannot find reader 'UnknownReaderName'" in str(warning.message) for warning in w))
465+
466+
# The flag should work properly with valid readers too
467+
loader_with_flag = LoadImage(image_only=True, reader="ITKReader", raise_on_missing_reader=False)
468+
loader_without_flag = LoadImage(image_only=True, reader="ITKReader")
469+
470+
# Both should work since ITK is available in this test environment
471+
self.assertIsInstance(loader_with_flag, LoadImage)
472+
self.assertIsInstance(loader_without_flag, LoadImage)
473+
445474
def test_itk_meta(self):
446475
"""test metadata from a directory"""
447476
out = LoadImage(image_only=True, reader="ITKReader", pixel_type=itk_uc, series_meta=True)(

0 commit comments

Comments
 (0)