Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand All @@ -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
Expand All @@ -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:
Comment on lines +213 to 223
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Chain the original cause and set stacklevel on warnings.

Use raise ... from err (B904) and add stacklevel=2 so warnings point at user code (B028).

Apply:

-                if the_reader is None:
-                    try:
-                        the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
-                    except ValueError:
+                if the_reader is None:
+                    try:
+                        the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
+                    except ValueError as err:
                         # 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)
+                            raise OptionalImportError(msg) from err
                         else:
-                            warnings.warn(f"{msg} Will use fallback readers if available.")
+                            warnings.warn(f"{msg} Will use fallback readers if available.", stacklevel=2)
                             continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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:
if the_reader is None:
try:
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
except ValueError as err:
# 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) from err
else:
warnings.warn(f"{msg} Will use fallback readers if available.", stacklevel=2)
continue
try:
🧰 Tools
🪛 Ruff (0.12.2)

219-219: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


221-221: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)

🤖 Prompt for AI Agents
In monai/transforms/io/array.py around lines 213 to 223, the except block
swallows the original ValueError and issues warnings without stacklevel; change
the except to "except ValueError as err:" then when raising OptionalImportError
use "raise OptionalImportError(msg) from err" to chain the original cause, and
when issuing the warning call warnings.warn(f"{msg} Will use fallback readers if
available.", stacklevel=2) so the warning points at user code.

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())
Comment on lines +225 to 233
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Also set stacklevel for dependency-missing warnings.

Keep the raised error chained; just add stacklevel=2 for the warning path.

Apply:

                 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.")
+                        warnings.warn(f"{msg} Will use fallback readers if available.", stacklevel=2)
                 except TypeError:  # the reader doesn't have the corresponding args/kwargs
-                    warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.")
+                    warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.", stacklevel=2)
                     self.register(the_reader())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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())
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.", stacklevel=2)
except TypeError: # the reader doesn't have the corresponding args/kwargs
warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.", stacklevel=2)
self.register(the_reader())
🧰 Tools
🪛 Ruff (0.12.2)

230-230: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)


232-232: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)

🤖 Prompt for AI Agents
In monai/transforms/io/array.py around lines 225 to 233, the OptionalImportError
except block currently warns when readers are missing but does not set
stacklevel; keep the existing chained raise as-is, and update the warning path
to include stacklevel=2 so the warning points to the caller (i.e., call
warnings.warn(message, stacklevel=2)). Do not change the raised error chaining;
only add the stacklevel=2 argument to the warnings.warn call in the else branch.

Expand Down
4 changes: 4 additions & 0 deletions monai/transforms/io/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
"""
Expand All @@ -136,6 +139,7 @@ def __init__(
prune_meta_pattern,
prune_meta_sep,
expanduser,
raise_on_missing_reader,
*args,
**kwargs,
)
Expand Down
55 changes: 55 additions & 0 deletions test_my_changes.py
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +36 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove the blind except Exception (BLE001).

Catching all exceptions hides real failures; let unexpected errors surface.

Apply:

-    except Exception as e:
-        print(f"FAIL: Unexpected error: {e}")
-        return False
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
print(f"FAIL: Unexpected error: {e}")
return False
🧰 Tools
🪛 Ruff (0.12.2)

36-36: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In test_my_changes.py around lines 36 to 38, the code currently uses a blind
except Exception which hides unexpected failures; replace it by either removing
the try/except so exceptions propagate, or catch only the specific exception
types you expect (e.g., ValueError, AssertionError) and handle those, and for
any other exception re-raise it (or omit handling) so test runners see real
errors; update logging to record expected exception details only and ensure
unexpected exceptions are not swallowed.


# 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)
30 changes: 29 additions & 1 deletion tests/transforms/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)(
Expand Down
Loading