Skip to content

Commit 1a165c9

Browse files
authored
Raise OptionalImportError for unavailable explicit image readers (#9006)
### Description Fixes #7437. When `LoadImage` is given an explicit reader string whose optional dependency is unavailable, the original `OptionalImportError` is now propagated instead of being converted into a warning and silently falling back to another registered reader. Automatic reader selection with `reader=None` remains unchanged. ### Implementation - Propagate `OptionalImportError` for explicitly requested unavailable string readers. - Add an environment-independent regression test using mock readers. - Update reader initialization tests to reflect the new explicit-reader behavior when optional dependencies are unavailable. ### Compatibility This is an intentional behavior change for explicitly requested unavailable readers. The following behavior remains unchanged: - automatic reader selection with `reader=None` - default registration skipping unavailable optional readers - runtime fallback when an installed reader cannot read a file - public APIs and signatures This PR does not redesign explicit tuple/list reader semantics. ### Validation Executed locally: - `python -m tests.transforms.test_load_image` - `python -m tests.transforms.test_load_imaged` - `python -m tests.data.test_init_reader` - Ruff All executed tests passed. Some optional-backend tests were skipped as expected in the current environment. --------- Signed-off-by: Minsu Kim <minsu.kim08@gmail.com>
1 parent faccb5a commit 1a165c9

3 files changed

Lines changed: 78 additions & 9 deletions

File tree

monai/transforms/io/array.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,7 @@ def __init__(
210210
try:
211211
self.register(the_reader(*args, **kwargs))
212212
except OptionalImportError:
213-
warnings.warn(
214-
f"required package for reader {_r} is not installed, or the version doesn't match requirement."
215-
)
213+
raise
216214
except TypeError: # the reader doesn't have the corresponding args/kwargs
217215
warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.")
218216
self.register(the_reader())

tests/data/test_init_reader.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader
2121
from monai.transforms import LoadImage, LoadImaged
22-
from monai.utils import MetaKeys
22+
from monai.utils import MetaKeys, OptionalImportError, optional_import
2323
from tests.test_utils import SkipIfNoModule
2424

2525

@@ -30,9 +30,27 @@ def test_load_image(self):
3030
self.assertIsInstance(instance1, LoadImage)
3131
self.assertIsInstance(instance2, LoadImage)
3232

33-
for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", "PydicomReader", None]:
34-
inst = LoadImaged("image", reader=r)
35-
self.assertIsInstance(inst, LoadImaged)
33+
optional_readers = {
34+
"NibabelReader": "nibabel",
35+
"PILReader": "PIL",
36+
"ITKReader": "itk",
37+
"NrrdReader": "nrrd",
38+
"PydicomReader": "pydicom",
39+
}
40+
for r, module in optional_readers.items():
41+
with self.subTest(reader=r):
42+
_, has_module = optional_import(module, allow_namespace_pkg=module in ("itk", "nrrd"))
43+
if has_module:
44+
inst = LoadImaged("image", reader=r)
45+
self.assertIsInstance(inst, LoadImaged)
46+
else:
47+
with self.assertRaises(OptionalImportError):
48+
LoadImaged("image", reader=r)
49+
50+
inst = LoadImaged("image", reader="NumpyReader")
51+
self.assertIsInstance(inst, LoadImaged)
52+
inst = LoadImaged("image", reader=None)
53+
self.assertIsInstance(inst, LoadImaged)
3654

3755
@SkipIfNoModule("nibabel")
3856
@SkipIfNoModule("cupy")

tests/transforms/test_load_image.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
import shutil
1616
import tempfile
1717
import unittest
18+
import warnings
1819
from pathlib import Path
20+
from unittest.mock import patch
1921

2022
import nibabel as nib
2123
import numpy as np
@@ -24,11 +26,11 @@
2426
from PIL import Image
2527

2628
from monai.apps import download_and_extract
27-
from monai.data import NibabelReader, PydicomReader
29+
from monai.data import ImageReader, NibabelReader, PydicomReader
2830
from monai.data.meta_obj import get_track_meta, set_track_meta
2931
from monai.data.meta_tensor import MetaTensor
3032
from monai.transforms import LoadImage
31-
from monai.utils import optional_import
33+
from monai.utils import OptionalImportError, optional_import
3234
from tests.test_utils import SkipIfNoModule, assert_allclose, skip_if_downloading_fails, testing_data_config
3335

3436
itk, has_itk = optional_import("itk", allow_namespace_pkg=True)
@@ -52,6 +54,38 @@ def get_data(self, _obj):
5254
return np.zeros((1, 1, 1)), {"name": "my test"}
5355

5456

57+
class _MissingDependencyReader(ImageReader):
58+
"""a test reader that simulates a missing optional dependency"""
59+
60+
def __init__(self):
61+
raise OptionalImportError("mock missing dependency")
62+
63+
def verify_suffix(self, _filename):
64+
return True
65+
66+
def read(self, _data, **_kwargs):
67+
return None
68+
69+
def get_data(self, _img):
70+
return np.zeros((1, 1)), {}
71+
72+
73+
class _FallbackReader(ImageReader):
74+
"""a test reader that should not be used after an explicit reader import failure"""
75+
76+
read_called = False
77+
78+
def verify_suffix(self, _filename):
79+
return True
80+
81+
def read(self, data, **_kwargs):
82+
type(self).read_called = True
83+
return data
84+
85+
def get_data(self, _img):
86+
return np.zeros((1, 1)), {"name": "fallback"}
87+
88+
5589
TEST_CASE_1 = [{}, ["test_image.nii.gz"], (128, 128, 128)]
5690

5791
TEST_CASE_2 = [{}, ["test_image.nii.gz"], (128, 128, 128)]
@@ -184,6 +218,25 @@ def get_data(self, _obj):
184218
TESTS_META.append([{"reader": "ITKReader", "fallback_only": False}, (128, 128, 128), track_meta])
185219

186220

221+
class TestLoadImageReaderSelection(unittest.TestCase):
222+
def test_explicit_string_reader_missing_dependency_raises(self):
223+
"""test explicitly requested string readers don't fall back when their dependency is missing"""
224+
_FallbackReader.read_called = False
225+
readers = {"missingreader": _MissingDependencyReader, "fallbackreader": _FallbackReader}
226+
with patch("monai.transforms.io.array.SUPPORTED_READERS", readers):
227+
loader = LoadImage()
228+
self.assertEqual(len(loader.readers), 1)
229+
self.assertIsInstance(loader.readers[0], _FallbackReader)
230+
231+
with warnings.catch_warnings(record=True) as caught:
232+
warnings.simplefilter("always")
233+
with self.assertRaises(OptionalImportError):
234+
LoadImage(reader="missingreader")
235+
236+
self.assertEqual(len(caught), 0)
237+
self.assertFalse(_FallbackReader.read_called)
238+
239+
187240
@unittest.skipUnless(has_itk, "itk not installed")
188241
class TestLoadImage(unittest.TestCase):
189242
@classmethod

0 commit comments

Comments
 (0)