Repository: holoviz/panel

Issue:
Add `pn.panelize` or at least improvements to `pn.depends`
## Background
I have been playing around with the [stock APIs example](http://panel.pyviz.org/gallery/apis/stocks_hvplot.html) which is totally great and has really been helping me understand the various APIs. 

In the course of my experiments I was surprised that while you can do:

```python 
def get_plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True, width=600)

pn.interact(get_plot, ticker=tickers, window_size=(1, 21, 5))
```

When working with `depends` the syntax becomes:

```python
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)

pn.depends(ticker=ticker.param.value, window_size=window.param.value)(get_plot))
```

This is just one example of how the interact and depends methods are different in kind of strange and surprising way. I know that these methods have been discussed at length, but it seems worth ironing out a little more since these are the first places that users get stuck when first starting out with panel.

## Proposal 1 (less ambitious)
To make depends more user-friendly, `pn.depends` should: 
1) accept a function or class method as the first argument, so that the syntax is the same as `pn.interact`
2) accept widgets in addition to parametrized values.
3) return a panel object (as if you had done `pn.Pane(get_plot)`)


### Example 1
PROPOSED:
```python
pn.depends(get_plot, ticker=ticker, window_size=window)
``` 
CURRENT:
```python
pn.depends(ticker=ticker.param.value, window_size=window.param.value)(get_plot)
``` 

### Example 2
PROPOSED:
```python
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)

@pn.depends(ticker=ticker, window_size=window)
def get_plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True, width=600)

get_plot
```
CURRENT:
```python
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)

@pn.depends(ticker=ticker.param.value, window_size=window.param.value)
def plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True, width=600)

pn.Pane(plot)
```

## Proposal 2 (more ambitious)
I'd like for there to be a combined decorator (perhaps called `pn.panelize`) that would act like `pn.interact` or like `pn.depends` based on what the inputs to the decorator are. 

### Example 1 (maps to interact)
```python
@pn.panelize(ticker=tickers, window_size=(1, 21, 5))
def get_plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True, width=600)

get_plot
```

### Example 2 (maps to depends)
```python
ticker = pn.widgets.Select(name='Ticker', options=tickers)
window = pn.widgets.IntSlider(name='Window Size', value=6, start=1, end=21)

@pn.panelize(ticker=ticker, window_size=window)
def get_plot(ticker, window_size):
    df = get_df(ticker, window_size)
    return df.hvplot.line('date', 'close', grid=True, width=600)

get_plot
```


Discussion:
One wrinkle I can see is that the depends one should not return the widgets whereas the the interact one should. So that might feel unexpected to the user. 
For proposal 1:

> To make depends more user-friendly, pn.depends should:
> 1. accept a function or class method as the first argument, so that the syntax is the same as pn.interact

Do we ever show something like `pn.depends(ticker=ticker.param.value, window_size=window.param.value)(get_plot))` in the docs?  pn.depends is normally used as a decorator, right?  Of course it can be called directly, but as far as I know the intent was purely for it to be used with the decorator syntax, and I think it will confuse users to use it in other ways. People are already uncomfortable enough with decorators used in the typical way!

> 2. accept widgets in addition to parametrized values.

If that's practical, sounds great!  I'd be very happy to get rid of the extra syntax and leave it easier for people to remember. Note that what they currently accept is not actually a parameterized value (i.e. not the value of a parameter), but the Parameter object named "value" on the widget.  So really the proposal is to accept widgets in addition to Parameter objects, automatically finding the "value" Parameter object of the widget and substituting that. Seems like a good idea to me.

> 3. return a panel object (as if you had done pn.Pane(get_plot))

Doesn't that conflict with the semantics of decorators?  Normally a decorator applied to a function will leave that function defined in the namespace, but replaced with a wrapper function.  With the above proposal, any subsequent call to `get_plot` as a function will fail, right?

The issues with 1 and 3 here suggest to me that you maybe want something other than @pn.depends, i.e. some other call that behaves the way that you like, and is not a decorator?



For proposal 2, I'm not quite sure what the plan would be, but I don't think it works, because in both cases if `panelize` is a decorator then `get_plot` should be a function (as a decorated function should still be a function), not a set of widgets or a panel.  It seems to me that you don't want a decorator, just a regular function, which seems easy enough to implement to me:

```
pn.panelize(get_plot, ticker=tickers, window_size=(1, 21, 5))
pn.panelize(get_plot, ticker=ticker, window_size=window)
```
That is an interesting point that decorators shouldn't alter the type of the inner function. This is not the case in the current version of `pn.interact`:

<img width="1021" alt="Screen Shot 2019-09-09 at 3 48 41 PM" src="https://user-images.githubusercontent.com/4806877/64561688-5548fa80-d319-11e9-8869-288de26e78f5.png">

For `interact` we are explicitly copying the interface from ipywidgets, but for `pn.depends` we are following standard usage of decorators.  In ipywidgets the decorator usage is an extra bonus (it's mainly meant as a direct call), but in panel `pn.depends` is intended entirely (or almost entirely) as a decorator.  So I think `pn.depends()` should work like a normal decorator, without any shenanigans like returning a non-function, and we should leave any other behavior to other functions.  @philippjfr would be the one to make the call, really, though.
Ok so maybe an in-between would be to allow `interact` to accept widgets and `param.value`s and allow `depends` to also accept widgets.
I'd be very happy to see that!  
Here is a WIP implementation of panelize anyways. 

```python
def panelize(func, **kwargs):
    parametrized = True
    for k, v in kwargs.items():
        if isinstance(v, pn.param.param.parameterized.Parameter):
            pass
        elif isinstance(v, pn.widgets.base.Widget):
            kwargs[k] = v.param.value
        else:
            parametrized = False
            continue
    if parametrized:
        return pn.Pane(pn.depends(**kwargs)(func))
    return pn.interact(func, **kwargs)
```

Resolving patch:
diff --git a/panel/__init__.py b/panel/__init__.py
--- a/panel/__init__.py
+++ b/panel/__init__.py
@@ -16,8 +16,7 @@
 from .pane import panel, Pane # noqa
 from .param import Param # noqa
 from .template import Template # noqa
-
-depends = _param.depends
+from .depends import depends # noqa
 
 __version__ = str(_param.version.Version(
     fpath=__file__, archive_commit="$Format:%h$", reponame="panel"))
diff --git a/panel/depends.py b/panel/depends.py
new file mode 100644
--- /dev/null
+++ b/panel/depends.py
@@ -0,0 +1,16 @@
+from __future__ import absolute_import, division, unicode_literals
+
+import param
+
+from .widgets import Widget
+
+def param_value_if_widget(arg):
+    if isinstance(arg, Widget):
+        return arg.param.value
+    return arg
+
+def depends(*args, **kwargs):
+    updated_args = [param_value_if_widget(a) for a in  args]
+    updated_kwargs = {k: param_value_if_widget(v) for k, v in kwargs.items()}
+
+    return param.depends(*updated_args, **updated_kwargs)


Test patch: