Skip to content

Fix LoadImage to raise OptionalImportError when specified reader is not available #8522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
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
26 changes: 21 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,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(
Copy link
Member

Choose a reason for hiding this comment

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

I think we need if self.raise_on_missing_reader here as well.

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())
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
27 changes: 26 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,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)(
Expand Down
Loading