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

Improve PeriodicCallback #1542

Merged
merged 5 commits into from
Aug 24, 2020
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
46 changes: 45 additions & 1 deletion examples/user_guide/Deploy_and_Export.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,51 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"In a notebook or bokeh server context we should now see the plot update periodically. The other nice thing about this is that `pn.state.add_periodic_callback` returns `PeriodicCallback` we can call `.stop()` and `.start()` on if we want to stop or pause the periodic execution. Additionally we can also dynamically adjust the period to speed up or slow down the callback."
"In a notebook or bokeh server context we should now see the plot update periodically. The other nice thing about this is that `pn.state.add_periodic_callback` returns `PeriodicCallback` we can call `.stop()` and `.start()` on if we want to stop or pause the periodic execution. Additionally we can also dynamically adjust the period by setting the `timeout` parameter to speed up or slow down the callback.\n",
"\n",
"Other nice features on a periodic callback are the ability to check the number of executions using the `cb.counter` property and the ability to toggle the callback on and off simply by setting the running parameter. This makes it possible to link a widget to the running state:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"toggle = pn.widgets.Toggle(name='Toggle callback')\n",
"\n",
"toggle.link(cb, bidirectional=True, value='running')\n",
"toggle"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that when starting a server dynamically with `pn.serve` you cannot start a periodic callback before the application is actually being served. Therefore you should create the application and start the callback in a wrapping function:\n",
"\n",
"```python\n",
"from functools import partial\n",
"\n",
"import numpy as np\n",
"import panel as pn\n",
"\n",
"from bokeh.models import ColumnDataSource\n",
"from bokeh.plotting import figure\n",
"\n",
"def update(source):\n",
" data = np.random.randint(0, 2 ** 31, 10)\n",
" source.data.update({\"y\": data})\n",
"\n",
"def panel_app():\n",
" source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n",
" p = figure()\n",
" p.line(x=\"x\", y=\"y\", source=source)\n",
" cb = pn.state.add_periodic_callback(partial(update, source), 200, timeout=5000)\n",
" return pn.pane.Bokeh(p)\n",
"\n",
"pn.serve(panel_app)\n",
"```"
]
},
{
Expand Down
66 changes: 54 additions & 12 deletions panel/io/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class PeriodicCallback(param.Parameterized):
default the callback will run until the stop method is called,
but count and timeout values can be set to limit the number of
executions or the maximum length of time for which the callback
will run.
will run. The callback may also be started and stopped by setting
the running parameter to True or False respectively.
"""

callback = param.Callable(doc="""
Expand All @@ -35,24 +36,28 @@ class PeriodicCallback(param.Parameterized):
Timeout in milliseconds from the start time at which the callback
expires.""")

running = param.Boolean(default=False, doc="""
Toggles whether the periodic callback is currently running.""")

def __init__(self, **params):
super(PeriodicCallback, self).__init__(**params)
self._counter = 0
self._start_time = None
self._cb = None
self._updating = False
self._doc = None

def start(self):
if self._cb is not None:
raise RuntimeError('Periodic callback has already started.')
self._start_time = time.time()
if state.curdoc and state.curdoc.session_context:
self._doc = state.curdoc
self._cb = self._doc.add_periodic_callback(self._periodic_callback, self.period)
else:
from tornado.ioloop import PeriodicCallback
self._cb = PeriodicCallback(self._periodic_callback, self.period)
self._cb.start()
@param.depends('running', watch=True)
def _start(self):
if not self.running or self._updating:
return
self.start()

@param.depends('running', watch=True)
def _stop(self):
if self.running or self._updating:
return
self.stop()

@param.depends('period', watch=True)
def _update_period(self):
Expand All @@ -76,7 +81,44 @@ def _periodic_callback(self):
if self._counter == self.count:
self.stop()

@property
def counter(self):
"""
Returns the execution count of the periodic callback.
"""
return self._counter

def start(self):
"""
Starts running the periodic callback.
"""
if self._cb is not None:
raise RuntimeError('Periodic callback has already started.')
if not self.running:
try:
self._updating = True
self.running = True
finally:
self._updating = False
self._start_time = time.time()
if state.curdoc and state.curdoc.session_context:
self._doc = state.curdoc
self._cb = self._doc.add_periodic_callback(self._periodic_callback, self.period)
else:
from tornado.ioloop import PeriodicCallback
self._cb = PeriodicCallback(self._periodic_callback, self.period)
self._cb.start()

def stop(self):
"""
Stops running the periodic callback.
"""
if self.running:
try:
self._updating = True
self.running = False
finally:
self._updating = False
self._counter = 0
self._timeout = None
if self._doc:
Expand Down
6 changes: 3 additions & 3 deletions panel/io/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def param_to_jslink(model, widget):
and w not in param_pane._callbacks]

if isinstance(pobj, Reactive):
tgt_links = [Watcher(*l[:-3]) for l in pobj._links]
tgt_links = [Watcher(*l[:-4]) for l in pobj._links]
tgt_watchers = [w for w in get_watchers(pobj) if w not in pobj._callbacks
and w not in tgt_links and w not in param_pane._callbacks]
else:
Expand Down Expand Up @@ -142,7 +142,7 @@ def link_to_jslink(model, source, src_spec, target, tgt_spec):
def links_to_jslinks(model, widget):
from ..widgets import Widget

src_links = [Watcher(*l[:-3]) for l in widget._links]
src_links = [Watcher(*l[:-4]) for l in widget._links]
if any(w not in widget._callbacks and w not in src_links for w in get_watchers(widget)):
return

Expand All @@ -155,7 +155,7 @@ def links_to_jslinks(model, widget):

mappings = []
for pname, tgt_spec in link.links.items():
if Watcher(*link[:-3]) in widget._param_watchers[pname]['value']:
if Watcher(*link[:-4]) in widget._param_watchers[pname]['value']:
mappings.append((pname, tgt_spec))

if mappings:
Expand Down
29 changes: 24 additions & 5 deletions panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .util import edit_readonly
from .viewable import Layoutable, Renderable, Viewable

LinkWatcher = namedtuple("Watcher","inst cls fn mode onlychanged parameter_names what queued target links transformed")
LinkWatcher = namedtuple("Watcher","inst cls fn mode onlychanged parameter_names what queued target links transformed bidirectional_watcher")


class Syncable(Renderable):
Expand Down Expand Up @@ -279,7 +279,7 @@ def add_periodic_callback(self, callback, period=500, count=None,
cb.start()
return cb

def link(self, target, callbacks=None, **links):
def link(self, target, callbacks=None, bidirectional=False, **links):
"""
Links the parameters on this object to attributes on another
object in Python. Supports two modes, either specify a mapping
Expand All @@ -294,6 +294,8 @@ def link(self, target, callbacks=None, **links):
The target object of the link.
callbacks: dict
Maps from a parameter in the source object to a callback.
bidirectional: boolean
Whether to link source and target bi-directionally
**links: dict
Maps between parameters on this object to the parameters
on the supplied object.
Expand All @@ -305,6 +307,10 @@ def link(self, target, callbacks=None, **links):
elif not links and not callbacks:
raise ValueError('Declare parameters to link or a set of '
'callbacks, neither was defined.')
elif callbacks and bidirectional:
raise ValueError('Bidirectional linking not supported for '
'explicit callbacks. You must define '
'separate callbacks for each direction.')

_updating = []
def link(*events):
Expand All @@ -316,13 +322,26 @@ def link(*events):
callbacks[event.name](target, event)
else:
setattr(target, links[event.name], event.new)
except Exception:
raise
finally:
_updating.pop(_updating.index(event.name))
params = list(callbacks) if callbacks else list(links)
cb = self.param.watch(link, params)
link = LinkWatcher(*tuple(cb)+(target, links, callbacks is not None))

bidirectional_watcher = None
if bidirectional:
_reverse_updating = []
reverse_links = {v: k for k, v in links.items()}
def reverse_link(*events):
for event in events:
if event.name in _reverse_updating: continue
_reverse_updating.append(event.name)
try:
setattr(self, reverse_links[event.name], event.new)
finally:
_reverse_updating.remove(event.name)
bidirectional_watcher = target.param.watch(reverse_link, list(reverse_links))

link = LinkWatcher(*tuple(cb)+(target, links, callbacks is not None, bidirectional_watcher))
self._links.append(link)
return cb

Expand Down
17 changes: 16 additions & 1 deletion panel/tests/test_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,22 @@
from panel.tests.util import hv_available


def test_widget_link_bidirectional(document, comm):
def test_widget_link_bidirectional():
t1 = TextInput()
t2 = TextInput()

t1.link(t2, value='value', bidirectional=True)

t1.value = 'ABC'
assert t1.value == 'ABC'
assert t2.value == 'ABC'

t2.value = 'DEF'
assert t1.value == 'DEF'
assert t2.value == 'DEF'


def test_widget_jslink_bidirectional(document, comm):
t1 = TextInput()
t2 = TextInput()

Expand Down