Repository: napari/napari

Issue:
Viewer type hint doesn't work with dock widget plugin 
## 🐛 Bug

On current master, my dock widget plugins cannot be imported. They do not show up in the plugins menu. In the plugin error dialog, I see the  error listed below. When I install napari 0.4.7 from pip, I do not get this error.

<details>
  <summary>plugin error</summary>
  
  ```python
   ======================== Errors for plugin 'napari-cropper' ========================

napari version: 0.4.3.dev241+g0b1bdac.d20210412


ERROR #1: Error while importing module napari_cropper ---------------------------

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/site-packages/napari_plugin_engine/manager.py in _load_and_register(self=, mod_name='napari_cropper', plugin_name='napari-cropper')
    291         try:
--> 292             module = load(mod_name)
        module = undefined
        global load = 
        mod_name = 'napari_cropper'
    293             if self.is_registered(module):

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/site-packages/napari_plugin_engine/manager.py in load(value='napari_cropper')
   1004         raise ValueError(f"malformed entry point string: {value}")
-> 1005     module = importlib.import_module(match.group('module'))
        module = undefined
        global importlib.import_module = 
        match.group = 
   1006     attrs = filter(None, (match.group('attr') or '').split('.'))

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/__init__.py in import_module(name='napari_cropper', package=None)
    126             level += 1
--> 127     return _bootstrap._gcd_import(name[level:], package, level)
        global _bootstrap._gcd_import = 
        name = 'napari_cropper'
        level = 0
        package = None
    128 

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap.py in _gcd_import(name='napari_cropper', package=None, level=0)

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap.py in _find_and_load(name='napari_cropper', import_=)

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap.py in _find_and_load_unlocked(name='napari_cropper', import_=)

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap.py in _load_unlocked(spec=ModuleSpec(name='napari_cropper', loader=

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap_external.py in exec_module(self=, module=)

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/importlib/_bootstrap.py in _call_with_frames_removed(f=, *args=( at 0x7fcad1596540, file "/...pari-cropper/napari_cropper/__init__.py", line 1>, {'__builtins__': {'ArithmeticError': , 'AssertionError': , 'AttributeError': , 'BaseException': , 'BlockingIOError': , 'BrokenPipeError': , 'BufferError': , 'BytesWarning': , 'ChildProcessError': , 'ConnectionAbortedError': , ...}, '__cached__': '/Users/kyamauch/Documents/napari-cropper/napari_cropper/__pycache__/__init__.cpython-37.pyc', '__doc__': None, '__file__': '/Users/kyamauch/Documents/napari-cropper/napari_cropper/__init__.py', '__loader__': , '__name__': 'napari_cropper', '__package__': 'napari_cropper', '__path__': ['/Users/kyamauch/Documents/napari-cropper/napari_cropper'], '__spec__': ModuleSpec(name='napari_cropper', loader=

~/Documents/napari-cropper/napari_cropper/__init__.py in 
      7 
----> 8 from ._dock_widget import napari_experimental_provide_dock_widget
        global _dock_widget = undefined
        global napari_experimental_provide_dock_widget = undefined
      9 

~/Documents/napari-cropper/napari_cropper/_dock_widget.py in 
      2 
----> 3 from .qt_cropper_widget import QtCropperWidget
        global qt_cropper_widget = undefined
        global QtCropperWidget = undefined
      4 

~/Documents/napari-cropper/napari_cropper/qt_cropper_widget.py in 
     13 
---> 14 class QtCropperWidget(QWidget):
        global QtCropperWidget = undefined
        global QWidget = 
     15     """The QWdiget containing the controls for the cropper tool

~/Documents/napari-cropper/napari_cropper/qt_cropper_widget.py in QtCropperWidget()
     25 
---> 26     def __init__(self, viewer: napari.viewer.Viewer):
        global __init__ = undefined
        global self = undefined
        global viewer = undefined
        global napari.viewer.Viewer = 
     27         super().__init__()

AttributeError: module 'napari' has no attribute 'viewer'

The above exception was the direct cause of the following exception:

PluginImportError                         Traceback (most recent call last)
~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/site-packages/napari_plugin_engine/manager.py in discover(self=, path=None, entry_point=None, prefix=None, ignore_errors=True)
    241 
    242             try:
--> 243                 if self._load_and_register(mod_name, name):
        self._load_and_register = >
        mod_name = 'napari_console'
        name = 'console'
    244                     count += 1
    245             except PluginError as e:

~/opt/miniconda3/envs/napari-interactive-cropping/lib/python3.7/site-packages/napari_plugin_engine/manager.py in _load_and_register(self=, mod_name='napari_cropper', plugin_name='napari-cropper')
    297                 f'Error while importing module {mod_name}',
    298                 plugin_name=plugin_name,
--> 299                 cause=exc,
        global cause = undefined
        exc = undefined
    300             )
    301         if not (inspect.isclass(module) or inspect.ismodule(module)):

PluginImportError: Error while importing module napari_cropper
================================================================================
  ```
</details>

## To Reproduce

Steps to reproduce the behavior:

1. Open napari with a dock widget plugin installed in the same environment (e.g., [napari-cropper](https://github.com/kevinyamauchi/napari-cropper))
2. Attempt to add the dock widget via the Plugins menu


## Environment

napari: 0.4.3.dev241+g0b1bdac.d20210412
Platform: Darwin-19.6.0-x86_64-i386-64bit
System: MacOS 10.15.7
Python: 3.7.10 (default, Feb 26 2021, 10:16:00) [Clang 10.0.0 ]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.20.2
SciPy: 1.6.2
Dask: 2021.04.0
VisPy: 0.6.6

OpenGL:
- GL version: 2.1 INTEL-14.7.12
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 2560x1440, scale 1.0
- screen 2: resolution 1440x900, scale 2.0

Plugins:
- console: 0.0.3
- svg: 0.1.4



Discussion:
I did a bit more digging and I think the issue is on my side. In my `__init__` for the dock widget, I had annotated the viewer kwarg as `napari.viewer.Viewer`. When I name it `napari_viewer` with no type annotation, it works. Is that correct, @tlambert03 ?

Before (this used to work, but is now broken):
```python
class QtCropperWidget(QWidget):
    """The QWdiget containing the controls for the cropper tool

    Parameters
    ----------
    viewer : napari.viewer.Viewer
        The parent napari viewer

    Attributes
    ----------
    """

    def __init__(self, viewer: napari.viewer.Viewer):
```

This works on current master:
```python
class QtCropperWidget(QWidget):
    """The QWdiget containing the controls for the cropper tool

    Parameters
    ----------
    napari_viewer : napari.viewer.Viewer
        The parent napari viewer

    Attributes
    ----------
    """

    def __init__(self, napari_viewer):
```
Looks like a regression, the type hint should work
git bisect has it at #2501 ... I'll look into it and push a fix later today.  This is a bit tricky to test as it's due to an import order that doesn't seem to happen when running in pytest.  in fact we _do_ actually test for type annotations with the actual object here: 
https://github.com/napari/napari/blob/e05e2efe1937b353b61e621b6aed93281be8b24f/napari/_qt/_tests/test_plugin_widgets.py#L20-L23

In the meantime, if you put `napari.viewer.Viewer` as a string (forward reference) rather than the actual napari type, I think it works:

```python
def __init__(self, viewer: 'napari.viewer.Viewer'):
```
Cool, thanks for the quick reply @tlambert03 . Putting the type hint is a great workaround for me.

Resolving patch:
diff --git a/napari/plugins/__init__.py b/napari/plugins/__init__.py
--- a/napari/plugins/__init__.py
+++ b/napari/plugins/__init__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import importlib
 import sys
 from inspect import signature
@@ -35,15 +37,7 @@
     from magicgui.widgets import FunctionGui
     from qtpy.QtWidgets import QWidget
 
-
-class PluginHookOption(TypedDict):
-    """Custom type specifying plugin and enabled state."""
-
-    plugin: str
-    enabled: bool
-
-
-CallOrderDict = Dict[str, List[PluginHookOption]]
+    from ..utils.settings._defaults import CallOrderDict
 
 
 class PluginManager(_PM):
diff --git a/napari/utils/settings/_defaults.py b/napari/utils/settings/_defaults.py
--- a/napari/utils/settings/_defaults.py
+++ b/napari/utils/settings/_defaults.py
@@ -1,14 +1,15 @@
 """Settings management.
 """
+from __future__ import annotations
 
 import os
 from enum import Enum
 from pathlib import Path
-from typing import List, Tuple
+from typing import Dict, List, Tuple
 
 from pydantic import BaseSettings, Field
+from typing_extensions import TypedDict
 
-from ...plugins import CallOrderDict
 from .._base import _DEFAULT_LOCALE
 from ..events.evented_model import EventedModel
 from ..notifications import NotificationSeverity
@@ -275,6 +276,16 @@ class NapariConfig:
         ]
 
 
+class PluginHookOption(TypedDict):
+    """Custom type specifying plugin and enabled state."""
+
+    plugin: str
+    enabled: bool
+
+
+CallOrderDict = Dict[str, List[PluginHookOption]]
+
+
 class PluginsSettings(BaseNapariSettings):
     """Plugins Settings."""
 


Test patch: