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

Improved bokeh server functionality #257

Merged
merged 4 commits into from
Mar 2, 2019
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
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