diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 49b0665a90..ab27c551e1 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -138,6 +138,7 @@ def __init__( prune_meta_pattern: str | None = None, prune_meta_sep: str = ".", expanduser: bool = True, + raise_on_missing_reader: bool = False, *args, **kwargs, ) -> None: @@ -161,6 +162,8 @@ def __init__( in the metadata (nested dictionary). default is ".", see also :py:class:`monai.transforms.DeleteItemsd`. e.g. ``prune_meta_pattern=".*_code$", prune_meta_sep=" "`` removes meta keys that ends with ``"_code"``. expanduser: if True cast filename to Path and call .expanduser on it, otherwise keep filename as is. + raise_on_missing_reader: if True, raise OptionalImportError when a specified reader is not available, + otherwise attempt to use fallback readers. Default is False to maintain backward compatibility. args: additional parameters for reader if providing a reader name. kwargs: additional parameters for reader if providing a reader name. @@ -183,6 +186,7 @@ def __init__( self.pattern = prune_meta_pattern self.sep = prune_meta_sep self.expanduser = expanduser + self.raise_on_missing_reader = raise_on_missing_reader self.readers: list[ImageReader] = [] for r in SUPPORTED_READERS: # set predefined readers as default @@ -206,13 +210,25 @@ def __init__( if not has_built_in: the_reader = locate(f"{_r}") # search dotted path if the_reader is None: - the_reader = look_up_option(_r.lower(), SUPPORTED_READERS) + try: + the_reader = look_up_option(_r.lower(), SUPPORTED_READERS) + except ValueError: + # If the reader name is not recognized at all, raise OptionalImportError + raise OptionalImportError( + f"Cannot find reader '{_r}'. It may not be installed or recognized." + ) try: self.register(the_reader(*args, **kwargs)) - except OptionalImportError: - warnings.warn( - f"required package for reader {_r} is not installed, or the version doesn't match requirement." - ) + except OptionalImportError as e: + if self.raise_on_missing_reader: + raise OptionalImportError( + f"required package for reader {_r} is not installed, or the version doesn't match requirement." + ) from e + else: + warnings.warn( + f"required package for reader {_r} is not installed, or the version doesn't match requirement. " + f"Will use fallback readers if available." + ) except TypeError: # the reader doesn't have the corresponding args/kwargs warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.") self.register(the_reader()) diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index be1e78db8a..1273b0ce0d 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -87,6 +87,7 @@ def __init__( prune_meta_sep: str = ".", allow_missing_keys: bool = False, expanduser: bool = True, + raise_on_missing_reader: bool = False, *args, **kwargs, ) -> None: @@ -123,6 +124,8 @@ def __init__( e.g. ``prune_meta_pattern=".*_code$", prune_meta_sep=" "`` removes meta keys that ends with ``"_code"``. allow_missing_keys: don't raise exception if key is missing. expanduser: if True cast filename to Path and call .expanduser on it, otherwise keep filename as is. + raise_on_missing_reader: if True, raise OptionalImportError when a specified reader is not available, + otherwise attempt to use fallback readers. Default is False to maintain backward compatibility. args: additional parameters for reader if providing a reader name. kwargs: additional parameters for reader if providing a reader name. """ @@ -136,6 +139,7 @@ def __init__( prune_meta_pattern, prune_meta_sep, expanduser, + raise_on_missing_reader, *args, **kwargs, ) diff --git a/tests/transforms/test_load_image.py b/tests/transforms/test_load_image.py index 930a18f2ee..30d34b37bc 100644 --- a/tests/transforms/test_load_image.py +++ b/tests/transforms/test_load_image.py @@ -28,7 +28,7 @@ from monai.data.meta_obj import set_track_meta from monai.data.meta_tensor import MetaTensor from monai.transforms import LoadImage -from monai.utils import optional_import +from monai.utils import OptionalImportError, optional_import from tests.test_utils import SkipIfNoModule, assert_allclose, skip_if_downloading_fails, testing_data_config itk, has_itk = optional_import("itk", allow_namespace_pkg=True) @@ -436,12 +436,37 @@ def test_my_reader(self): self.assertEqual(out.meta["name"], "my test") out = LoadImage(image_only=True, reader=_MiniReader, is_compatible=False)("test") self.assertEqual(out.meta["name"], "my test") + + def test_reader_not_installed_exception(self): + """test if an exception is raised when a specified reader is not installed""" + with self.assertRaises(OptionalImportError): + LoadImage(image_only=True, reader="NonExistentReader")("test") for item in (_MiniReader, _MiniReader(is_compatible=False)): out = LoadImage(image_only=True, reader=item)("test") self.assertEqual(out.meta["name"], "my test") out = LoadImage(image_only=True)("test", reader=_MiniReader(is_compatible=False)) self.assertEqual(out.meta["name"], "my test") + def test_raise_on_missing_reader_flag(self): + """test raise_on_missing_reader flag behavior""" + # Test with flag enabled - should raise exception for unknown reader name + with self.assertRaises(OptionalImportError): + LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=True) + + # Test with flag disabled (default) - should also raise exception for unknown reader name + # because this is caught before the new flag logic + with self.assertRaises(OptionalImportError): + LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=False) + + # The flag primarily helps when reader is recognized but dependencies are missing + # Since we're in an ITK test environment, let's just verify the flag exists and doesn't break normal behavior + loader_with_flag = LoadImage(image_only=True, reader="ITKReader", raise_on_missing_reader=False) + loader_without_flag = LoadImage(image_only=True, reader="ITKReader") + + # Both should work since ITK is available in this test environment + self.assertIsInstance(loader_with_flag, LoadImage) + self.assertIsInstance(loader_without_flag, LoadImage) + def test_itk_meta(self): """test metadata from a directory""" out = LoadImage(image_only=True, reader="ITKReader", pixel_type=itk_uc, series_meta=True)(