Skip to content

Commit

Permalink
Callbacks enhancements and docs (#1507)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Jul 29, 2020
1 parent cf60b75 commit 844c883
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 156 deletions.
95 changes: 94 additions & 1 deletion examples/user_guide/Deploy_and_Export.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
Expand Down Expand Up @@ -496,7 +498,98 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"### Accessing the Bokeh model\n",
"## Scheduling callbacks\n",
"\n",
"When you build an app you frequently want to schedule a callback to be run periodically to refresh the data and update visual components. Additionally if you want to update Bokeh components directly you may need to schedule a callback to get around Bokeh's document lock to avoid errors like this:\n",
"\n",
"> RuntimeError: _pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes\n",
"\n",
"In this section we will discover how we can leverage Bokeh's Document and `pn.state.add_periodic_callback` to set this up.\n",
"\n",
"### Server callbacks\n",
"\n",
"The Bokeh server that Panel builds on is designed to be thread safe which requires a set of locks to avoid multiple threads modifying the Bokeh models simultaneously. Therefore if we want to work with Bokeh models directly we should ensure that any changes to a Bokeh model are executed on the correct thread by adding a callback, which the event loop will then execute safely.\n",
"\n",
"In the example below we will launch an application on a thread using `pn.serve` and make the Bokeh plot (in practice you may provide handles to this object on a class). Finally we will wait 1 second until the server is launched and schedule a callback which updates the `y_range` by accessing the `Document` and calling `add_next_tick_callback` on it. This pattern will ensure that the update to the Bokeh model is executed on the correct thread:\n",
"\n",
"```python\n",
"import time\n",
"import panel as pn\n",
"\n",
"from bokeh.plotting import figure\n",
"\n",
"global p\n",
"p = None\n",
"\n",
"def app():\n",
" global p\n",
" doc = pn.state.curdoc\n",
" p = figure()\n",
" p.line([1, 2, 3], [1, 2, 3])\n",
" return p\n",
"\n",
"pn.serve(app, threaded=True)\n",
"\n",
"time.sleep(1)\n",
"\n",
"p.document.add_next_tick_callback(lambda: p.y_range.update(start=0, end=4))\n",
"```\n",
"\n",
"### Periodic callbacks\n",
"\n",
"As we discussed above periodic callbacks allow periodically updating your application with new data. Below we will create a simple Bokeh plot and display it with Panel:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from bokeh.models import ColumnDataSource\n",
"from bokeh.plotting import figure\n",
"\n",
"source = ColumnDataSource({\"x\": range(10), \"y\": range(10)})\n",
"p = figure()\n",
"p.line(x=\"x\", y=\"y\", source=source)\n",
"\n",
"bokeh_pane = pn.pane.Bokeh(p)\n",
"bokeh_pane.servable()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we will define a callback that updates the data on the `ColumnDataSource` and use the `pn.state.add_periodic_callback` method to schedule updates every 200 ms. We will also set a timeout of 5 seconds after which the callback will automatically stop."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def update():\n",
" data = np.random.randint(0, 2 ** 31, 10)\n",
" source.data.update({\"y\": data})\n",
" bokeh_pane.param.trigger('object') # Only needed in notebook\n",
"\n",
"cb = pn.state.add_periodic_callback(update, 200, timeout=5000)"
]
},
{
"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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Accessing the Bokeh model\n",
"\n",
"Since Panel is built on top of Bokeh, all Panel objects can easily be converted to a Bokeh model. The ``get_root`` method returns a model representing the contents of a Panel:"
]
Expand Down
4 changes: 3 additions & 1 deletion examples/user_guide/Overview.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@
"\n",
"> #### Methods\n",
"\n",
"> - `add_periodic_callback`: Schedules a periodic callback to be run at an interval set by the period\n",
"> - `kill_all_servers`: Stops all running server sessions.\n",
"> - `onload`: Allows defining a callback which is run when a server is fully loaded"
"> - `onload`: Allows defining a callback which is run when a server is fully loaded\n",
"> - `sync_busy`: Sync an indicator with a boolean value parameter to the busy property on state"
]
}
],
Expand Down
81 changes: 6 additions & 75 deletions panel/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,12 @@
Defines callbacks to be executed on a thread or by scheduling it
on a running bokeh server.
"""
from __future__ import absolute_import, division, unicode_literals


import time
import param

from .io.state import state


class PeriodicCallback(param.Parameterized):
"""
Periodic encapsulates a periodic callback which will run both
in tornado based notebook environments and on bokeh server. By
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.
"""

callback = param.Callable(doc="""
The callback to execute periodically.""")

count = param.Integer(default=None, doc="""
Number of times the callback will be executed, by default
this is unlimited.""")

period = param.Integer(default=500, doc="""
Period in milliseconds at which the callback is executed.""")

timeout = param.Integer(default=None, doc="""
Timeout in seconds from the start time at which the callback
expires""")

def __init__(self, **params):
super(PeriodicCallback, self).__init__(**params)
self._counter = 0
self._start_time = None
self._timeout = None
self._cb = None
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('period', watch=True)
def _update_period(self):
if self._cb:
self.stop()
self.start()

def _periodic_callback(self):
self.callback()
self._counter += 1
if self._timeout is not None:
dt = (time.time() - self._start_time)
if dt > self._timeout:
self.stop()
if self._counter == self.count:
self.stop()

def stop(self):
self._counter = 0
self._timeout = None
if self._doc:
self._doc.remove_periodic_callback(self._cb)
else:
self._cb.stop()
self._cb = None
from .io.callbacks import PeriodicCallback # noqa

param.main.param.warning(
"panel.callbacks module is deprecated and has been moved to "
"panel.io.callbacks. Update your import as it will be removed "
"in the next minor release."
)
1 change: 1 addition & 0 deletions panel/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from ..config import config

from .callbacks import PeriodicCallback # noqa
from .embed import embed_state # noqa
from .state import state # noqa
from .model import add_to_doc, remove_root, diff # noqa
Expand Down
79 changes: 79 additions & 0 deletions panel/io/callbacks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Defines callbacks to be executed on a thread or by scheduling it
on a running bokeh server.
"""
from __future__ import absolute_import, division, unicode_literals

import time
import param

from .state import state


class PeriodicCallback(param.Parameterized):
"""
Periodic encapsulates a periodic callback which will run both
in tornado based notebook environments and on bokeh server. By
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.
"""

callback = param.Callable(doc="""
The callback to execute periodically.""")

count = param.Integer(default=None, doc="""
Number of times the callback will be executed, by default
this is unlimited.""")

period = param.Integer(default=500, doc="""
Period in milliseconds at which the callback is executed.""")

timeout = param.Integer(default=None, doc="""
Timeout in milliseconds from the start time at which the callback
expires.""")

def __init__(self, **params):
super(PeriodicCallback, self).__init__(**params)
self._counter = 0
self._start_time = None
self._cb = None
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('period', watch=True)
def _update_period(self):
if self._cb:
self.stop()
self.start()

def _periodic_callback(self):
self.callback()
self._counter += 1
if self.timeout is not None:
dt = (time.time() - self._start_time) * 1000
if dt > self.timeout:
self.stop()
if self._counter == self.count:
self.stop()

def stop(self):
self._counter = 0
self._timeout = None
if self._doc:
self._doc.remove_periodic_callback(self._cb)
else:
self._cb.stop()
self._cb = None
25 changes: 19 additions & 6 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from bokeh.server.views.static_handler import StaticHandler
from bokeh.server.urls import per_app_patterns
from bokeh.settings import settings
from tornado.ioloop import IOLoop
from tornado.websocket import WebSocketHandler
from tornado.web import RequestHandler, StaticFileHandler, authenticated
from tornado.wsgi import WSGIContainer
Expand Down Expand Up @@ -152,7 +153,7 @@ def unlocked():

def serve(panels, port=0, address=None, websocket_origin=None, loop=None,
show=True, start=True, title=None, verbose=True, location=True,
**kwargs):
threaded=False, **kwargs):
"""
Allows serving one or more panel objects on a single server.
The panels argument should be either a Panel object or a function
Expand Down Expand Up @@ -191,11 +192,26 @@ def serve(panels, port=0, address=None, websocket_origin=None, loop=None,
location : boolean or panel.io.location.Location
Whether to create a Location component to observe and
set the URL location.
threaded: boolean (default=False)
Whether to start the server on a new Thread
kwargs: dict
Additional keyword arguments to pass to Server instance
"""
return get_server(panels, port, address, websocket_origin, loop,
show, start, title, verbose, location, **kwargs)
kwargs = dict(kwargs, **dict(
port=port, address=address, websocket_origin=websocket_origin,
loop=loop, show=show, start=start, title=title, verbose=verbose,
location=location
))
if threaded:
from tornado.ioloop import IOLoop
kwargs['loop'] = loop = IOLoop() if loop is None else loop
server = StoppableThread(
target=get_server, io_loop=loop, args=(panels,), kwargs=kwargs
)
server.start()
else:
server = get_server(panels, **kwargs)
return server


class ProxyFallbackHandler(RequestHandler):
Expand All @@ -215,7 +231,6 @@ def prepare(self):
self.on_finish()



def get_static_routes(static_dirs):
"""
Returns a list of tornado routes of StaticFileHandlers given a
Expand Down Expand Up @@ -301,8 +316,6 @@ def get_server(panel, port=0, address=None, websocket_origin=None,
server : bokeh.server.server.Server
Bokeh Server instance running this panel
"""
from tornado.ioloop import IOLoop

server_id = kwargs.pop('server_id', uuid.uuid4().hex)
kwargs['extra_patterns'] = extra_patterns = kwargs.get('extra_patterns', [])
if isinstance(panel, dict):
Expand Down
Loading

0 comments on commit 844c883

Please sign in to comment.