Repository: napari/napari

Issue:
magic name guessing no longer works with napari.view_*
## 🐛 Bug

Tragically, it looks like 0e53da7965c96315e74716f25a477c918d1c18ff / #978 broke @kne42's wonderful magic name guessing when using `v = napari.view_image(my_named_data)`. This makes sense, since the generated code is no longer easily detectable as being part of our stack.

One option would be to generate the code at *install time* rather than *run time*. Thoughts @tlambert03?

## To Reproduce

```
import numpy as np
import napari

image = np.random.random((5, 48, 48))

with napari.gui_qt():
    v = napari.Viewer()
    v.add_image(image)
    v2 = napari.view_image(image)
```

Gives this for `v`:

<img width="1200" alt="Screen Shot 2020-08-03 at 11 49 33 pm" src="https://user-images.githubusercontent.com/492549/89190497-e6d1f280-d5e4-11ea-87e8-cc0093bbfb48.png">


and this for `v2`:

<img width="1200" alt="Screen Shot 2020-08-03 at 11 49 28 pm" src="https://user-images.githubusercontent.com/492549/89190801-55af4b80-d5e5-11ea-80ba-83c382b07256.png">



Discussion:

Resolving patch:
diff --git a/napari/view_layers.py b/napari/view_layers.py
--- a/napari/view_layers.py
+++ b/napari/view_layers.py
@@ -66,22 +66,22 @@ def _generate_view_function(layer_string: str, method_name: str = None):
     IPython help, introspection, tab completion, and autodocs.
 
     Here's how it works:
-    1. we define `real_func`, which is the (easier to understand) function that
-       will do the work of creating a new viewer and adding a layer to it.
-    2. we create a **string** (`fakefunc`) that represents how we _would_ have
+    1. we create a **string** (`fakefunc`) that represents how we _would_ have
        typed out the original `view_*` method.
         - `{combo_sig}` is an `inspect.Signature
           <https://docs.python.org/3/library/inspect.html#inspect.Signature>`_
           object (whose string representation is, conveniently, exactly how we
           would have typed the original function).
-        - the inner `real_func({inner_sig})` part is basically how we were
-          typing the body of the `view_*` functions before, e.g.:
-          `(data=data, name=name, scale=scale ...)`
-    3. we compile that string into `view_func_code`
-    4. finally, we actually evaluate the compiled code and add it to the
-       current module's namespace, and provide a `locals()` dict that tells
-       python that the function name `real_func` in the `fakefunc` string
-       actually corresponds to the `real_func` that we previuosly defined.
+        - the inner part is basically how we were typing the body of the
+          `view_*` functions before.  That is, ``Viewer`` kwargs go to the
+          ``Viewer`` constructor, and everything else goes to the ``add_*``
+          method.  Note that we use ``_kw = locals()`` to get a dict of all
+          arguments passed to the function.
+    2. we compile that string, giving ``__file__`` as the second (``filename``)
+       argument, so it appears that the compiled code comes from this file.
+    3. finally, we evaluate the compiled code and add it to the
+       current module's "globals".  The second argument in the ``eval`` call
+       is a locals() namespace required to interpret the evaluated code.
        (Note: evaluation at this step is essentially exactly what was
        previously happening when python hit each `def view_*` declaration when
        importing `view_layers.py`)
@@ -116,27 +116,25 @@ def _generate_view_function(layer_string: str, method_name: str = None):
     new_params = sorted(new_params, key=lambda p: p.kind)
     combo_sig = add_sig.replace(parameters=new_params)
 
-    # define the actual function that will create a new Viewer and add a layer
-    def real_func(*args, **kwargs):
+    # make new function string with the combined signature
+    fakefunc = f"def view_{layer_string}{combo_sig}:"
+    fakefunc += """
+        _kw = locals()
         view_kwargs = {
-            k: kwargs.pop(k) for k in list(kwargs) if k in view_sig.parameters
+            k: _kw.pop(k) for k in list(_kw) if k in view_sig.parameters
         }
-        viewer = Viewer(**view_kwargs)
-        getattr(viewer, add_string)(*args, **kwargs)
-        return viewer
-
-    # make new function string with the combined signature that calls real_func
-    fname = f'view_{layer_string}'
-    inner_sig = ", ".join([f"{p}={p}" for p in combo_sig.parameters])
-    fakefunc = f"def {fname}{combo_sig}:\n    return real_func({inner_sig})\n"
-
+        viewer = napari.Viewer(**view_kwargs)
+        if 'kwargs' in _kw:
+            _kw.update(_kw.pop("kwargs"))
+    """
+    fakefunc += f"    viewer.{add_string}(**_kw)\n        return viewer"
     # evaluate the new function into the current module namespace
     globals = sys.modules[__name__].__dict__
     eval(
-        compile(fakefunc, "fakesource", "exec"),
+        compile(fakefunc, __file__, "exec"),
         {
-            "real_func": real_func,
             'typing': typing,
+            'view_sig': view_sig,
             'Union': typing.Union,
             'List': typing.List,
             'NoneType': type(None),
@@ -145,7 +143,7 @@ def real_func(*args, **kwargs):
         },
         globals,
     )
-    view_func = globals[fname]  # this is the final function.
+    view_func = globals[f'view_{layer_string}']  # this is the final function.
     view_func.__doc__ = merge_docs(add_method, layer_string)
 
 


Test patch:
diff --git a/napari/_tests/test_cli.py b/napari/_tests/test_cli.py
--- a/napari/_tests/test_cli.py
+++ b/napari/_tests/test_cli.py
@@ -1,7 +1,11 @@
-from napari import __main__
 import sys
-import pytest
 from contextlib import contextmanager
+from unittest.mock import MagicMock, call
+
+import pytest
+
+import napari
+from napari import __main__
 
 
 def test_cli_works(monkeypatch, capsys):
@@ -79,3 +83,26 @@ def test_cli_runscript(monkeypatch, tmp_path):
         m.setattr(sys, 'argv', ['napari', str(script)])
         with pytest.raises(SystemExit):
             __main__.main()
+
+
+def test_cli_passes_kwargs(monkeypatch):
+    """test that we can parse layer keyword arg variants"""
+
+    @contextmanager
+    def gui_qt(**kwargs):
+        yield
+
+    viewer_mock = MagicMock(napari.Viewer)
+    monkeypatch.setattr(napari, 'Viewer', viewer_mock)
+    monkeypatch.setattr(__main__, 'gui_qt', gui_qt)
+    with monkeypatch.context() as m:
+        m.setattr(sys, 'argv', ['n', 'file', '--name', 'some name'])
+        __main__.main()
+    expected = call().open(
+        path=['file'],
+        stack=False,
+        plugin=None,
+        layer_type=None,
+        name='some name',
+    )
+    assert expected in viewer_mock.mock_calls
diff --git a/napari/_tests/test_napari.py b/napari/_tests/test_napari.py
deleted file mode 100644
--- a/napari/_tests/test_napari.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import numpy as np
-import pytest
-
-
-import napari
-from napari._tests.utils import (
-    check_viewer_functioning,
-    layer_test_data,
-    view_layer_type,
-)
-
-
-@pytest.mark.parametrize('layer_type, data, ndim', layer_test_data)
-def test_view(qtbot, layer_type, data, ndim):
-    np.random.seed(0)
-    viewer = view_layer_type(layer_type, data)
-    view = viewer.window.qt_viewer
-    check_viewer_functioning(viewer, view, data, ndim)
-    viewer.close()
-
-
-def test_view_multichannel(qtbot):
-    """Test adding image."""
-    np.random.seed(0)
-    data = np.random.random((15, 10, 5))
-    viewer = napari.view_image(data, channel_axis=-1, show=False)
-    assert len(viewer.layers) == data.shape[-1]
-    for i in range(data.shape[-1]):
-        assert np.all(viewer.layers[i].data == data.take(i, axis=-1))
-    viewer.close()
diff --git a/napari/_tests/test_viewer_layer_parity.py b/napari/_tests/test_view_layers.py
similarity index 76%
rename from napari/_tests/test_viewer_layer_parity.py
rename to napari/_tests/test_view_layers.py
--- a/napari/_tests/test_viewer_layer_parity.py
+++ b/napari/_tests/test_view_layers.py
@@ -5,13 +5,20 @@
 
 import inspect
 import re
+from unittest.mock import MagicMock, call
 
+import numpy as np
 import pytest
-from numpydoc.docscrape import FunctionDoc, ClassDoc
-
-from napari import layers as module, Viewer
-from napari.utils.misc import camel_to_snake, callsignature
+from numpydoc.docscrape import ClassDoc, FunctionDoc
 
+import napari
+from napari import Viewer
+from napari import layers as module
+from napari._tests.utils import (
+    check_viewer_functioning,
+    layer_test_data,
+)
+from napari.utils.misc import callsignature, camel_to_snake
 
 layers = []
 
@@ -155,3 +162,50 @@ def test_signature(layer):
         f'method to layer {name}'
     )
     assert args == autogen, msg
+
+
+@pytest.mark.parametrize('layer_type, data, ndim', layer_test_data)
+def test_view(qtbot, layer_type, data, ndim):
+    np.random.seed(0)
+    viewer = getattr(napari, f'view_{layer_type.__name__.lower()}')(
+        data, show=False
+    )
+    view = viewer.window.qt_viewer
+    check_viewer_functioning(viewer, view, data, ndim)
+    viewer.close()
+
+
+def test_view_multichannel(qtbot):
+    """Test adding image."""
+    np.random.seed(0)
+    data = np.random.random((15, 10, 5))
+    viewer = napari.view_image(data, channel_axis=-1, show=False)
+    assert len(viewer.layers) == data.shape[-1]
+    for i in range(data.shape[-1]):
+        assert np.all(viewer.layers[i].data == data.take(i, axis=-1))
+    viewer.close()
+
+
+def test_kwargs_passed(monkeypatch):
+    viewer_mock = MagicMock(napari.Viewer)
+    monkeypatch.setattr(napari, 'Viewer', viewer_mock)
+    napari.view_path(
+        path='some/path', title='my viewer', name='img name', scale=(1, 2, 3)
+    )
+    assert viewer_mock.mock_calls == [
+        call(
+            title='my viewer',
+            ndisplay=2,
+            order=None,
+            axis_labels=None,
+            show=True,
+        ),
+        call().open(
+            path='some/path',
+            stack=False,
+            plugin=None,
+            layer_type=None,
+            name='img name',
+            scale=(1, 2, 3),
+        ),
+    ]
diff --git a/napari/_tests/utils.py b/napari/_tests/utils.py
--- a/napari/_tests/utils.py
+++ b/napari/_tests/utils.py
@@ -1,4 +1,3 @@
-import napari
 import numpy as np
 from napari import Viewer
 from napari.layers import Image, Labels, Points, Shapes, Vectors, Surface
@@ -35,9 +34,6 @@
     cls: getattr(Viewer, 'add_' + name) for cls, name in zip(classes, names)
 }
 
-layer2viewmethod = {
-    cls: getattr(napari, 'view_' + name) for cls, name in zip(classes, names)
-}
 
 # examples of valid tuples that might be passed to viewer._add_layer_from_data
 good_layer_data = [
@@ -73,20 +69,6 @@ def add_layer_by_type(viewer, layer_type, data, visible=True):
     return layer2addmethod[layer_type](viewer, data, visible=visible)
 
 
-def view_layer_type(layer_type, data):
-    """
-    Convenience method that maps a LayerType to it's view method.
-
-    Parameters
-    ----------
-    layer_type : LayerTypes
-        Layer type to view
-    data
-        The layer data to view
-    """
-    return layer2viewmethod[layer_type](data, show=False)
-
-
 def check_viewer_functioning(viewer, view=None, data=None, ndim=2):
     viewer.dims.ndisplay = 2
     assert np.all(viewer.layers[0].data == data)