Skip to content

Commit

Permalink
Merge branch 'main' into pre-commit-ci-update-config
Browse files Browse the repository at this point in the history
  • Loading branch information
nclack authored Aug 2, 2023
2 parents ee17f22 + d01b554 commit ade575d
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 15 deletions.
37 changes: 32 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## [v0.7.1](https://github.com/napari/npe2/tree/v0.7.1) (2023-07-16)

[Full Changelog](https://github.com/napari/npe2/compare/v0.7.0...v0.7.1)

**Implemented enhancements:**

- feat: support python3.11 [\#293](https://github.com/napari/npe2/pull/293) ([tlambert03](https://github.com/tlambert03))
- add graph layer [\#292](https://github.com/napari/npe2/pull/292) ([JoOkuma](https://github.com/JoOkuma))

**Fixed bugs:**

- fix: use constraints in napari tests [\#298](https://github.com/napari/npe2/pull/298) ([Czaki](https://github.com/Czaki))
- Use full `plugin_name` when finding chosen `reader` rather than `startswith` [\#297](https://github.com/napari/npe2/pull/297) ([DragaDoncila](https://github.com/DragaDoncila))
- Change ArrayLike type to use read only properties [\#289](https://github.com/napari/npe2/pull/289) ([andy-sweet](https://github.com/andy-sweet))
- Bugfix: use .lower\(\) to make paths & pattern fnmatch case insensitive [\#275](https://github.com/napari/npe2/pull/275) ([psobolewskiPhD](https://github.com/psobolewskiPhD))

**Documentation:**

- Fix typo in `DynamicPlugin` [\#304](https://github.com/napari/npe2/pull/304) ([lucyleeow](https://github.com/lucyleeow))
- DOCS: Widget guide should consistently use parent=None kwarg in examples [\#302](https://github.com/napari/npe2/pull/302) ([GenevieveBuckley](https://github.com/GenevieveBuckley))

**Merged pull requests:**

- remove tomlpp [\#294](https://github.com/napari/npe2/pull/294) ([tlambert03](https://github.com/tlambert03))
- Use hatchling as build backend [\#282](https://github.com/napari/npe2/pull/282) ([tlambert03](https://github.com/tlambert03))

## [v0.7.0](https://github.com/napari/npe2/tree/v0.7.0) (2023-04-14)

[Full Changelog](https://github.com/napari/npe2/compare/v0.6.2...v0.7.0)
Expand All @@ -26,6 +52,7 @@

**Merged pull requests:**

- chore: changelog v0.7.0 [\#286](https://github.com/napari/npe2/pull/286) ([tlambert03](https://github.com/tlambert03))
- ci\(dependabot\): bump peter-evans/create-pull-request from 4 to 5 [\#284](https://github.com/napari/npe2/pull/284) ([dependabot[bot]](https://github.com/apps/dependabot))
- Pin pydantic bellow 2.0 [\#279](https://github.com/napari/npe2/pull/279) ([Czaki](https://github.com/Czaki))

Expand Down Expand Up @@ -187,7 +214,7 @@

## [v0.4.0](https://github.com/napari/npe2/tree/v0.4.0) (2022-06-13)

[Full Changelog](https://github.com/napari/npe2/compare/v0.3.0...v0.4.0)
[Full Changelog](https://github.com/napari/npe2/compare/v0.3.0.rc0...v0.4.0)

**Implemented enhancements:**

Expand Down Expand Up @@ -217,13 +244,13 @@
- Add doc links to README [\#158](https://github.com/napari/npe2/pull/158) ([nclack](https://github.com/nclack))
- Fix codeblock directive in docstring [\#156](https://github.com/napari/npe2/pull/156) ([melissawm](https://github.com/melissawm))

## [v0.3.0](https://github.com/napari/npe2/tree/v0.3.0) (2022-04-05)
## [v0.3.0.rc0](https://github.com/napari/npe2/tree/v0.3.0.rc0) (2022-04-05)

[Full Changelog](https://github.com/napari/npe2/compare/v0.3.0.rc0...v0.3.0)
[Full Changelog](https://github.com/napari/npe2/compare/v0.3.0...v0.3.0.rc0)

## [v0.3.0.rc0](https://github.com/napari/npe2/tree/v0.3.0.rc0) (2022-04-05)
## [v0.3.0](https://github.com/napari/npe2/tree/v0.3.0) (2022-04-05)

[Full Changelog](https://github.com/napari/npe2/compare/v0.2.2...v0.3.0.rc0)
[Full Changelog](https://github.com/napari/npe2/compare/v0.2.2...v0.3.0)

**Implemented enhancements:**

Expand Down
2 changes: 1 addition & 1 deletion _docs/templates/_npe2_writers_guide.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ by the corresponding writer contribution in the manifest).
```python
DataType = Any # usually something like a numpy array, but varies by layer
LayerAttributes = dict
LayerName = Literal["image", "labels", "points", "shapes", "surface", "tracks", "vectors"]
LayerName = Literal["graph", "image", "labels", "points", "shapes", "surface", "tracks", "vectors"]
FullLayerData = Tuple[DataType, LayerAttributes, LayerName]
MultiWriterFunction = Callable[[str, List[FullLayerData]], List[str]]
```
Expand Down
3 changes: 3 additions & 0 deletions src/npe2/_inspection/_visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ def napari_get_writer(self, node: ast.FunctionDef):
# we can't convert this to an npe2 command contribution
pass # pragma: no cover

def napari_write_graph(self, node: ast.FunctionDef):
self._parse_writer(node, "graph") # pragma: no cover

def napari_write_image(self, node: ast.FunctionDef):
self._parse_writer(node, "image")

Expand Down
97 changes: 93 additions & 4 deletions src/npe2/io_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,16 @@ def _read(
if _pm is None:
_pm = PluginManager.instance()

for rdr in _pm.iter_compatible_readers(paths):
if plugin_name and rdr.plugin_name != plugin_name:
continue
# get readers compatible with paths and chosen plugin - raise errors if
# choices are invalid or there's nothing to try
chosen_compatible_readers = _get_compatible_readers_by_choice(
plugin_name, paths, _pm
)
assert (
chosen_compatible_readers
), "No readers to try. Expected an exception before this point."

for rdr in chosen_compatible_readers:
read_func = rdr.exec(
kwargs={"path": paths, "stack": stack, "_registry": _pm.commands}
)
Expand All @@ -167,12 +174,94 @@ def _read(

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


def _get_compatible_readers_by_choice(
plugin_name: Union[str, None], paths: Union[str, Sequence[str]], pm: PluginManager
):
"""Returns compatible readers filtered by validated plugin choice.
Checks that plugin_name is an existing plugin (and command if
a specific contribution was passed), and that it is compatible
with paths. Raises ValueError if given plugin doesn't exist,
it is not compatible with the given paths, or no compatible
readers can be found for paths (if no plugin was chosen).
Parameters
----------
plugin_name: Union[str, None]
name of chosen plugin, or None
paths: Union[str, Sequence[str]]
paths to read
pm: PluginManager
plugin manager instance to check for readers
Raises
------
ValueError
If the given reader doesn't exist
ValueError
If there are no compatible readers
ValueError
If the given reader is not compatible
Returns
-------
compat_readers : List[ReaderContribution]
Compatible readers for plugin choice
"""
passed_contrib = plugin_name and ("." in plugin_name)
compat_readers = list(pm.iter_compatible_readers(paths))
compat_reader_names = sorted(
{(rdr.command if passed_contrib else rdr.plugin_name) for rdr in compat_readers}
)
helper_error_message = (
f"Available readers for {paths!r} are: {compat_reader_names!r}."
if compat_reader_names
else f"No compatible readers are available for {paths!r}."
)

# check whether plugin even exists.
if plugin_name:
try:
# note that get_manifest works with a full command e.g. my-plugin.my-reader
pm.get_manifest(plugin_name)
if passed_contrib:
pm.get_command(plugin_name)
except KeyError:
raise ValueError(
f"Given reader {plugin_name!r} does not exist. {helper_error_message}"
) from None

# no choice was made and there's no readers to try
if not plugin_name and not len(compat_reader_names):
raise ValueError(helper_error_message)

# user didn't make a choice and we have some readers to try, return them
if not plugin_name:
return compat_readers

# user made a choice and it exists, but it may not be a compatible reader
plugin, _, _ = plugin_name.partition(".")
chosen_compatible_readers = [
rdr
for rdr in compat_readers
if rdr.plugin_name == plugin
and (not passed_contrib or rdr.command == plugin_name)
]
# the user's choice is not compatible with the paths. let them know what is
if not chosen_compatible_readers:
raise ValueError(
f"Given reader {plugin_name!r} is not a compatible reader for {paths!r}. "
+ helper_error_message
)
return chosen_compatible_readers


@overload
def _write(
path: str,
Expand Down
5 changes: 5 additions & 0 deletions src/npe2/manifest/contributions/_themes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Literal, Optional, Union

from pydantic import BaseModel, color
Expand Down Expand Up @@ -64,3 +65,7 @@ class ThemeContribution(BaseModel):
' - RGB/RGBA strings: `"rgb(255, 255, 255)"`, `"rgba(255, 255, 255, 0.5)`"\n'
' - HSL strings: "`hsl(270, 60%, 70%)"`, `"hsl(270, 60%, 70%, .5)`"\n'
)
font_size: str = Field(
default="12pt" if sys.platform == "darwin" else "9pt",
description="Font size (in points, pt) used in the application.",
)
1 change: 1 addition & 0 deletions src/npe2/manifest/contributions/_writers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@


class LayerType(str, Enum):
graph = "graph"
image = "image"
labels = "labels"
points = "points"
Expand Down
2 changes: 1 addition & 1 deletion src/npe2/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __array__(self) -> "np.ndarray":


LayerName = Literal[
"image", "labels", "points", "shapes", "surface", "tracks", "vectors"
"graph", "image", "labels", "points", "shapes", "surface", "tracks", "vectors"
]
Metadata = Dict
DataType = Union[ArrayLike, Sequence[ArrayLike]]
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class HookSpecs:
def napari_provide_sample_data(): ... # type: ignore
def napari_get_reader(path): ...
def napari_get_writer(path, layer_types): ...
def napari_write_graph(path, data, meta): ...
def napari_write_image(path, data, meta): ...
def napari_write_labels(path, data, meta): ...
def napari_write_points(path, data, meta): ...
Expand Down
104 changes: 100 additions & 4 deletions tests/test__io_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# extra underscore in name to run this first
from pathlib import Path
from unittest.mock import patch

import pytest

Expand All @@ -23,14 +24,30 @@ def test_read(uses_sample_plugin):

def test_read_with_unknown_plugin(uses_sample_plugin):
# no such plugin name.... skips over the sample plugin & error is specific
with pytest.raises(ValueError, match="Plugin 'nope' was selected"):
read(["some.fzzy"], plugin_name="nope", stack=False)
paths = ["some.fzzy"]
chosen_reader = "not-a-plugin"
with pytest.raises(
ValueError, match=f"Given reader {chosen_reader!r} does not exist."
) as e:
read(paths, plugin_name=chosen_reader, stack=False)
assert f"Available readers for {paths!r} are: {[SAMPLE_PLUGIN_NAME]!r}" in str(e)


def test_read_with_unknown_plugin_no_readers(uses_sample_plugin):
paths = ["some.nope"]
chosen_reader = "not-a-plugin"
with pytest.raises(
ValueError, match=f"Given reader {chosen_reader!r} does not exist."
) as e:
read(paths, plugin_name=chosen_reader, stack=False)
assert "No compatible readers are available" in str(e)


def test_read_with_no_plugin():
# no plugin passed and none registered
with pytest.raises(ValueError, match="No readers returned"):
read(["some.nope"], stack=False)
paths = ["some.nope"]
with pytest.raises(ValueError, match="No compatible readers are available"):
read(paths, stack=False)


def test_read_uses_correct_passed_plugin(tmp_path):
Expand All @@ -40,6 +57,9 @@ def test_read_uses_correct_passed_plugin(tmp_path):
long_name_plugin = DynamicPlugin(long_name, plugin_manager=pm)
short_name_plugin = DynamicPlugin(short_name, plugin_manager=pm)

long_name_plugin.register()
short_name_plugin.register()

path = "something.fzzy"
mock_file = tmp_path / path
mock_file.touch()
Expand All @@ -62,6 +82,82 @@ def read(paths):
io_utils._read(["some.fzzy"], plugin_name=short_name, stack=False, _pm=pm)


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

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

with pytest.raises(ValueError, match=f"Reader {plugin_name!r} was selected"):
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_with_incompatible_reader(uses_sample_plugin):
paths = ["some.notfzzy"]
chosen_reader = f"{SAMPLE_PLUGIN_NAME}"
with pytest.raises(
ValueError, match=f"Given reader {chosen_reader!r} is not a compatible reader"
):
read(paths, stack=False, plugin_name=chosen_reader)


def test_read_with_no_compatible_reader():
paths = ["some.notfzzy"]
with pytest.raises(ValueError, match="No compatible readers are available"):
read(paths, stack=False)


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,)]

# if the wrong contribution is passed we get useful error message
chosen_reader = f"{SAMPLE_PLUGIN_NAME}.not_a_reader"
with pytest.raises(
ValueError,
match=f"Given reader {chosen_reader!r} does not exist.",
) as e:
read(paths, stack=False, plugin_name=chosen_reader)
assert "Available readers for" in str(e)


def test_read_assertion_with_no_compatible_readers(uses_sample_plugin):
paths = ["some.noreader"]
with patch("npe2.io_utils._get_compatible_readers_by_choice", return_value=[]):
with pytest.raises(AssertionError, match="No readers to try."):
read(paths, stack=False)


def test_available_readers_show_commands(uses_sample_plugin):
paths = ["some.fzzy"]
chosen_reader = "not-a-plugin.not-a-reader"
with pytest.raises(
ValueError,
match=f"Given reader {chosen_reader!r} does not exist.",
) as e:
read(paths, stack=False, plugin_name=chosen_reader)
assert "Available readers " in str(e)
assert f"{SAMPLE_PLUGIN_NAME}.some_reader" in str(e)

chosen_reader = "not-a-plugin"
with pytest.raises(
ValueError,
match=f"Given reader {chosen_reader!r} does not exist.",
) as e:
read(paths, stack=False, plugin_name=chosen_reader)
assert "Available readers " in str(e)
assert f"{SAMPLE_PLUGIN_NAME}.some_reader" not in str(e)
assert f"{SAMPLE_PLUGIN_NAME}" in str(e)


def test_read_return_reader(uses_sample_plugin):
data, reader = read_get_reader("some.fzzy")
assert data == [(None,)]
Expand Down

0 comments on commit ade575d

Please sign in to comment.