Repository: napari/napari

Issue:
AttributeError trying to test file opening with a plugin
## 🐛 Bug

I am trying to test the automatic opening of a file with a `napari` plugin. When running `pytest` on the test code below, I get the following error.

I suspect that I'm doing something wrong here, and that the solution to this issue would be to improve the error message (perhaps no plugings are acutally registered? perhaps it's not possible to do this type of testing?)

```python
_________________________________________________________ test_load_cellfinder_xml __________________________________________________________

path = '/Users/dstansby/projects/napari/brainglobe-napari-io/brainglobe_napari_io/tests/data/cell_classification.xml', plugin = None

# Rest of function cut out for readability

        layer_data = None
        result = hook_caller.call_with_result_obj(path=path)
        reader = result.result  # will raise exceptions if any occurred
        try:
>           layer_data = reader(path)  # try to read data
E           TypeError: 'NoneType' object is not callable

../../../mambaforge/envs/napari/lib/python3.9/site-packages/napari/plugins/io.py:108: TypeError

During handling of the above exception, another exception occurred:

viewer = Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 0.0,...ine at 0x15a8773a0>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x15a877f70>})

    def test_load_cellfinder_xml(viewer):
>       viewer.open(test_data_path / 'cell_classification.xml')

brainglobe_napari_io/tests/test_brainglobe_napari_io.py:18: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../mambaforge/envs/napari/lib/python3.9/site-packages/napari/components/viewer_model.py:907: in open
    self._add_layers_with_plugins(
../../../mambaforge/envs/napari/lib/python3.9/site-packages/napari/components/viewer_model.py:952: in _add_layers_with_plugins
    layer_data, hookimpl = read_data_with_plugins(
../../../mambaforge/envs/napari/lib/python3.9/site-packages/napari/plugins/io.py:111: in read_data_with_plugins
    raise PluginCallError(result.implementation, cause=exc)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = PluginCallError(None), hook_implementation = None, msg = None, cause = TypeError("'NoneType' object is not callable")

    def __init__(self, hook_implementation, msg=None, cause=None):
>       plugin = hook_implementation.plugin
E       AttributeError: 'NoneType' object has no attribute 'plugin'

../../../mambaforge/envs/napari/lib/python3.9/site-packages/napari_plugin_engine/exceptions.py:197: AttributeError
```

## To Reproduce

```python
@pytest.fixture
def viewer(make_napari_viewer):
    viewer = make_napari_viewer()
    plugin_manager.register(plugin_namespace)
    return viewer


def test_load_cellfinder_xml(viewer):
    viewer.open(test_data_path / 'cell_classification.xml')
```
## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

## Environment

 - Please copy and paste the information at napari info option in help menubar here:

 - Any other relevant information:

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Thanks @dstansby.  Can you tell me exactly what `plugin_namespace` is in your example above?
It's a custom one that imports a bunch of custom plugins. I can provide the source, but commenting out the `plugin_manager.register(plugins)` line I get exactly the same error as above, so I don't think that's the issue?
Ok, it seems like there must be some other important detail about the way you're setting up the tests or plugin:
This works for example...  

```python
import pytest
import tifffile
import numpy as np

@pytest.fixture
def viewer(make_napari_viewer):
    return make_napari_viewer()


def test_load_cellfinder_xml(tmp_path, viewer):
    fname = (tmp_path / 'test.tif')
    tifffile.imwrite(fname, np.random.rand(4, 4))
    assert not viewer.layers
    viewer.open(fname)
    assert viewer.layers
```

can you create an example for me?
I think I'm hitting the case where the file isn't recognised by any of the registered plugins. I think in your example `napari` knows how to read the `test.tif` file. If you try and read an empty `.csv` file (for example) I think it should reproduce the error. (sorry, don't have time to write a full example immediately)
thanks, that helps:
```python
import pytest

@pytest.fixture
def viewer(make_napari_viewer):
    return make_napari_viewer()


def test_load_cellfinder_xml(tmp_path, viewer):
    fname = (tmp_path / 'cell_classification.xml')
    fname.touch()
    viewer.open(fname)
```


Resolving patch:
diff --git a/napari/plugins/io.py b/napari/plugins/io.py
--- a/napari/plugins/io.py
+++ b/napari/plugins/io.py
@@ -113,12 +113,12 @@ def read_data_with_plugins(
 
     layer_data = None
     result = hook_caller.call_with_result_obj(path=npe1_path)
-    reader = result.result  # will raise exceptions if any occurred
-    try:
-        layer_data = reader(npe1_path)  # try to read data
-        hookimpl = result.implementation
-    except Exception as exc:
-        raise PluginCallError(result.implementation, cause=exc)
+    if reader := result.result:  # will raise exceptions if any occurred
+        try:
+            layer_data = reader(npe1_path)  # try to read data
+            hookimpl = result.implementation
+        except Exception as exc:
+            raise PluginCallError(result.implementation, cause=exc) from exc
 
     if not layer_data:
         # if layer_data is empty, it means no plugin could read path


Test patch:
diff --git a/napari/components/_tests/test_add_layers.py b/napari/components/_tests/test_add_layers.py
--- a/napari/components/_tests/test_add_layers.py
+++ b/napari/components/_tests/test_add_layers.py
@@ -69,6 +69,15 @@ def test_viewer_open():
     assert all(lay.source == expected_source for lay in viewer.layers)
 
 
+def test_viewer_open_no_plugin(tmp_path):
+    viewer = ViewerModel()
+    fname = tmp_path / 'gibberish.gbrsh'
+    fname.touch()
+    with pytest.raises(ValueError) as e:
+        viewer.open(fname)
+    assert 'No plugin found capable of reading' in str(e.value)
+
+
 plugin_returns = [
     ([(img, {'name': 'foo'})], {'name': 'bar'}),
     ([(img, {'blending': 'additive'}), (img,)], {'blending': 'translucent'}),