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
48 changes: 42 additions & 6 deletions src/npe2/io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import (
TYPE_CHECKING,
Any,
List,
Literal,
Optional,
Expand Down Expand Up @@ -161,20 +162,32 @@ def _read(
chosen_compatible_readers
), "No readers to try. Expected an exception before this point."

tried_reader = False
for rdr in chosen_compatible_readers:
read_func = rdr.exec(
kwargs={"path": paths, "stack": stack, "_registry": _pm.commands}
)
if read_func is not None:
tried_reader = True
# if the reader function raises an exception here, we don't try to catch it
if layer_data := read_func(paths, stack=stack):
return (layer_data, rdr) if return_reader else layer_data
layer_data = read_func(paths, stack=stack)
if plugin_name and _is_null_layer_sentinel(layer_data):
# we don't return null layers if the user selected a plugin,
# so that we can raise a meaningful error
continue
return (layer_data, rdr) if return_reader else layer_data

if plugin_name:
raise ValueError(
f"Reader {plugin_name!r} was selected to open "
+ f"{paths!r}, but returned no data."
)
if tried_reader:
raise ValueError(
f"Reader {plugin_name!r} was selected to open "
+ f"{paths!r}, but returned no data."
)
else:
raise ValueError(
f"Reader {plugin_name!r} was selected to open "
+ f"{paths!r}, but refused the file."
)
raise ValueError(f"No readers returned data for {paths!r}")


Expand Down Expand Up @@ -260,6 +273,29 @@ def _get_compatible_readers_by_choice(
return chosen_compatible_readers


def _is_null_layer_sentinel(layer_data: Any) -> bool:
"""Checks if the layer data returned from a reader function indicates an
empty file. The sentinel value used for this is ``[(None,)]``.

Parameters
----------
layer_data : LayerData
The layer data returned from a reader function to check

Returns
-------
bool
True, if the layer_data indicates an empty file, False otherwise
"""
return (
isinstance(layer_data, list)
Copy link
Contributor

Choose a reason for hiding this comment

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

is it always list or can it be like a tuple?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I think technically speaking it could be a tuple, but this function was reproduced from napari, so it's what we've always used to validate the return type of readers. I'd say we keep it like this for now, and change it later if need be. Looking at the docs for readers, we always refer to a list also.

and len(layer_data) == 1
and isinstance(layer_data[0], tuple)
and len(layer_data[0]) == 1
and layer_data[0][0] is None
)


@overload
def _write(
path: str,
Expand Down
37 changes: 32 additions & 5 deletions tests/test__io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,14 @@ def read(paths):
return read

# "gooby-again" isn't used even though given plugin starts with the same name
# if an error is thrown here, it means we selected the wrong plugin
io_utils._read(["some.fzzy"], plugin_name=short_name, stack=False, _pm=pm)
# we check that the thrown error is from "gooby" NOT "gooby-again"
with pytest.raises(
ValueError, match=f"Reader {short_name!r} was selected .* but returned no data"
):
io_utils._read(["some.fzzy"], plugin_name=short_name, stack=False, _pm=pm)


def test_read_fails():
def test_read_fails_with_refused_reader():
pm = PluginManager()
plugin_name = "always-fails"
plugin = DynamicPlugin(plugin_name, plugin_manager=pm)
Expand All @@ -92,13 +95,34 @@ def test_read_fails():
def get_read(path):
return None

with pytest.raises(ValueError, match=f"Reader {plugin_name!r} was selected"):
with pytest.raises(
ValueError, match=f"Reader {plugin_name!r} was selected .* refused the file"
):
io_utils._read(["some.fzzy"], plugin_name=plugin_name, stack=False, _pm=pm)

with pytest.raises(ValueError, match="No readers returned data"):
io_utils._read(["some.fzzy"], stack=False, _pm=pm)


def test_read_fails_with_null_layer():
pm = PluginManager()
plugin_name = "always-fails"
plugin = DynamicPlugin(plugin_name, plugin_manager=pm)
plugin.register()

def reader_func(path):
return [(None,)]

@plugin.contribute.reader(filename_patterns=["*.fzzy"])
def get_read(path):
return reader_func

with pytest.raises(
ValueError, match=f"Reader {plugin_name!r} was selected .* returned no data"
):
io_utils._read(["some.fzzy"], plugin_name=plugin_name, stack=False, _pm=pm)


def test_read_with_incompatible_reader(uses_sample_plugin):
paths = ["some.notfzzy"]
chosen_reader = f"{SAMPLE_PLUGIN_NAME}"
Expand All @@ -117,7 +141,10 @@ def test_read_with_no_compatible_reader():
def test_read_with_reader_contribution_plugin(uses_sample_plugin):
paths = ["some.fzzy"]
chosen_reader = f"{SAMPLE_PLUGIN_NAME}.some_reader"
assert read(paths, stack=False, plugin_name=chosen_reader) == [(None,)]
with pytest.raises(
ValueError, match=f"Reader {chosen_reader!r} was selected .* returned no data"
):
read(paths, stack=False, plugin_name=chosen_reader)

# if the wrong contribution is passed we get useful error message
chosen_reader = f"{SAMPLE_PLUGIN_NAME}.not_a_reader"
Expand Down
Loading