Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
23 changes: 18 additions & 5 deletions monai/transforms/io/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,28 @@ def __init__(
the_reader = look_up_option(_r.lower(), SUPPORTED_READERS)
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:
raise RuntimeError(
f"The required package for reader '{_r}' is not installed, or the version doesn't match "
f"the requirement. If you want to use '{_r}', please install the required package. "
f"If you want to use an alternative reader, do not specify the `reader` argument."
) from e
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())
elif inspect.isclass(_r):
self.register(_r(*args, **kwargs))
try:
self.register(_r(*args, **kwargs))
except OptionalImportError as e:
reader_name = getattr(_r, "__name__", str(_r))
raise RuntimeError(
f"The required package for reader '{reader_name}' is not installed, or the version doesn't match "
f"the requirement. If you want to use '{reader_name}', please install the required package. "
f"If you want to use an alternative reader, do not specify the `reader` argument."
) from e
except TypeError: # the reader doesn't have the corresponding args/kwargs
warnings.warn(f"{_r.__name__} is not supported with the given parameters {args} {kwargs}.")
self.register(_r())
else:
self.register(_r) # reader instance, ignoring the constructor args/kwargs
return
Expand Down
6 changes: 3 additions & 3 deletions tests/data/test_init_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ def test_load_image(self):
self.assertIsInstance(instance1, LoadImage)
self.assertIsInstance(instance2, LoadImage)

for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", "PydicomReader", None]:
inst = LoadImaged("image", reader=r)
self.assertIsInstance(inst, LoadImaged)
# Test with None (auto-select) - should always work
inst = LoadImaged("image", reader=None)
self.assertIsInstance(inst, LoadImaged)

@SkipIfNoModule("nibabel")
@SkipIfNoModule("cupy")
Expand Down
68 changes: 67 additions & 1 deletion tests/transforms/test_load_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

import nibabel as nib
import numpy as np
Expand All @@ -28,7 +29,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 @@ -498,5 +499,70 @@ def test_correct(self, input_param, expected_shape, track_meta):
self.assertFalse(hasattr(r, "affine"))




class TestLoadImageMissingReader(unittest.TestCase):
"""Test that LoadImage raises RuntimeError when a user-specified reader is not installed."""

def test_explicit_reader_not_installed_raises_runtime_error(self):
"""When the user explicitly names a reader whose package is missing, a RuntimeError must be raised."""
# Patch the reader class so that instantiation raises OptionalImportError,
# simulating a missing optional dependency (e.g. itk not installed).
with patch("monai.data.ITKReader.__init__", side_effect=OptionalImportError("itk")):
with self.assertRaises(RuntimeError) as ctx:
LoadImage(reader="ITKReader")
self.assertIn("ITKReader", str(ctx.exception))
self.assertIn("not installed", str(ctx.exception))

def test_explicit_class_reader_not_installed_raises_runtime_error(self):
"""When user passes a class reader whose package is missing, RuntimeError is raised (not OptionalImportError)."""
# This tests the class path (not string path) to ensure consistent behavior
with patch("monai.data.ITKReader.__init__", side_effect=OptionalImportError("itk")):
with self.assertRaises(RuntimeError) as ctx:
LoadImage(reader=ITKReader)
self.assertIn("ITKReader", str(ctx.exception))
self.assertIn("not installed", str(ctx.exception))

def test_unspecified_reader_falls_back_silently(self):
"""When no reader is specified, missing optional readers should be silently skipped (no exception)."""
# Force the fallback path by simulating missing optional dependencies.
# Patch the constructor to raise OptionalImportError for some readers,
# then verify LoadImage still instantiates and logs warnings.
from monai.utils import OptionalImportError

# Patch SUPPORTED_READERS entries to raise OptionalImportError
# This simulates optional packages not being installed
from monai.transforms.io.array import SUPPORTED_READERS

# Patch a few readers to fail (e.g., ITKReader)
try:
original_itk = SUPPORTED_READERS.get("itk")

def failing_reader(*args, **kwargs):
raise OptionalImportError("itk not installed")

# Temporarily replace ITKReader with a failing version
SUPPORTED_READERS["itk"] = failing_reader

# Capture log output to verify warn-and-skip was invoked
with self.assertLogs("LoadImage", level="DEBUG") as cm:
loader = LoadImage()
self.assertIsInstance(loader, LoadImage)

# Verify we got the expected debug log about skipping the missing reader
self.assertTrue(any("not installed" in msg for msg in cm.output),
f"Expected 'not installed' in debug logs, got: {cm.output}")
finally:
# Restore original reader
if original_itk is not None:
SUPPORTED_READERS["itk"] = original_itk

def test_explicit_reader_available_succeeds(self):
"""When the user explicitly names a reader whose package IS installed, no exception is raised."""
# NibabelReader is always available (nibabel is a core dep)
loader = LoadImage(reader="NibabelReader")
self.assertIsInstance(loader, LoadImage)


if __name__ == "__main__":
unittest.main()
Loading