Skip to content

Raise exception when user-specified reader package is not installed#8771

Open
haoyu-haoyu wants to merge 1 commit intoProject-MONAI:devfrom
haoyu-haoyu:fix/raise-exception-when-specified-reader-not-installed
Open

Raise exception when user-specified reader package is not installed#8771
haoyu-haoyu wants to merge 1 commit intoProject-MONAI:devfrom
haoyu-haoyu:fix/raise-exception-when-specified-reader-not-installed

Conversation

@haoyu-haoyu
Copy link

Fixes #7437

Description

When a user explicitly specifies a reader (e.g., reader="ITKReader") but the required package is not installed, LoadImage now raises an OptionalImportError instead of silently falling back to another reader with only a warning.

Before (current behavior):

# User specifies ITKReader but itk is not installed
loader = LoadImage(reader="ITKReader")
# Only a warning is printed, then PILReader is used silently
# This can lead to unexpected results

After (new behavior):

# User specifies ITKReader but itk is not installed
loader = LoadImage(reader="ITKReader")
# OptionalImportError is raised immediately with a clear message:
# "Required package for reader ITKReader is not installed..."

Default behavior (reader=None) remains unchanged — missing optional packages are still handled gracefully with debug logging, and the system falls back to available readers as before.

Changes

  1. monai/transforms/io/array.py: Changed warnings.warn() to raise OptionalImportError() in the user-specified reader registration path (lines 212-216).

  2. tests/transforms/test_load_image.py: Added TestLoadImageReaderNotInstalled test class with:

    • test_specified_reader_not_installed_raises: Direct test (runs when itk is NOT installed)
    • test_specified_reader_not_installed_raises_mocked: Mock-based test that always runs, simulating a reader whose package is not installed

Testing

  • New unit tests added
  • Existing tests unaffected (default reader=None path unchanged)

Signed-off-by: haoyu-haoyu haoyu-haoyu@users.noreply.github.com

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Added a mapping _READER_INSTALL_HINTS mapping reader names to pip install hints. LoadImage now, when registering a user-specified reader by name and an OptionalImportError occurs, appends an installation hint (when available) to the raised OptionalImportError instead of issuing a warning and falling back. Tests: new TestLoadImageReaderNotInstalled with two tests verifying LoadImage(reader=...) raises OptionalImportError with install hint (real and mocked missing readers). tests/data/test_init_reader.py updated to import OptionalImportError and wrap LoadImaged instantiation in try/except to skip assertions when a reader backend is missing.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main change: raising an exception when a user-specified reader package is not installed.
Description check ✅ Passed Description includes issue reference, clear before/after behavior, code changes, and test coverage. Matches template structure with proper sections.
Linked Issues check ✅ Passed PR fully addresses #7437: raises OptionalImportError when user specifies a reader that isn't installed, while preserving fallback behavior for reader=None.
Out of Scope Changes check ✅ Passed All changes are scoped to the objective: exception raising in user-specified reader path, corresponding tests, and test fixture robustness for missing backends.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/transforms/test_load_image.py (2)

513-514: Assert the error text too.

These pass on any OptionalImportError, including one bubbling up from deeper in reader init. assertRaisesRegex on the reader name or install hint would lock in the new contract.

Patch
-        with self.assertRaises(OptionalImportError):
+        with self.assertRaisesRegex(OptionalImportError, r"ITKReader|install"):
             LoadImage(reader="ITKReader")
@@
-            with self.assertRaises(OptionalImportError):
+            with self.assertRaisesRegex(OptionalImportError, r"MockMissingReader|install"):
                 LoadImage(reader="MockMissingReader")

Also applies to: 537-538

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/transforms/test_load_image.py` around lines 513 - 514, The test
currently uses assertRaises(OptionalImportError) when constructing
LoadImage(reader="ITKReader"); replace it with
assertRaisesRegex(OptionalImportError, r"(ITKReader|install.*ITK|itk)") to
ensure the raised OptionalImportError specifically mentions the reader name or
the install hint; do the same change for the other occurrence
(LoadImage(reader="ITKReader") around lines 537-538) so the tests fail only when
the expected import hint is present rather than any unrelated
OptionalImportError.

501-518: Use Google-style docstrings on the new test definitions.

The docstrings are present, but they do not follow the required Google-style sections. As per coding guidelines, "Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/transforms/test_load_image.py` around lines 501 - 518, Update the
docstrings in class TestLoadImageReaderNotInstalled for the methods
test_specified_reader_not_installed_raises and
test_specified_reader_not_installed_raises_mocked to use Google-style
docstrings: include a short summary, an Args: section if the test takes
parameters (omit if none), a Returns: section (state that it returns None), and
a Raises: section documenting that OptionalImportError is expected when calling
LoadImage(reader="ITKReader"); reference the test names and the
LoadImage/OptionalImportError symbols so the intent is clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@monai/transforms/io/array.py`:
- Around line 212-216: The except block currently discards the original
OptionalImportError context; change the handler to capture the exception (e.g.,
except OptionalImportError as e:) and re-raise the new OptionalImportError with
exception chaining (raise OptionalImportError(... ) from e) so the original
traceback is preserved; reference the caught OptionalImportError and the reader
identifier _r when updating the except block in monai/transforms/io/array.py.

---

Nitpick comments:
In `@tests/transforms/test_load_image.py`:
- Around line 513-514: The test currently uses assertRaises(OptionalImportError)
when constructing LoadImage(reader="ITKReader"); replace it with
assertRaisesRegex(OptionalImportError, r"(ITKReader|install.*ITK|itk)") to
ensure the raised OptionalImportError specifically mentions the reader name or
the install hint; do the same change for the other occurrence
(LoadImage(reader="ITKReader") around lines 537-538) so the tests fail only when
the expected import hint is present rather than any unrelated
OptionalImportError.
- Around line 501-518: Update the docstrings in class
TestLoadImageReaderNotInstalled for the methods
test_specified_reader_not_installed_raises and
test_specified_reader_not_installed_raises_mocked to use Google-style
docstrings: include a short summary, an Args: section if the test takes
parameters (omit if none), a Returns: section (state that it returns None), and
a Raises: section documenting that OptionalImportError is expected when calling
LoadImage(reader="ITKReader"); reference the test names and the
LoadImage/OptionalImportError symbols so the intent is clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1553a00f-91e7-46a5-9318-e9d99eafc257

📥 Commits

Reviewing files that changed from the base of the PR and between daaedaa and 1b6730f.

📒 Files selected for processing (2)
  • monai/transforms/io/array.py
  • tests/transforms/test_load_image.py

@haoyu-haoyu haoyu-haoyu force-pushed the fix/raise-exception-when-specified-reader-not-installed branch 2 times, most recently from 255db19 to 1e6dd49 Compare March 14, 2026 02:28
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/data/test_init_reader.py`:
- Around line 30-36: The test currently swallows OptionalImportError in the
LoadImaged instantiation which can mask regressions; change the test to
explicitly assert the expected behavior instead of pass: either use
self.assertRaises(OptionalImportError, LoadImaged, "image", reader=r) to assert
the error is raised in minimal-dependency environments, or keep the try block
and in the except OptionalImportError as e branch assert something specific
about e (e.g., error message contains the backend name) so the failing branch is
tested rather than silently ignored; reference the LoadImaged constructor and
OptionalImportError in your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c0e4bb13-0db3-43af-a6f6-a6ee036899fe

📥 Commits

Reviewing files that changed from the base of the PR and between 255db19 and 1e6dd49.

📒 Files selected for processing (3)
  • monai/transforms/io/array.py
  • tests/data/test_init_reader.py
  • tests/transforms/test_load_image.py

Comment on lines +30 to +36
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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid swallowing OptionalImportError without assertions.

This except ...: pass weakens the test and can mask regressions. Assert the expected branch instead of silently continuing.

Suggested test-hardening patch
         for r in ["NibabelReader", "PILReader", "ITKReader", "NumpyReader", "NrrdReader", "PydicomReader", None]:
-            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
+            with self.subTest(reader=r):
+                try:
+                    inst = LoadImaged("image", reader=r)
+                except OptionalImportError as err:
+                    self.assertIsNotNone(r)
+                    self.assertIn(str(r), str(err))
+                else:
+                    self.assertIsInstance(inst, LoadImaged)

As per coding guidelines, "Examine code for logical error or inconsistencies, and suggest what may be changed to addressed these. Ensure new or modified definitions will be covered by existing or new unit tests."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/test_init_reader.py` around lines 30 - 36, The test currently
swallows OptionalImportError in the LoadImaged instantiation which can mask
regressions; change the test to explicitly assert the expected behavior instead
of pass: either use self.assertRaises(OptionalImportError, LoadImaged, "image",
reader=r) to assert the error is raised in minimal-dependency environments, or
keep the try block and in the except OptionalImportError as e branch assert
something specific about e (e.g., error message contains the backend name) so
the failing branch is tested rather than silently ignored; reference the
LoadImaged constructor and OptionalImportError in your change.

When a user explicitly specifies a reader (e.g., reader="ITKReader") but the
required package is not installed, LoadImage now raises an OptionalImportError
instead of silently falling back to another reader with only a warning.

This makes the behavior explicit: if the user specifically requested a reader,
they should be informed immediately that it cannot be used, rather than having
the system silently use a different reader which may produce unexpected results.

Default behavior (reader=None) remains unchanged — missing optional packages
are still handled gracefully with debug logging.

Fixes Project-MONAI#7437

Signed-off-by: haoyu-haoyu <haoyu-haoyu@users.noreply.github.com>
Signed-off-by: SexyERIC0723 <haoyuwang144@gmail.com>
@haoyu-haoyu haoyu-haoyu force-pushed the fix/raise-exception-when-specified-reader-not-installed branch from 1e6dd49 to 8024900 Compare March 16, 2026 23:24
@haoyu-haoyu
Copy link
Author

Updated with improved error messages:

Before (v1):

OptionalImportError: Required package for reader NibabelReader is not installed...

The user knows which reader failed but not which pip package to install.

After (v2):

OptionalImportError: required package `nibabel` is not installed or the version doesn't match requirement. Install with: pip install nibabel

Changes:

  1. Preserved the original error messagestr(e) already contains the package name (e.g. nibabel), so we prepend it instead of overwriting
  2. Added _READER_INSTALL_HINTS mapping — reader name → pip install command for all built-in readers (NibabelReader, ITKReader, PILReader, PydicomReader, NrrdReader)
  3. Updated tests to verify the install hint appears in the error and the original message is preserved

This also partially addresses #7980 (which requests install hints for writers) — the same pattern could be applied there.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/transforms/test_load_image.py`:
- Around line 501-541: Add Google-style docstrings for the newly introduced
nested definitions: the function _mock_optional_import, the class _Unavailable,
and its __init__ method so they document purpose, parameters, return values, and
the raised OptionalImportError; update _mock_optional_import to include a short
description, args (module, name, *args, **kwargs), and returns (callable, bool)
and _Unavailable.__init__ to include an Args section and a Raises section noting
OptionalImportError("mock package is not installed"); reference these exact
names (_mock_optional_import, _Unavailable, and _Unavailable.__init__) when
adding the docstrings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ccc0f226-bec7-40fb-85a8-aaafd6f376d9

📥 Commits

Reviewing files that changed from the base of the PR and between 1e6dd49 and 8024900.

📒 Files selected for processing (3)
  • monai/transforms/io/array.py
  • tests/data/test_init_reader.py
  • tests/transforms/test_load_image.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/data/test_init_reader.py
  • monai/transforms/io/array.py

Comment on lines +501 to +541
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))

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Docstring guideline is only partially satisfied for newly added definitions.

New definitions in this block are missing full Google-style docstrings (notably nested _mock_optional_import, _Unavailable, and _Unavailable.__init__), including explicit raised-exception documentation.

As per coding guidelines, "Docstrings should be present for all definition which describe each variable, return value, and raised exception in the appropriate section of the Google-style of docstrings."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/transforms/test_load_image.py` around lines 501 - 541, Add Google-style
docstrings for the newly introduced nested definitions: the function
_mock_optional_import, the class _Unavailable, and its __init__ method so they
document purpose, parameters, return values, and the raised OptionalImportError;
update _mock_optional_import to include a short description, args (module, name,
*args, **kwargs), and returns (callable, bool) and _Unavailable.__init__ to
include an Args section and a Raises section noting OptionalImportError("mock
package is not installed"); reference these exact names (_mock_optional_import,
_Unavailable, and _Unavailable.__init__) when adding the docstrings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Raise the exception when LoadImage has a reader specified but it is not installed

1 participant