Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for reactive generator functions #5019

Merged
merged 9 commits into from
Jun 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/how_to/interactivity/bind_component.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,28 @@ pn.Row(slider, select, pn.pane.Markdown(iobject))

This approach is preferred over rendering reactive functions directly because it is more efficient and updates only the specific *Parameters* that are being changed.

If you want to update multiple *Parameters* at the same time you can pass a reactive function (or **Parameter**) as the `refs` keyword argument. The function (or **Parameter**) must return a dictionary of parameters to update, e.g. let's say you wanted to write a function that returns both


```{pyodide}
slider = pn.widgets.IntSlider(value=5, start=1, end=10, name='Number')
select = pn.widgets.RadioButtonGroup(value="⭐", options=["⭐", "🐘"], name='String', align='center')
size = pn.widgets.IntSlider(value=12, start=6, end=24, name='Size')

def refs(string, number, size):
return {
'object': string * number,
'styles': {'font-size': f'{size}pt'}
}

irefs = pn.bind(refs, select, slider, size)

pn.Row(slider, size, select, pn.pane.Markdown(refs=irefs))
```

In this way we can update both the current `object` and the `styles` **Parameter** of the `Markdown` pane simultaneously.

## Related Resources

- Learn [how to use generators with `bind`](./bind_generator)
- Understand [Param](../../explanation/dependencies/param.md)
7 changes: 4 additions & 3 deletions doc/how_to/interactivity/bind_function.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Add interactivity to a function

This guide addresses how to make your functions interactive by binding widgets to them. This is done with the use of `pn.bind`, which binds a function or method to the value of a widget.
This guide addresses how to make your functions interactive by binding widgets to them. This is done with the use of `pn.bind`, which allows binding the value of a widget to a function or method.

---

The recommended approach to adding interactivity to your applications is by writing reactive functions or method. To discover how to write one of these first, we need a function.
The recommended approach to adding interactivity to your applications is by writing reactive functions or methods. To discover how to write one of these first, we need a function.

Let's start by creating a function. The function takes an argument `number` and will return a string of stars equal to the number:

Expand Down Expand Up @@ -43,5 +43,6 @@ Internally the layout will create a so called `ParamFunction` component to wrap

## Related Resources

- Learn [how to use interactive functions to update components](./bind_component)
- Learn [how to use reactive functions to update components](./bind_component)
- Learn [how to use reactive generators to generate interactive components ](./bind_generator)
- Understand [Param](../../explanation/dependencies/param.md)
63 changes: 63 additions & 0 deletions doc/how_to/interactivity/bind_generators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Add interactivity with generators

This guide addresses how to use generators to build interactive components. This is done with the use of `pn.bind`, which binds a function or method to the value of a widget. Compared to simple reactive functions this allows for more complex interactivity.

---

```{pyodide}
import time
import panel as pn

pn.extension()
```

Let us say we have some action that is triggered by a widget, such as a button, and while we are computing the results we want to provide feedback to the user. Using imperative programming this involves writing callbacks that update the current state of our components. This is complex and really we prefer to write reactive components. This is where reactive generators come in.

In the example below we add a `Button` to trigger some calculation. Initially the calculation hasn't yet run, so we check the value provided by the `Button` indicating whether a calculation has been triggered and while it is `False` we `yield` some text and `return`. However, when the `Button` is clicked the function is called again with `run=True` and we kick off some calculation. As this calculation progresses we can `yield` updates and then once the calculation is successful we `yield` again with the final result:

```{pyodide}
run = pn.widgets.Button(name="Press to run calculation", align='center')

def runner(run):
if not run:
yield "Calculation did not run yet"
return
for i in range(101):
time.sleep(0.01) # Some calculation
yield pn.Column(
'Running ({i}/100%)', pn.indicators.Progress(value=i)
)
yield "Success ✅︎"
pn.Row(run, pn.bind(runner, run))
```

This provides a powerful mechanism for providing incrememental updates as we load some data, perform some data processing processing.

This can also be combined with asynchronous processing, e.g. to dynamically stream in new data as it arrives:

```{pyodide}
import random

async def slideshow():
index = 0
while True:
url = f"https://picsum.photos/800/300?image={index}"

if pn.state._is_pyodide:
from pyodide.http import pyfetch
img, _ = await asyncio.gather(pyfetch(url), asyncio.sleep(1))
yield pn.pane.JPG(await img.bytes())

import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
img, _ = await asyncio.gather(resp.read(), asyncio.sleep(1))
yield pn.pane.JPG(img)
index = (index + 1) % 10

pn.Row(slideshow)
```

## Related Resources

- Learn [how to use async callbacks to perform operations concurrently](../concurrency/async.md)
10 changes: 10 additions & 0 deletions doc/how_to/interactivity/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ This section will help you add interactivity to your applications and explorator
Discover how to bind widgets to a function to add interactivity.
:::


:::{grid-item-card} {octicon}`gear;2.5em;sd-mr-1 sd-animate-grow50` Build self-updating components
:link: bind_generators
:link-type: doc

How to use Python generators with `pn.bind` to build components that update themselves.
:::


:::{grid-item-card} {octicon}`package-dependencies;2.5em;sd-mr-1 sd-animate-grow50` Add reactivity to components
:link: bind_component
:link-type: doc
Expand All @@ -34,6 +43,7 @@ How to use `hvplot.interactive` with widgets to make your data workflows interac
:maxdepth: 1

bind_function
bind_generators
bind_component
hvplot_interactive
```
32 changes: 26 additions & 6 deletions panel/depends.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sys

from inspect import isasyncgenfunction

import param

from param.parameterized import iscoroutinefunction
Expand Down Expand Up @@ -183,7 +185,7 @@ def _param_bind(function, *args, watch=False, **kwargs):
elif isinstance(v, param.Parameter):
dependencies[kw] = v

def combine_arguments(wargs, wkwargs):
def combine_arguments(wargs, wkwargs, asynchronous=False):
combined_args = []
for arg in args:
if hasattr(arg, '_dinfo'):
Expand All @@ -200,7 +202,13 @@ def combine_arguments(wargs, wkwargs):
arg = getattr(arg.owner, arg.name)
combined_kwargs[kw] = arg
for kw, arg in wkwargs.items():
if kw.startswith('__arg') or kw.startswith('__kwarg') or kw.startswith('__fn'):
if asynchronous:
if kw.startswith('__arg'):
combined_args[int(kw[5:])] = arg
elif kw.startswith('__kwarg'):
combined_kwargs[kw[8:]] = arg
continue
elif kw.startswith('__arg') or kw.startswith('__kwarg') or kw.startswith('__fn'):
continue
combined_kwargs[kw] = arg
return combined_args, combined_kwargs
Expand All @@ -216,12 +224,24 @@ def eval_fn():
fn = eval_function(p)
return fn

if iscoroutinefunction(function):
if isasyncgenfunction(function):
async def wrapped(*wargs, **wkwargs):
combined_args, combined_kwargs = combine_arguments(
wargs, wkwargs, asynchronous=True
)
evaled = eval_fn()(*combined_args, **combined_kwargs)
async for val in evaled:
yield val
wrapper_fn = depends(**dependencies, watch=watch)(wrapped)
wrapped._dinfo = wrapper_fn._dinfo
elif iscoroutinefunction(function):
@depends(**dependencies, watch=watch)
async def wrapped(*wargs, **wkwargs):
combined_args, combined_kwargs = combine_arguments(wargs, wkwargs)

return await eval_fn()(*combined_args, **combined_kwargs)
combined_args, combined_kwargs = combine_arguments(
wargs, wkwargs, asynchronous=True
)
evaled = eval_fn()(*combined_args, **combined_kwargs)
return await evaled
else:
@depends(**dependencies, watch=watch)
def wrapped(*wargs, **wkwargs):
Expand Down
3 changes: 2 additions & 1 deletion panel/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,8 @@ def __init__(self, *objects: Any, **params: Any):
"not both." % type(self).__name__)
params['objects'] = [panel(pane) for pane in objects]
elif 'objects' in params:
params['objects'] = [panel(pane) for pane in params['objects']]
if not hasattr(params['objects'], '_dinfo'):
params['objects'] = [panel(pane) for pane in params['objects']]
super(Panel, self).__init__(**params)

@property
Expand Down
31 changes: 26 additions & 5 deletions panel/param.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from __future__ import annotations

import asyncio
import inspect
import itertools
import json
Expand All @@ -17,7 +18,8 @@
from contextlib import contextmanager
from functools import partial
from typing import (
TYPE_CHECKING, Any, ClassVar, List, Mapping, Optional, Tuple, Type,
TYPE_CHECKING, Any, ClassVar, Generator, List, Mapping, Optional, Tuple,
Type,
)

import param
Expand Down Expand Up @@ -770,6 +772,7 @@ class ParamMethod(ReplacementPane):

def __init__(self, object=None, **params):
super().__init__(object, **params)
self._async_task = None
self._evaled = not (self.lazy or self.defer_load)
self._link_object_params()
if object is not None:
Expand Down Expand Up @@ -803,10 +806,24 @@ def eval(self, function):
return eval_function(function)

async def _eval_async(self, awaitable):
if self._async_task:
self._async_task.cancel()
self._async_task = task = asyncio.current_task()
curdoc = state.curdoc
has_context = bool(curdoc.session_context) if curdoc else False
if has_context:
curdoc.on_session_destroyed(lambda context: task.cancel())
try:
new_object = await awaitable
self._update_inner(new_object)
if isinstance(awaitable, types.AsyncGeneratorType):
async for new_obj in awaitable:
self._update_inner(new_obj)
else:
self._update_inner(await awaitable)
except Exception as e:
if not curdoc or (has_context and curdoc.session_context):
raise e
finally:
self._async_task = None
self._inner_layout.loading = False

def _replace_pane(self, *args, force=False):
Expand All @@ -821,10 +838,14 @@ def _replace_pane(self, *args, force=False):
new_object = Spacer()
else:
new_object = self.eval(self.object)
if inspect.isawaitable(new_object):
if inspect.isawaitable(new_object) or isinstance(new_object, types.AsyncGeneratorType):
param.parameterized.async_executor(partial(self._eval_async, new_object))
return
self._update_inner(new_object)
elif isinstance(new_object, Generator):
for new_obj in new_object:
self._update_inner(new_obj)
else:
self._update_inner(new_object)
finally:
self._inner_layout.loading = False

Expand Down
Loading
Loading