Repository: napari/napari

Issue:
Cannot import some napari component in plugin using the napari_experimental_provide_dock_widget hook spec
## 🐛 Bug

Thank you for releasing the `napari_experimental_provide_dock_widget` hook spec. I am trying to use it but I find that I am not able to import some of the napari functionality in the plugin code, such as `from napari.qt.threading import thread_worker`

## To Reproduce

Steps to reproduce the behavior:

1. The simple dock widget plugin:
```
from qtpy.QtWidgets import QWidget
from napari_plugin_engine import napari_hook_implementation

@napari_hook_implementation(specname="napari_experimental_provide_dock_widget")
def test_widget():
    return (TestWidget, {"area": "right"})


class TestWidget(QWidget):

    def __init__(self) -> None:
        super().__init__()
```
would successfully give me a dock widget.

 2. However if I just add one line on the top

```from napari.qt.threading import thread_worker```

the plugin cannot be properly installed. From the Plugins->Plugin Errors menu I got the error message and the key error is this:
```
...
---> 11 from napari.qt.threading import thread_worker
        global napari.qt.threading = undefined
        global thread_worker = undefined

...\user\napari\napari\qt\__init__.py in 
----> 1 from .._qt.qt_event_loop import get_app, run
        global _qt.qt_event_loop = undefined
        global get_app = undefined
        global run = undefined
      2 from .._qt.qt_main_window import Window
      3 from .._qt.qt_viewer import QtViewer

...\user\napari\napari\_qt\__init__.py in 
     40 from .qt_event_loop import gui_qt
---> 41 from .qt_main_window import Window
        global qt_main_window = undefined
        global Window = undefined
     42 from .qt_resources import import_resources

...\user\napari\napari\_qt\qt_main_window.py in 
     31 from ..utils.theme import get_theme, template
---> 32 from ..viewer import Viewer
        global viewer = undefined
        global Viewer = undefined
     33 from .dialogs.qt_about import QtAbout

ImportError: cannot import name 'Viewer' from 'napari.viewer' (...\user\napari\napari\viewer.py)

The above exception was the direct cause of the following exception:
...
```
So any component that would require `from napar.viewer import Viewer` would end up with this error. I don't have this error when I was using code from #1800. I cannot remember if I used `thread_worker` when I was using code from #1849 . I don't really understand what's the cause of this issue. I can certainly do `from napari.viewer import Viewer` in a simple python script. Would appreciate any help. Thank you!


## Environment

napari: 0.4.3
napari_plugin_engine: 0.1.9




Discussion:
I suspect that you are somehow causing a circular import. The real fix is for us to audit our import structure so that loading of hook implementations does not interfere with imports. The short-term fix is for you to move that import *inside* of the function that needs it, so that it is not imported when we import the hook implementation. Could you give that a go and report back?
Yep @jni is right.  I ran into this the other day when updating my napari-omero plugin (sorry I didn't make an issue yet!).  I can confirm that you can just move that import into the body of the function... until we fix it here
> So any component that would require from napari.viewer import Viewer would end up with this error.

I just want to add that you can make your widget accept the napari viewer as follows

```python
class TestWidget(QWidget):

    def __init__(self, viewer: 'napari.viewer.Viewer') -> None:
        super().__init__()
        # do things with the viewer
```

I think this is the recommended way to get access to the viewer as a plugin, to maximize compatibility with other plugins.

Threading in plugins with things like `thread_worker` need to be thought about more
Thank you all for the help!

It seems to me that it is this line that is causing the issue:

https://github.com/napari/napari/blob/24948427e2728ef2216e59f4904d568850db2df9/napari/_qt/qt_main_window.py#L569

This implementation require the import of `napari.viewer.Viewer`
https://github.com/napari/napari/blob/24948427e2728ef2216e59f4904d568850db2df9/napari/_qt/qt_main_window.py#L32

removing both would solve this issue. Also importing `napari.viewer.Viewer` would somehow cause the `napari._qt.__init__.py` to run twice. I don't fully understand why though.

I personally like how #1800 let hook pass the `viewer` to widget directly and the hook_implementation returns a widget object, instead of a widget class. The widget that doesn't require the viewer can just ignore it in the hook_implementation. By returning the widget class instead of the object make it difficult to pass the `viewer` object directly to widget. But maybe there are merits to return the widget class that I don't understand. 

I am not sure if the above mentioned issue has an easy fix. I feel that forcing the widget `__init__` to have a fixed parameter name `napari_viewer` is a bit strange. Annotation is relatively clearer but it's causing this import issue.
> Annotation is relatively clearer but it's causing this import issue. 

I'm not sure that's the always the case (or at least in my case, it was the `from napari.qt` import that was the problem, I didn't import viewer anywhere).  

> This implementation require the import of napari.viewer.Viewer

it doesn't require it...  you can annotate with the string [forward reference](https://www.python.org/dev/peps/pep-0484/#forward-references) `'napari.viewer.Viewer'` (rather than the actual `Viewer` class object), which doesn't require you to import anything from napari at all.


> hook_implementation returns a widget object, instead of a widget class.

yes... we were just discussing that over here https://github.com/napari/napari/issues/2114#issuecomment-761620026 (though I think we'd still stick to the parameter naming and/or parameter annotations method above)
(Actually we were discussing a slight variant, which would be a hook spec that doesn't return an instance, but which returns a function that returns an instance...). If the hookspec returns an already instantiated widget, then we don't have the means to make additional dock widgets.  
> I'm not sure that's the always the case (or at least in my case, it was the `from napari.qt` import that was the problem, I didn't import viewer anywhere).

> it doesn't require it... you can annotate with the string [forward reference](https://www.python.org/dev/peps/pep-0484/#forward-references) `'napari.viewer.Viewer'` (rather than the actual `Viewer` class object), which doesn't require you to import anything from napari at all.

I probably didn't explain it properly. I do not mean that I am importing the `napari.viewer.Viewer` in the plugin code. I mean in the current `napari/napari/_qt/qt_main_window.py` implementation, the fact that that script has `from ..viewer import Viewer` is causing the circular import. If I remove that line, the import error I mentioned in the very beginning is gone. 
ah! ok!  did you try moving the import from the top of the module to inside of the `Window._add_plugin_dock_widget` method?  If that fixes it, we'd happily accept a PR to that effect :)
> ah! ok! did you try moving the import from the top of the module to inside of the `Window._add_plugin_dock_widget` method? If that fixes it, we'd happily accept a PR to that effect :)

Yes it works. I will submit a PR soon. Thanks!


Resolving patch:
diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py
--- a/napari/_qt/qt_main_window.py
+++ b/napari/_qt/qt_main_window.py
@@ -29,7 +29,6 @@
 from ..utils.io import imsave
 from ..utils.misc import in_jupyter
 from ..utils.theme import get_theme, template
-from ..viewer import Viewer
 from .dialogs.qt_about import QtAbout
 from .dialogs.qt_plugin_dialog import QtPluginDialog
 from .dialogs.qt_plugin_report import QtPluginErrReporter
@@ -553,6 +552,8 @@ def _add_plugin_dock_widget(self, key):
         key : 2-tuple of str
             Plugin name and widget name.
         """
+        from ..viewer import Viewer
+
         full_name = plugins.menu_item_template.format(*key)
         if full_name in self._dock_widgets:
             warnings.warn(f'Dock widget {key!r} already added')


Test patch: