Skip to content

Raise exception for uninstalled LoadImage reader and print writer pkg…#8915

Open
VedhSontha wants to merge 8 commits into
Project-MONAI:devfrom
VedhSontha:fix-reader-exceptions-and-writer-suggestions
Open

Raise exception for uninstalled LoadImage reader and print writer pkg…#8915
VedhSontha wants to merge 8 commits into
Project-MONAI:devfrom
VedhSontha:fix-reader-exceptions-and-writer-suggestions

Conversation

@VedhSontha

Copy link
Copy Markdown

… suggestion

Fixes # .

Description

A few sentences describing the changes proposed in this pull request.

Types of changes

  • Non-breaking change (fix or new feature that would not break existing functionality).
  • Breaking change (fix or new feature that would cause existing functionality to change).
  • New tests added to cover the changes.
  • Integration tests passed locally by running ./runtests.sh -f -u --net --coverage.
  • Quick tests passed locally by running ./runtests.sh --quick --unittests --disttests.
  • In-line docstrings updated.
  • Documentation updated, tested make html command in the docs/ folder.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR updates optional-dependency handling for image writer resolution and explicit reader construction, adds warnings and clearer errors for unsafe archive extraction paths and links, and changes two README lines.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is mostly placeholder text and lacks a real summary of the changes. Replace the template text with a concrete description of the reader and writer error-handling changes, and note any tests or docs updated.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main changes: LoadImage reader errors and writer package suggestions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/data/test_image_rw.py (1)

142-153: ⚡ Quick win

Make the missing-backend path deterministic.

Lines 144-153 only exercise the new OptionalImportError message when both nibabel and itk are absent. In most environments at least one is installed, so the new diagnostic path can regress without any test coverage. Isolate this branch with a temporary registry or mocked writer constructors instead of ambient package state.

As per coding guidelines, "Ensure new or modified definitions will be covered by existing or new unit tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/data/test_image_rw.py` around lines 142 - 153, The current test relies
on ambient package presence; make the missing-backend path deterministic by
mocking or isolating the registry so resolve_writer and OptionalImportError are
exercised reliably: in the test for the "else" branch, patch optional_import (or
monkeypatch the module that defines optional_import) to return (_, False) for
both "nibabel" and "itk", or temporarily replace the writer registry looked up
by resolve_writer with an empty/mock registry, then call resolve_writer(".nrrd")
and resolve_writer("unknown") to assert OptionalImportError contains "Please
install: itk or nibabel."; ensure you restore the original state after the
assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@monai/transforms/io/array.py`:
- Line 217: The warning call in LoadImage.__init__ currently uses
warnings.warn(f"{_r} is not supported with the given parameters {args}
{kwargs}.") which points the stack to MONAI internals; update this call to pass
stacklevel=2 so the emitted warning points to the user's call site (i.e., change
the warnings.warn call inside the LoadImage.__init__ method to include
stacklevel=2).

In `@tests/data/test_init_reader.py`:
- Around line 47-53: TestInitLoadImage.test_load_image currently calls
optional_import(pkg_map[r]) (defaulting allow_namespace_pkg=False) to set
has_pkg, but runtime code imports ITK (and NRRD) with optional_import(...,
allow_namespace_pkg=True), causing false negatives; update the test to call
optional_import(pkg_map[r], allow_namespace_pkg=True) when probing readers for
"itk" and "nrrd" (or set allow_namespace_pkg=True for those pkg_map entries) so
has_pkg matches the runtime import behavior used by LoadImaged and the
OptionalImportError expectation is correct.

---

Nitpick comments:
In `@tests/data/test_image_rw.py`:
- Around line 142-153: The current test relies on ambient package presence; make
the missing-backend path deterministic by mocking or isolating the registry so
resolve_writer and OptionalImportError are exercised reliably: in the test for
the "else" branch, patch optional_import (or monkeypatch the module that defines
optional_import) to return (_, False) for both "nibabel" and "itk", or
temporarily replace the writer registry looked up by resolve_writer with an
empty/mock registry, then call resolve_writer(".nrrd") and
resolve_writer("unknown") to assert OptionalImportError contains "Please
install: itk or nibabel."; ensure you restore the original state after the
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 056e8222-bb33-4ffe-899d-a967383763a4

📥 Commits

Reviewing files that changed from the base of the PR and between eccefc5 and 5673b52.

📒 Files selected for processing (4)
  • monai/data/image_writer.py
  • monai/transforms/io/array.py
  • tests/data/test_image_rw.py
  • tests/data/test_init_reader.py

)
) 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}.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and show the relevant section around line 217
ls -la monai/transforms/io/array.py
python3 - <<'PY'
import itertools, pathlib
p = pathlib.Path("monai/transforms/io/array.py")
lines = p.read_text().splitlines()
start, end = 200, 240
for i in range(start-1, min(end, len(lines))):
    print(f"{i+1:4d}: {lines[i]}")
PY

# Find all warnings.warn occurrences in this file
rg -n "warnings\.warn\s*\(" monai/transforms/io/array.py

Repository: Project-MONAI/MONAI

Length of output: 2624


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import pathlib
p = pathlib.Path("monai/transforms/io/array.py")
lines = p.read_text().splitlines()
# show more context to identify surrounding def/class
for i in range(150, 230):
    if i < 0 or i >= len(lines): 
        continue
    print(f"{i+1:4d}: {lines[i]}")
PY

Repository: Project-MONAI/MONAI

Length of output: 5311


Add stacklevel=2 to the warning in LoadImage.__init__.

monai/transforms/io/array.py line 217 uses warnings.warn(...) without stacklevel, so the warning location points at MONAI internals instead of the user call site.

Suggested change
-                    warnings.warn(f"{_r} is not supported with the given parameters {args} {kwargs}.")
+                    warnings.warn(
+                        f"{_r} is not supported with the given parameters {args} {kwargs}.",
+                        stacklevel=2,
+                    )
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 217-217: No explicit stacklevel keyword argument found

Set stacklevel=2

(B028)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/transforms/io/array.py` at line 217, The warning call in
LoadImage.__init__ currently uses warnings.warn(f"{_r} is not supported with the
given parameters {args} {kwargs}.") which points the stack to MONAI internals;
update this call to pass stacklevel=2 so the emitted warning points to the
user's call site (i.e., change the warnings.warn call inside the
LoadImage.__init__ method to include stacklevel=2).

Source: Linters/SAST tools

Comment on lines +47 to +53
_, has_pkg = optional_import(pkg_map[r])
if has_pkg:
inst = LoadImaged("image", reader=r)
self.assertIsInstance(inst, LoadImaged)
else:
with self.assertRaises(OptionalImportError):
LoadImaged("image", reader=r)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=py 'optional_import\("itk"' -C2

Repository: Project-MONAI/MONAI

Length of output: 5327


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact code under review
sed -n '1,140p' tests/data/test_init_reader.py | nl -ba | sed -n '35,90p'

# Find the pkg_map definition / ITKReader mapping and the exact optional_import call site
rg -n "pkg_map|ITKReader|LoadImaged|test_load_image|optional_import" tests/data/test_init_reader.py -n

# Inspect optional_import signature/docs in repo
optional_import_file="$(rg -n --type=py "def optional_import" -S . | head -n 1 | cut -d: -f1)"
echo "Found optional_import definition in: $optional_import_file"
sed -n '1,200p' "$optional_import_file" | nl -ba | sed -n '1,200p'

Repository: Project-MONAI/MONAI

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show reviewed section with line numbers (no `nl` in this sandbox)
sed -n '1,120p' tests/data/test_init_reader.py | cat -n | sed -n '35,90p'

# Locate the exact test and the ITK probe line
rg -n "TestInitLoadImage|test_load_image|pkg_map|optional_import|ITKReader|LoadImaged" tests/data/test_init_reader.py

# Find optional_import implementation and show its signature/defaults
opt_file="$(rg -n --type=py "def optional_import" -S . | head -n 1 | cut -d: -f1)"
echo "optional_import definition: $opt_file"
sed -n '1,220p' "$opt_file" | cat -n | sed -n '1,220p'

Repository: Project-MONAI/MONAI

Length of output: 13955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get line number of optional_import definition
opt_def_line="$(rg -n --type=py "^def optional_import\\b" monai/utils/module.py | head -n 1 | cut -d: -f1)"
echo "optional_import def line: $opt_def_line"

# Print a window around the function start (signature + docstring header)
start=$((opt_def_line-20))
end=$((opt_def_line+120))
sed -n "${start},${end}p" monai/utils/module.py | cat -n

# Also find any occurrences of allow_namespace_pkg inside the function/file
rg -n "allow_namespace_pkg" monai/utils/module.py | head -n 50

Repository: Project-MONAI/MONAI

Length of output: 7091


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where ITKReader is resolved/used during LoadImaged/LoadImage
rg -n "ITKReader" monai -S
rg -n "LoadImaged\\(|LoadImage\\(" monai/transforms -S
rg -n "reader=.*ITKReader|resolve_reader|resolve_reader\\(|image_reader" monai -S

# Inspect relevant reader resolution code (likely monai/data/image_reader.py and monai/data/__init__.py / transforms loader)
sed -n '1,220p' monai/data/image_reader.py | cat -n

# Also inspect image_writer if needed (for writer mapping consistency)
sed -n '1,140p' monai/data/image_writer.py | cat -n

Repository: Project-MONAI/MONAI

Length of output: 21170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type=py "^def require_pkg\\b" monai/utils/module.py
req_file="monai/utils/module.py"

req_line="$(rg -n --type=py "^def require_pkg\\b" "$req_file" | head -n 1 | cut -d: -f1)"
echo "require_pkg def line: $req_line"

start=$((req_line-20))
end=$((req_line+220))
sed -n "${start},${end}p" "$req_file" | cat -n

# See whether allow_namespace_pkg is referenced inside require_pkg
rg -n "allow_namespace_pkg" "$req_file" | head -n 50

Repository: Project-MONAI/MONAI

Length of output: 10172


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "has_itk|\\bitk\\b" monai/data/image_reader.py

Repository: Project-MONAI/MONAI

Length of output: 1442


Handle ITK as a namespace package in TestInitLoadImage.test_load_image

tests/data/test_init_reader.py:47 probes ITK with optional_import(pkg_map[r]) (defaults allow_namespace_pkg=False), but runtime monai/data/image_reader.py loads ITK with optional_import("itk", allow_namespace_pkg=True, so namespace-style ITK installs can make has_pkg false and cause a false OptionalImportError expectation.

  • Same mismatch likely applies to nrrd (runtime also uses allow_namespace_pkg=True there, but this test doesn’t special-case it).
Suggested change
-            _, has_pkg = optional_import(pkg_map[r])
+            import_kwargs = {"allow_namespace_pkg": True} if pkg_map[r] == "itk" else {}
+            _, has_pkg = optional_import(pkg_map[r], **import_kwargs)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_, has_pkg = optional_import(pkg_map[r])
if has_pkg:
inst = LoadImaged("image", reader=r)
self.assertIsInstance(inst, LoadImaged)
else:
with self.assertRaises(OptionalImportError):
LoadImaged("image", reader=r)
import_kwargs = {"allow_namespace_pkg": True} if pkg_map[r] == "itk" else {}
_, has_pkg = optional_import(pkg_map[r], **import_kwargs)
if has_pkg:
inst = LoadImaged("image", reader=r)
self.assertIsInstance(inst, LoadImaged)
else:
with self.assertRaises(OptionalImportError):
LoadImaged("image", reader=r)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/data/test_init_reader.py` around lines 47 - 53,
TestInitLoadImage.test_load_image currently calls optional_import(pkg_map[r])
(defaulting allow_namespace_pkg=False) to set has_pkg, but runtime code imports
ITK (and NRRD) with optional_import(..., allow_namespace_pkg=True), causing
false negatives; update the test to call optional_import(pkg_map[r],
allow_namespace_pkg=True) when probing readers for "itk" and "nrrd" (or set
allow_namespace_pkg=True for those pkg_map entries) so has_pkg matches the
runtime import behavior used by LoadImaged and the OptionalImportError
expectation is correct.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
monai/apps/utils.py (1)

136-156: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Quote untrusted member paths in warnings to avoid log injection.

member_path comes straight from the archive (attacker-controlled). Logging it raw lets crafted names inject newlines/control chars into logs. Use !r to quote.

🛡️ Proposed fix
-        logger.warning(f"Unsafe path guard: Symbolic link blocked: {member_path}")
+        logger.warning(f"Unsafe path guard: Symbolic link blocked: {member_path!r}")
         raise ValueError(f"Symbolic link detected in archive: {member_path}")
     if hasattr(member, "islnk") and member.islnk():
-        logger.warning(f"Unsafe path guard: Hard link blocked: {member_path}")
+        logger.warning(f"Unsafe path guard: Hard link blocked: {member_path!r}")
         raise ValueError(f"Hard link detected in archive: {member_path}")

     member_path = os.path.normpath(member_path)

     if os.path.isabs(member_path) or ".." in member_path.split(os.sep):
-        logger.warning(f"Unsafe path guard: Absolute/relative path traversal blocked: {member_path}")
+        logger.warning(f"Unsafe path guard: Absolute/relative path traversal blocked: {member_path!r}")
         raise ValueError(f"Unsafe path detected in archive: {member_path}")

And likewise at Line 155:

-        logger.warning(f"Unsafe path guard: Out-of-bounds path traversal blocked: {member_path}")
+        logger.warning(f"Unsafe path guard: Out-of-bounds path traversal blocked: {member_path!r}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/apps/utils.py` around lines 136 - 156, The unsafe path guard in utils
logs attacker-controlled member paths directly, which can allow log injection.
Update the warning calls in the archive extraction path to quote member_path
using a repr-style format so control characters and newlines are escaped. Apply
this consistently in the same guard logic that handles symbolic links, hard
links, path traversal, and out-of-bounds checks, using the existing
logger.warning statements and the member_path variable.
🧹 Nitpick comments (1)
monai/apps/utils.py (1)

125-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Expand the docstring to Google style.

The single-line docstring omits the now-relevant Raises: ValueError, plus Args/Returns. As per path instructions, docstrings should describe each variable, return value, and raised exception in Google-style sections.

📝 Proposed docstring
 def safe_extract_member(member, extract_to):
-    """Securely verify compressed package member paths to prevent path traversal attacks"""
+    """Securely verify compressed package member paths to prevent path traversal attacks.
+
+    Args:
+        member: archive entry (`zipfile.ZipInfo` or `tarfile.TarInfo`) to validate.
+        extract_to: destination root directory for extraction.
+
+    Returns:
+        The validated absolute target path within ``extract_to``.
+
+    Raises:
+        ValueError: if the member is a symbolic/hard link, or its path is absolute,
+            relative, or otherwise resolves outside ``extract_to``.
+    """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/apps/utils.py` around lines 125 - 146, The docstring on
safe_extract_member is too minimal for the current behavior, so expand it to
Google style and document the parameters, the return value, and the ValueError
cases raised for symbolic links, hard links, and unsafe paths. Keep the
description aligned with the existing safe_extract_member logic and clearly
describe extract_to, the member path handling, and the exception conditions in
Args, Returns, and Raises sections.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@monai/apps/utils.py`:
- Around line 136-156: The unsafe path guard in utils logs attacker-controlled
member paths directly, which can allow log injection. Update the warning calls
in the archive extraction path to quote member_path using a repr-style format so
control characters and newlines are escaped. Apply this consistently in the same
guard logic that handles symbolic links, hard links, path traversal, and
out-of-bounds checks, using the existing logger.warning statements and the
member_path variable.

---

Nitpick comments:
In `@monai/apps/utils.py`:
- Around line 125-146: The docstring on safe_extract_member is too minimal for
the current behavior, so expand it to Google style and document the parameters,
the return value, and the ValueError cases raised for symbolic links, hard
links, and unsafe paths. Keep the description aligned with the existing
safe_extract_member logic and clearly describe extract_to, the member path
handling, and the exception conditions in Args, Returns, and Raises sections.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f92135dd-b06f-4fb5-8339-f7a0606319bf

📥 Commits

Reviewing files that changed from the base of the PR and between d06b6ff and f1f0143.

📒 Files selected for processing (1)
  • monai/apps/utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (1)
monai/apps/utils.py (1)

126-126: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring lacks Args/Returns/Raises.

Given the function now raises ValueError on several paths, a Google-style docstring documenting params, return, and raised exceptions would help. As per path instructions: "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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@monai/apps/utils.py` at line 126, The docstring for the path-verification
helper is incomplete and should follow Google style. Update the docstring on the
function that checks compressed package member paths to include an Args section
for its parameters, a Returns section for the boolean/result it produces, and a
Raises section documenting the ValueError cases now emitted from this logic.
Keep the wording aligned with the function’s purpose and mention the specific
validation behavior in the docstring.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@monai/apps/utils.py`:
- Around line 154-161: The ValueError raised for a confirmed out-of-bounds
member in the path check is being caught by the same except block, which causes
double logging and misclassifies a real traversal as a drive-mismatch case. In
the utility that compares `extract_root` and `target_real` with
`os.path.commonpath`, limit the `try/except ValueError` to just the
`commonpath()` call, then handle the out-of-bounds result separately in the
surrounding logic so only genuine `commonpath()` errors hit the fallback logger
and re-raise path-traversal errors once with the correct message.

---

Nitpick comments:
In `@monai/apps/utils.py`:
- Line 126: The docstring for the path-verification helper is incomplete and
should follow Google style. Update the docstring on the function that checks
compressed package member paths to include an Args section for its parameters, a
Returns section for the boolean/result it produces, and a Raises section
documenting the ValueError cases now emitted from this logic. Keep the wording
aligned with the function’s purpose and mention the specific validation behavior
in the docstring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 40a9185a-1343-435e-81ab-91f3f82c0a2e

📥 Commits

Reviewing files that changed from the base of the PR and between 42f2353 and be02d5d.

📒 Files selected for processing (1)
  • monai/apps/utils.py

Comment thread monai/apps/utils.py
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.

1 participant