diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 49b0665a90..6fca972edf 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,24 @@ 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 + msg = f"Cannot find reader '{_r}'. It may not be installed or recognized." + if self.raise_on_missing_reader: + raise OptionalImportError(msg) + else: + warnings.warn(f"{msg} Will use fallback readers if available.") + continue 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: + msg = f"Required package for reader {_r} is not installed, or the version doesn't match requirement." + if self.raise_on_missing_reader: + raise OptionalImportError(msg) from e + else: + warnings.warn(f"{msg} 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/test_my_changes.py b/test_my_changes.py new file mode 100644 index 0000000000..5fe9338142 --- /dev/null +++ b/test_my_changes.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +"""Test to verify the changes to LoadImage raise_on_missing_reader flag work correctly.""" + +import warnings +from monai.transforms import LoadImage +from monai.utils import OptionalImportError + +def test_raise_on_missing_reader(): + """Test the raise_on_missing_reader flag behavior.""" + print("Testing LoadImage raise_on_missing_reader flag...") + + # Test 1: Unknown reader with flag enabled - should raise OptionalImportError + print("Test 1: Unknown reader with raise_on_missing_reader=True") + try: + LoadImage(reader="UnknownReader", raise_on_missing_reader=True) + print("FAIL: Expected OptionalImportError but didn't get one") + return False + except OptionalImportError as e: + print(f"PASS: Got expected OptionalImportError: {e}") + + # Test 2: Unknown reader with flag disabled - should warn but not raise + print("\nTest 2: Unknown reader with raise_on_missing_reader=False") + try: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + loader = LoadImage(reader="UnknownReader", raise_on_missing_reader=False) + if w and "UnknownReader" in str(w[0].message): + print(f"PASS: Got expected warning: {w[0].message}") + else: + print("WARNING: Warning message may have been different than expected") + print("PASS: LoadImage instance created successfully with fallback behavior") + except OptionalImportError as e: + print(f"FAIL: Unexpected OptionalImportError with flag disabled: {e}") + return False + except Exception as e: + print(f"FAIL: Unexpected error: {e}") + return False + + # Test 3: Valid reader - should work regardless of flag + print("\nTest 3: Valid reader (PILReader) with both flag settings") + try: + loader1 = LoadImage(reader="PILReader", raise_on_missing_reader=True) + loader2 = LoadImage(reader="PILReader", raise_on_missing_reader=False) + print("PASS: Both loaders created successfully with valid reader") + except Exception as e: + print(f"FAIL: Unexpected error with valid reader: {e}") + return False + + print("\nAll tests passed!") + return True + +if __name__ == "__main__": + success = test_raise_on_missing_reader() + exit(0 if success else 1) diff --git a/tests/transforms/test_load_image.py b/tests/transforms/test_load_image.py index 930a18f2ee..4784fe08f1 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,40 @@ 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 - should warn but not raise exception for unknown reader name + # This should succeed and create the loader with fallback behavior + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + loader_with_fallback = LoadImage(image_only=True, reader="UnknownReaderName", raise_on_missing_reader=False) + self.assertIsInstance(loader_with_fallback, LoadImage) + # Should have produced a warning about the unknown reader + self.assertTrue(any("Cannot find reader 'UnknownReaderName'" in str(warning.message) for warning in w)) + + # The flag should work properly with valid readers too + 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)(