Skip to content

Commit

Permalink
Improved bokeh server functionality (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Mar 2, 2019
1 parent ebead54 commit 36ec88b
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 35 deletions.
32 changes: 23 additions & 9 deletions panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import sys

import param as _param
from bokeh.document import Document as _Document

from . import holoviews # noqa
from . import layout # noqa
from . import param # noqa
from . import pipeline # noqa
from . import plotly # noqa
from . import vega # noqa
Expand All @@ -17,11 +21,21 @@
from .util import load_notebook as _load_nb
from .viewable import Viewable

import param
from pyviz_comms import JupyterCommManager, extension as _pyviz_extension
from pyviz_comms import JupyterCommManager as _JupyterCommManager, extension as _pyviz_extension

__version__ = str(_param.version.Version(
fpath=__file__, archive_commit="$Format:%h$", reponame="panel"))


class state(_param.Parameterized):
"""
Holds global state associated with running apps, allowing running
apps to indicate their state to a user.
"""

__version__ = str(param.version.Version(fpath=__file__, archive_commit="$Format:%h$",
reponame="panel"))
curdoc = _param.ClassSelector(class_=_Document, doc="""
The bokeh Document for which a server event is currently being
processed.""")


class extension(_pyviz_extension):
Expand All @@ -30,7 +44,7 @@ class extension(_pyviz_extension):
bokeh and enable comms.
"""

inline = param.Boolean(default=True, doc="""
inline = _param.Boolean(default=True, doc="""
Whether to inline JS and CSS resources.
If disabled, resources are loaded from CDN if one is available.""")

Expand All @@ -43,16 +57,16 @@ def __call__(self, *args, **params):
except:
return

p = param.ParamOverrides(self, params)
p = _param.ParamOverrides(self, params)
if hasattr(ip, 'kernel') and not self._loaded:
# TODO: JLab extension and pyviz_comms should be changed
# to allow multiple cleanup comms to be registered
JupyterCommManager.get_client_comm(self._process_comm_msg,
"hv-extension-comm")
_JupyterCommManager.get_client_comm(self._process_comm_msg,
"hv-extension-comm")
_load_nb(p.inline)
self._loaded = True

Viewable._comm_manager = JupyterCommManager
Viewable._comm_manager = _JupyterCommManager

if 'holoviews' in sys.modules:
import holoviews as hv
Expand Down
5 changes: 3 additions & 2 deletions panel/holoviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
return model

def _link_widgets(self, pane, root, comm):
from . import state

def update_plot(change):
from holoviews.core.util import cross_index
Expand All @@ -135,11 +136,11 @@ def update_plot(change):
if comm:
plot.update(key)
plot.push()
elif state.curdoc:
plot.update(key)
else:

def update_plot():
plot.update(key)

plot.document.add_next_tick_callback(update_plot)
else:
plot.update(key)
Expand Down
3 changes: 3 additions & 0 deletions panel/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(self, *objects, **params):
super(Panel, self).__init__(objects=objects, **params)

def _link_params(self, model, params, doc, root, comm=None):
from . import state
def set_value(*events):
msg = {event.name: event.new for event in events}
events = {event.name: event for event in events}
Expand All @@ -54,6 +55,8 @@ def update_model():
if comm:
update_model()
push(doc, comm)
elif state.curdoc:
update_model()
else:
doc.add_next_tick_callback(update_model)

Expand Down
3 changes: 3 additions & 0 deletions panel/pane.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def _link_object(self, doc, root, parent, comm=None):
Links the object parameter to the rendered Bokeh model, triggering
an update when the object changes.
"""
from . import state
ref = root.ref['id']

def update_pane(change):
Expand Down Expand Up @@ -197,6 +198,8 @@ def update_models():
if comm:
update_models()
push(doc, comm)
elif state.curdoc:
update_models()
else:
doc.add_next_tick_callback(update_models)

Expand Down
25 changes: 25 additions & 0 deletions panel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import inspect
import numbers
import hashlib
import threading

from collections import defaultdict, MutableSequence, MutableMapping, OrderedDict
from datetime import datetime
Expand Down Expand Up @@ -213,6 +214,30 @@ def __call__(self, pname):
return pname


class StoppableThread(threading.Thread):
"""Thread class with a stop() method."""

def __init__(self, io_loop=None, timeout=1000, **kwargs):
from tornado import ioloop
super(StoppableThread, self).__init__(**kwargs)
self._stop_event = threading.Event()
self.io_loop = io_loop
self._cb = ioloop.PeriodicCallback(self._check_stopped, timeout)
self._cb.start()

def _check_stopped(self):
if self.stopped:
self._cb.stop()
self.io_loop.stop()

def stop(self):
self._stop_event.set()

@property
def stopped(self):
return self._stop_event.is_set()


################################
# Display and update utilities #
################################
Expand Down
71 changes: 47 additions & 24 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,24 @@ def app(self, notebook_url="localhost:8888"):
"""
show(self._modify_doc, notebook_url=notebook_url)

def show(self, port=0, websocket_origin=None):
def _start_server(self, app, loop, port, websocket_origin):
if websocket_origin and not isinstance(websocket_origin, list):
websocket_origin = [websocket_origin]
opts = dict(allow_websocket_origin=websocket_origin) if websocket_origin else {}
loop.make_current()
opts['io_loop'] = loop
server = Server({'/': app}, port=port, **opts)
def show_callback():
server.show('/')
server.io_loop.add_callback(show_callback)
server.start()
try:
loop.start()
except RuntimeError:
pass
return server

def show(self, port=0, websocket_origin=None, threaded=False):
"""
Starts a bokeh server and displays the Viewable in a new tab
Expand All @@ -347,39 +364,40 @@ def show(self, port=0, websocket_origin=None):
an external web site.
If None, "localhost" is used.
threaded: boolean (optiona)
Whether to launch the Server on a separate thread, allowing
interactive use.
Returns
-------
server: bokeh Server instance
server: bokeh.server.Server or threading.Thread
Returns the bokeh server instance or the thread the server
was launched on (if threaded=True)
"""
def modify_doc(doc):
return self.server_doc(doc)
handler = FunctionHandler(modify_doc)
app = Application(handler)

from tornado.ioloop import IOLoop
loop = IOLoop.current()
if websocket_origin and not isinstance(websocket_origin, list):
websocket_origin = [websocket_origin]
opts = dict(allow_websocket_origin=websocket_origin) if websocket_origin else {}
opts['io_loop'] = loop
server = Server({'/': app}, port=port, **opts)
def show_callback():
server.show('/')
server.io_loop.add_callback(show_callback)
server.start()
if threaded:
from .util import StoppableThread
loop = IOLoop()
server = StoppableThread(target=self._start_server, io_loop=loop,
args=(app, loop, port, websocket_origin))
server.start()
else:
loop = IOLoop.current()
server = self._start_server(app, loop, port, websocket_origin)

def sig_exit(*args, **kwargs):
loop.add_callback_from_signal(do_stop)
def sig_exit(*args, **kwargs):
loop.add_callback_from_signal(do_stop)

def do_stop(*args, **kwargs):
loop.stop()
def do_stop(*args, **kwargs):
loop.stop()

signal.signal(signal.SIGINT, sig_exit)

signal.signal(signal.SIGINT, sig_exit)
try:
loop.start()
except RuntimeError:
pass
return server


Expand Down Expand Up @@ -575,6 +593,7 @@ def _process_param_change(self, msg):
return properties

def _link_params(self, model, params, doc, root, comm=None):
from . import state
def param_change(*events):
msgs = []
for event in events:
Expand Down Expand Up @@ -611,6 +630,8 @@ def update_model():
raise
else:
self._expecting = self._expecting[:-len(msg)]
elif state.curdoc:
update_model()
else:
doc.add_next_tick_callback(update_model)

Expand Down Expand Up @@ -640,17 +661,19 @@ def _comm_change(self, msg):
def _server_change(self, doc, attr, old, new):
self._events.update({attr: new})
if not self._active:
doc.add_timeout_callback(self._change_event, self._debounce)
self._active = list(self._events)
doc.add_timeout_callback(partial(self._change_event, doc), self._debounce)

def _change_event(self):
def _change_event(self, doc=None):
from . import state
try:
state.curdoc = doc
self.set_param(**self._process_property_change(self._events))
except:
raise
finally:
self._events = {}
self._active = []
state.curdoc = None

def _get_customjs(self, change, client_comm, plot_id):
"""
Expand Down
4 changes: 4 additions & 0 deletions panel/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,8 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
return model

def _link_params(self, model, slider, div, params, doc, root, comm=None):
from . import state

def param_change(*events):
combined_msg = {}
for event in events:
Expand All @@ -687,6 +689,8 @@ def update_model():
if comm:
update_model()
push(doc, comm)
elif state.curdoc:
update_model()
else:
doc.add_next_tick_callback(update_model)

Expand Down

0 comments on commit 36ec88b

Please sign in to comment.