diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 0628a7fbc4..3816055640 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -70,6 +70,16 @@ "nibabelreader": NibabelReader, } +# Maps reader names (lower-cased) to pip install commands so error messages +# can tell users exactly how to fix a missing-package error. +_READER_INSTALL_HINTS: dict[str, str] = { + "nibabelreader": "pip install nibabel", + "itkreader": "pip install itk", + "pilreader": "pip install Pillow", + "pydicomreader": "pip install pydicom", + "nrrdreader": "pip install pynrrd", +} + def switch_endianness(data, new="<"): """ @@ -209,10 +219,10 @@ 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: + hint = _READER_INSTALL_HINTS.get(_r.lower(), "") + install_msg = f" Install with: {hint}" if hint else "" + raise OptionalImportError(f"{e}{install_msg}") 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()) diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 4170412207..7728650609 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -15,6 +15,7 @@ from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader from monai.transforms import LoadImage, LoadImaged +from monai.utils import OptionalImportError from tests.test_utils import SkipIfNoModule @@ -26,8 +27,13 @@ def test_load_image(self): self.assertIsInstance(instance2, LoadImage) for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", "PydicomReader", None]: - inst = LoadImaged("image", reader=r) - self.assertIsInstance(inst, LoadImaged) + try: + inst = LoadImaged("image", reader=r) + self.assertIsInstance(inst, LoadImaged) + except OptionalImportError: + # Reader's backend package is not installed — expected in + # minimal-dependency environments after the fix for #7437. + pass @SkipIfNoModule("nibabel") @SkipIfNoModule("cupy") diff --git a/tests/transforms/test_load_image.py b/tests/transforms/test_load_image.py index 031e38272e..495d899fc4 100644 --- a/tests/transforms/test_load_image.py +++ b/tests/transforms/test_load_image.py @@ -498,5 +498,47 @@ def test_correct(self, input_param, expected_shape, track_meta): self.assertFalse(hasattr(r, "affine")) +class TestLoadImageReaderNotInstalled(unittest.TestCase): + """Test that specifying a reader whose required package is not installed raises an error. + + Addresses https://github.com/Project-MONAI/MONAI/issues/7437 + """ + + @unittest.skipIf(has_itk, "test requires itk to NOT be installed") + def test_specified_reader_not_installed_raises(self): + """When a user explicitly specifies a reader that is not installed, LoadImage should raise + an OptionalImportError with an install hint instead of silently falling back.""" + from monai.utils import OptionalImportError + + with self.assertRaises(OptionalImportError) as ctx: + LoadImage(reader="ITKReader") + self.assertIn("pip install itk", str(ctx.exception)) + + def test_specified_reader_not_installed_raises_mocked(self): + """Mock test to verify OptionalImportError is raised with original message preserved + when a user-specified reader's required package is not installed.""" + from unittest.mock import patch + + from monai.utils import OptionalImportError + + _original = __import__("monai.transforms.io.array", fromlist=["optional_import"]).optional_import + + def _mock_optional_import(module, name="", *args, **kwargs): + if name == "MockMissingReader": + + class _Unavailable: + def __init__(self, *a, **kw): + raise OptionalImportError("mock package is not installed") + + return _Unavailable, True + return _original(module, *args, name=name, **kwargs) + + with patch("monai.transforms.io.array.optional_import", side_effect=_mock_optional_import): + with self.assertRaises(OptionalImportError) as ctx: + LoadImage(reader="MockMissingReader") + # Original error message should be preserved in the re-raised exception + self.assertIn("mock package is not installed", str(ctx.exception)) + + if __name__ == "__main__": unittest.main()