Skip to content
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
22 changes: 20 additions & 2 deletions monai/data/image_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@

SUPPORTED_WRITERS: dict = {}

# Map import names to their corresponding Python distribution names where they differ.
_PACKAGE_INSTALL_NAMES = {"PIL": "pillow"}


def register_writer(ext_name, *im_writers):
"""
Expand Down Expand Up @@ -99,24 +102,39 @@ def resolve_writer(ext_name, error_if_not_found=True) -> Sequence:
As an indexing key it will be converted to a lower case string.
error_if_not_found: whether to raise an error if no suitable image writer is found.
if True , raise an ``OptionalImportError``, otherwise return an empty tuple. Default is ``True``.

Raises:
OptionalImportError: When no registered writer is available. If candidate writers require missing optional
dependencies, the error includes commands for installing them.
"""
if not SUPPORTED_WRITERS:
init()
fmt = f"{ext_name}".lower()
if fmt.startswith("."):
fmt = fmt[1:]
avail_writers = []
missing_packages: set[str] = set()
default_writers = SUPPORTED_WRITERS.get(EXT_WILDCARD, ())
for _writer in look_up_option(fmt, SUPPORTED_WRITERS, default=default_writers):
try:
_writer() # this triggers `monai.utils.module.require_pkg` to check the system availability
avail_writers.append(_writer)
except OptionalImportError:
except OptionalImportError as error:
if error.name:
missing_packages.add(_PACKAGE_INSTALL_NAMES.get(error.name, error.name))
continue
except Exception: # other writer init errors indicating it exists
avail_writers.append(_writer)
if not avail_writers and error_if_not_found:
raise OptionalImportError(f"No ImageWriter backend found for {fmt}.")
message = f"No ImageWriter backend found for {fmt}."
packages = sorted(missing_packages)
if len(packages) == 1:
package = packages[0]
message += f" Install `{package}` with `pip install {package}`."
elif packages:
hints = ", ".join(f"`{package}` (`pip install {package}`)" for package in packages)
message += f" Install one of the following packages: {hints}."
raise OptionalImportError(message)
writer_tuple = ensure_tuple(avail_writers)
SUPPORTED_WRITERS[fmt] = writer_tuple
return writer_tuple
Expand Down
2 changes: 1 addition & 1 deletion monai/utils/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ def _wrapper(*args, **kwargs):
if not has:
err_msg = f"required package `{pkg_name}` is not installed or the version doesn't match requirement."
if raise_error:
raise OptionalImportError(err_msg)
raise OptionalImportError(err_msg, name=pkg_name)
else:
warnings.warn(err_msg)

Expand Down
40 changes: 39 additions & 1 deletion tests/data/test_image_rw.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
import shutil
import tempfile
import unittest
from unittest.mock import patch

import numpy as np
import torch
from parameterized import parameterized

from monai.data.image_reader import ITKReader, NibabelReader, NrrdReader, PILReader
from monai.data.image_writer import ITKWriter, NibabelWriter, PILWriter, register_writer, resolve_writer
from monai.data.image_writer import (
SUPPORTED_WRITERS,
ITKWriter,
NibabelWriter,
PILWriter,
register_writer,
resolve_writer,
)
from monai.data.meta_tensor import MetaTensor
from monai.transforms import LoadImage, SaveImage, moveaxis
from monai.utils import MetaKeys, OptionalImportError, optional_import
Expand Down Expand Up @@ -150,6 +158,36 @@ def test_1_new(self):
register_writer("new2", lambda x: x + 1)
self.assertEqual(resolve_writer("new")[0](0), 1)

def test_missing_writer_install_hint(self):
def missing_pillow():
raise OptionalImportError("PIL is unavailable", name="PIL")

with patch.dict(SUPPORTED_WRITERS, {"png": (missing_pillow,)}, clear=True):
with self.assertRaisesRegex(OptionalImportError, r"Install `pillow` with `pip install pillow`"):
resolve_writer(".PNG")

def test_multiple_missing_writer_install_hints(self):
def missing_itk():
raise OptionalImportError("ITK is unavailable", name="itk")

def missing_nibabel():
raise OptionalImportError("Nibabel is unavailable", name="nibabel")

with patch.dict(SUPPORTED_WRITERS, {"nii": (missing_nibabel, missing_itk)}, clear=True):
with self.assertRaises(OptionalImportError) as context:
resolve_writer("nii")
self.assertIn("Install one of the following packages", str(context.exception))
self.assertIn("`itk` (`pip install itk`)", str(context.exception))
self.assertIn("`nibabel` (`pip install nibabel`)", str(context.exception))

def test_missing_writer_without_package_name(self):
def missing_unknown():
raise OptionalImportError("Unknown dependency")

with patch.dict(SUPPORTED_WRITERS, {"unknown": (missing_unknown,)}, clear=True):
with self.assertRaisesRegex(OptionalImportError, r"^No ImageWriter backend found for unknown\.$"):
resolve_writer("unknown")


@unittest.skipUnless(has_itk, "itk not installed")
class TestLoadSaveNrrd(unittest.TestCase):
Expand Down
3 changes: 2 additions & 1 deletion tests/utils/test_require_pkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ def test_func(x):
test_func(x=None)

def test_class_exception(self):
with self.assertRaises(OptionalImportError):
with self.assertRaises(OptionalImportError) as context:

@require_pkg(pkg_name="test123")
class TestClass:
pass

TestClass()
self.assertEqual(context.exception.name, "test123")

def test_class_version_exception(self):
with self.assertRaises(OptionalImportError):
Expand Down
Loading