Repository: napari/napari

Issue:
MultipleReaderError when loading example images in napari 0.4.16rc1
## 🐛 Bug

Opening examples from the `File > Open Sample > clEsperanto` doesn't work anymore with napari 0.4.16rc1. 

## To Reproduce

Steps to reproduce the behavior:

1. Install [devbio-napari](https://github.com/haesleinhuepf/devbio-napari#installation).
2. Start napari
3. Click on menu `File > Open Sample > clEsperanto > Blobs (from ImageJ)`

Error stack trace:
```
---------------------------------------------------------------------------
MultipleReaderError                       Traceback (most recent call last)
File ~\miniconda3\envs\bio_39\lib\site-packages\napari\_qt\menus\file_menu.py:190, in FileMenu._rebuild_samples_menu.<locals>._add_sample(plg='clEsperanto', smp='Blobs (from ImageJ)', *args=(False,))
    189 def _add_sample(*args, plg=plugin_name, smp=samp_name):
--> 190     self._win._qt_viewer.viewer.open_sample(plg, smp)
        plg = 'clEsperanto'
        smp = 'Blobs (from ImageJ)'
        self._win = <napari._qt.qt_main_window.Window object at 0x000001CD198102E0>
        self = <napari._qt.menus.file_menu.FileMenu object at 0x000001CD3F9498B0>

File ~\miniconda3\envs\bio_39\lib\site-packages\napari\components\viewer_model.py:841, in ViewerModel.open_sample(self=Viewer(axes=Axes(visible=False, labels=True, col...._transform_active_layer at 0x000001CD2C681550>}), plugin='clEsperanto', sample='Blobs (from ImageJ)', reader_plugin=None, **kwargs={})
    839     return added
    840 elif isinstance(data, (str, Path)):
--> 841     return self.open(data, plugin=reader_plugin)
        data = WindowsPath('c:/structure/code/napari_pyclesperanto_assistant/napari_pyclesperanto_assistant/data/blobs.tif')
        self = Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 127.5, 127.5), zoom=1.8962890625, angles=(0.0, 0.0, 90.0), perspective=0.0, interactive=True), cursor=Cursor(position=(30.0, 200.53738294785023), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=2, ndisplay=2, last_used=0, range=((0.0, 2.0, 1.0), (0.0, 2.0, 1.0)), current_step=(0, 0), order=(0, 1), axis_labels=('1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[], scale_bar=ScaleBar(visible=False, colored=False, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10, unit=None), text_overlay=TextOverlay(visible=False, color=(0.5, 0.5, 0.5, 1.0), font_size=10, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803A00>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803A60>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803AC0>, transform=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803B20>, allow_new_selection=True, selected_vertex=None)), help='', status='nuclei [ 30 201 -66]', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x000001CD2CA1BE50>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x000001CD2C9F49D0>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x000001CD1F183F70>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x000001CD2C9E1430>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x000001CD2C681160>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x000001CD2C681550>})
        reader_plugin = None
    842 else:
    843     raise TypeError(
    844         trans._(
    845             'Got unexpected type for sample ({plugin!r}, {sample!r}): {data_type}',
   (...)
    850         )
    851     )

File ~\miniconda3\envs\bio_39\lib\site-packages\napari\components\viewer_model.py:927, in ViewerModel.open(self=Viewer(axes=Axes(visible=False, labels=True, col...._transform_active_layer at 0x000001CD2C681550>}), path=WindowsPath('c:/structure/code/napari_pyclespera...t/napari_pyclesperanto_assistant/data/blobs.tif'), stack=False, plugin=None, layer_type=None, **kwargs={})
    916             added.extend(
    917                 self._add_layers_with_plugins(
    918                     [_path],
   (...)
    923                 )
    924             )
    925         # no plugin choice was made
    926         else:
--> 927             layers = self._open_or_raise_error(
        layers = <module 'napari.layers' from 'C:\\Users\\rober\\miniconda3\\envs\\bio_39\\lib\\site-packages\\napari\\layers\\__init__.py'>
        self = Viewer(axes=Axes(visible=False, labels=True, colored=True, dashed=False, arrows=True), camera=Camera(center=(0.0, 127.5, 127.5), zoom=1.8962890625, angles=(0.0, 0.0, 90.0), perspective=0.0, interactive=True), cursor=Cursor(position=(30.0, 200.53738294785023), scaled=True, size=1, style=<CursorStyle.STANDARD: 'standard'>), dims=Dims(ndim=2, ndisplay=2, last_used=0, range=((0.0, 2.0, 1.0), (0.0, 2.0, 1.0)), current_step=(0, 0), order=(0, 1), axis_labels=('1', '2')), grid=GridCanvas(stride=1, shape=(-1, -1), enabled=False), layers=[], scale_bar=ScaleBar(visible=False, colored=False, ticks=True, position=<Position.BOTTOM_RIGHT: 'bottom_right'>, font_size=10, unit=None), text_overlay=TextOverlay(visible=False, color=(0.5, 0.5, 0.5, 1.0), font_size=10, position=<TextOverlayPosition.TOP_LEFT: 'top_left'>, text=''), overlays=Overlays(interaction_box=InteractionBox(points=None, show=False, show_handle=False, show_vertices=False, selection_box_drag=None, selection_box_final=None, transform_start=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803A00>, transform_drag=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803A60>, transform_final=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803AC0>, transform=<napari.utils.transforms.transforms.Affine object at 0x000001CD19803B20>, allow_new_selection=True, selected_vertex=None)), help='', status='nuclei [ 30 201 -66]', tooltip=Tooltip(visible=False, text=''), theme='dark', title='napari', mouse_move_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_move at 0x000001CD2CA1BE50>], mouse_drag_callbacks=[<function InteractionBoxMouseBindings.initialize_mouse_events.<locals>.mouse_drag at 0x000001CD2C9F49D0>], mouse_double_click_callbacks=[], mouse_wheel_callbacks=[<function dims_scroll at 0x000001CD1F183F70>], _persisted_mouse_event={}, _mouse_drag_gen={}, _mouse_wheel_gen={}, keymap={'Shift': <function InteractionBoxMouseBindings.initialize_key_events.<locals>.hold_to_lock_aspect_ratio at 0x000001CD2C9E1430>, 'Control-Shift-R': <function InteractionBoxMouseBindings._reset_active_layer_affine at 0x000001CD2C681160>, 'Control-Shift-A': <function InteractionBoxMouseBindings._transform_active_layer at 0x000001CD2C681550>})
        stack = False
        _path = WindowsPath('c:/structure/code/napari_pyclesperanto_assistant/napari_pyclesperanto_assistant/data/blobs.tif')
        kwargs = {}
        layer_type = None
        [_path] = [WindowsPath('c:/structure/code/napari_pyclesperanto_assistant/napari_pyclesperanto_assistant/data/blobs.tif')]
    928                 [_path], kwargs, layer_type, stack
    929             )
    930             added.extend(layers)
    932 return added

File ~\miniconda3\envs\bio_39\lib\site-packages\napari\components\viewer_model.py:1046, in ViewerModel._open_or_raise_error(self=Viewer(axes=Axes(visible=False, labels=True, col...._transform_active_layer at 0x000001CD2C681550>}), paths=[r'c:\structure\code\napari_pyclesperanto_assistant\napari_pyclesperanto_assistant\data\blobs.tif'], kwargs={}, layer_type=None, stack=False)
   1034     raise MissingAssociatedReaderError(
   1035         trans._(
   1036             "Can't find {plugin} plugin associated with {extension} files.",
   (...)
   1042         paths,
   1043     )
   1044 # multiple plugins
   1045 else:
-> 1046     raise MultipleReaderError(
        trans = <napari.utils.translations.TranslationBundle object at 0x000001CD19B04670>
        path_message = 'c:\\structure\\code\\napari_pyclesperanto_assistant\\napari_pyclesperanto_assistant\\data\\blobs.tif'
        readers = {'builtins': 'builtins', 'aicsimageio-in-memory': 'aicsimageio-in-memory', 'aicsimageio-out-of-memory': 'aicsimageio-out-of-memory', 'napari-itk-io': 'napari-itk-io'}
        paths = ['c:\\structure\\code\\napari_pyclesperanto_assistant\\napari_pyclesperanto_assistant\\data\\blobs.tif']
   1047         trans._(
   1048             "Multiple plugins found capable of reading {path_message}. Select plugin from {plugins} and pass to reading function e.g. `viewer.open(..., plugin=...)`.",
   1049             path_message=path_message,
   1050             plugins=readers,
   1051             deferred=True,
   1052         ),
   1053         list(readers.keys()),
   1054         paths,
   1055     )
   1057 return added

MultipleReaderError: Multiple plugins found capable of reading c:\structure\code\napari_pyclesperanto_assistant\napari_pyclesperanto_assistant\data\blobs.tif. Select plugin from {'builtins': 'builtins', 'aicsimageio-in-memory': 'aicsimageio-in-memory', 'aicsimageio-out-of-memory': 'aicsimageio-out-of-memory', 'napari-itk-io': 'napari-itk-io'} and pass to reading function e.g. `viewer.open(..., plugin=...)`.
```

## Expected behavior

In napari 0.4.15 the behaviour was that a dialog opens asking me with which plugin to open the file.

## Environment

 - Please copy and paste the information at napari info option in help menubar here:
```
napari: 0.4.16rc1
Platform: Windows-10-10.0.19044-SP0
Python: 3.9.10 | packaged by conda-forge | (main, Feb 1 2022, 21:22:07) [MSC v.1929 64 bit (AMD64)]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.21.5
SciPy: 1.8.0
Dask: 2022.02.0
VisPy: 0.10.0

OpenGL:
- GL version: 4.6.0 NVIDIA 496.49
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 1920x1200, scale 1.0

Plugins:
- PlatyMatch: 0.0.3
- RedLionfish: 0.6
- aicsimageio-in-memory: 0.5.0
- aicsimageio-out-of-memory: 0.5.0
- animation: 0.0.2
- cellpose-napari: 0.1.4
- clEsperanto: 0.17.1
- console: 0.0.4
- devbio_napari: 0.5.0
- morphometrics: 0.0.1
- napari-3d-ortho-viewer: 0.0.1
- napari-accelerated-pixel-and-object-classification: 0.7.0
- napari-allencell-segmenter: 1.1.4
- napari-animated-gif-io: 0.1.2
- napari-assistant: 0.1.12
- napari-brightness-contrast: 0.1.7
- napari-clusters-plotter: 0.5.0
- napari-crop: 0.1.5
- napari-cupy-image-processing: 0.2.2
- napari-curtain: 0.1.1
- napari-folder-browser: 0.1.3
- napari-itk-io: 0.1.0
- napari-layer-details-display: 0.1.2
- napari-npe2-test: 0.0.1
- napari-ome-zarr: 0.1.0
- napari-plot-profile: 0.2.1
- napari-plugin-search: 0.1.3
- napari-process-points-and-surfaces: 0.1.4
- napari-script-editor: 0.2.8
- napari-segment-blobs-and-things-with-membranes: 0.2.21
- napari-simpleitk-image-processing: 0.2.7
- napari-tabu: 0.1.5
- napari-time-slicer: 0.4.3
- napari-tools-menu: 0.1.11
- napari-workflow-inspector: 0.2.1
- napari-workflow-optimizer: 0.1.3
- napari_skimage_regionprops1: 0.4.6
- napari_skimage_regionprops2: 0.4.6
- natari: 0.2.7
- ome-types: 0.2.10
- ome_zarr: 0.0.18
- omero: 0.1.1
- scikit-image: 0.4.16rc1
- svg: 0.1.5
```
 - Any other relevant information:

## Additional context

In case the napari API changed and I should change [how examples are opened in clEsperanto](https://github.com/clEsperanto/napari_pyclesperanto_assistant/blob/master/napari_pyclesperanto_assistant/_napari_plugin.py#L61), please point me to the relevant documentation (transition guide).


Discussion:
Thanks for reporting @haesleinhuepf! We'll fix before 0.4.16. 

Resolving patch:
diff --git a/napari/_qt/menus/file_menu.py b/napari/_qt/menus/file_menu.py
--- a/napari/_qt/menus/file_menu.py
+++ b/napari/_qt/menus/file_menu.py
@@ -4,6 +4,9 @@
 from qtpy.QtCore import QSize
 from qtpy.QtWidgets import QAction
 
+from napari._qt.dialogs.qt_reader_dialog import handle_gui_reading
+from napari.errors.reader_errors import MultipleReaderError
+
 from ...settings import get_settings
 from ...utils.history import get_save_history, update_save_history
 from ...utils.misc import running_as_bundled_app
@@ -187,7 +190,15 @@ def _rebuild_samples_menu(self):
                     action = QAction(full_name, parent=self)
 
                 def _add_sample(*args, plg=plugin_name, smp=samp_name):
-                    self._win._qt_viewer.viewer.open_sample(plg, smp)
+                    try:
+                        self._win._qt_viewer.viewer.open_sample(plg, smp)
+                    except MultipleReaderError as e:
+                        handle_gui_reading(
+                            e.paths,
+                            self._win._qt_viewer,
+                            plugin_name=plugin_name,
+                            stack=False,
+                        )
 
                 menu.addAction(action)
                 action.triggered.connect(_add_sample)
diff --git a/napari/components/viewer_model.py b/napari/components/viewer_model.py
--- a/napari/components/viewer_model.py
+++ b/napari/components/viewer_model.py
@@ -814,6 +814,9 @@ def open_sample(
                 data = plugin_manager._sample_data[plugin][sample]['data']
             except KeyError:
                 available += list(plugin_manager.available_samples())
+        # npe2 uri sample data, extract the path so we can use viewer.open
+        elif hasattr(data.__self__, 'uri'):
+            data = data.__self__.uri
 
         if data is None:
             msg = trans._(
diff --git a/napari/plugins/utils.py b/napari/plugins/utils.py
--- a/napari/plugins/utils.py
+++ b/napari/plugins/utils.py
@@ -28,7 +28,6 @@ def get_potential_readers(filename: str) -> Dict[str, str]:
         dictionary of registered name to display_name
     """
     readers = _npe2.get_readers(filename)
-
     npe1_readers = {}
     for spec, hook_caller in plugin_manager.hooks.items():
         if spec == 'napari_get_reader':


Test patch:
diff --git a/napari/_qt/menus/_tests/__init__.py b/napari/_qt/menus/_tests/__init__.py
new file mode 100644
diff --git a/napari/_qt/menus/_tests/test_file_menu.py b/napari/_qt/menus/_tests/test_file_menu.py
new file mode 100644
--- /dev/null
+++ b/napari/_qt/menus/_tests/test_file_menu.py
@@ -0,0 +1,33 @@
+from unittest import mock
+
+from npe2 import DynamicPlugin
+from npe2.manifest.contributions import SampleDataURI
+
+
+def test_sample_data_triggers_reader_dialog(
+    mock_npe2_pm, tmp_reader, make_napari_viewer
+):
+    """Sample data pops reader dialog if multiple compatible readers"""
+    # make two tmp readers that take tif files
+    tmp_reader(mock_npe2_pm, 'tif-reader', filename_patterns=['*.tif'])
+    tmp_reader(mock_npe2_pm, 'other-tif-reader', filename_patterns=['*.tif'])
+
+    # make a sample data reader for tif file
+    tmp_sample_plugin = DynamicPlugin('sample-plugin', mock_npe2_pm)
+    my_sample = SampleDataURI(
+        key='tmp-sample',
+        display_name='Temp Sample',
+        uri='some-path/some-file.tif',
+    )
+    tmp_sample_plugin.manifest.contributions.sample_data = [my_sample]
+    tmp_sample_plugin.register()
+
+    viewer = make_napari_viewer()
+    sample_action = viewer.window.file_menu.open_sample_menu.actions()[0]
+    with mock.patch(
+        'napari._qt.menus.file_menu.handle_gui_reading'
+    ) as mock_read:
+        sample_action.trigger()
+
+    # assert that handle gui reading was called
+    mock_read.assert_called_once()