From 3bcecb66c09874f14016cfe4e209d064f398cf0e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 14:47:15 +0000 Subject: [PATCH 01/21] Add support for embedding the state space --- panel/layout.py | 34 ++++++++++++ panel/models/state.py | 19 +++++++ panel/models/state.ts | 55 +++++++++++++++++++ panel/util.py | 122 +++++++++++++++++++++++++++++++++++++++--- panel/viewable.py | 25 ++++++++- 5 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 panel/models/state.py create mode 100644 panel/models/state.ts diff --git a/panel/layout.py b/panel/layout.py index 417f754b2b..1218c98a45 100644 --- a/panel/layout.py +++ b/panel/layout.py @@ -68,10 +68,44 @@ def _update_model(self, events, msg, root, model, doc, comm=None): model.update(**msg) self._preprocess(root) #preprocess links between new elements + def _link_params(self, model, params, doc, root, comm=None): + def set_value(*events): + msg = {event.name: event.new for event in events} + events = {event.name: event for event in events} + + def update_model(): + if 'objects' in msg: + old = events['objects'].old + msg['objects'] = self._get_objects(model, old, doc, root, comm) + for pane in old: + if pane not in self.objects: + pane._cleanup(root) + self._preprocess(root) #preprocess links between new elements + processed = self._process_param_change(msg) + model.update(**processed) + + if comm: + update_model() + if 'embedded' not in root.tags: + push(doc, comm) + else: + doc.add_next_tick_callback(update_model) + + ref = root.ref['id'] + if ref not in self._callbacks: + watcher = self.param.watch(set_value, params) + self._callbacks[ref].append(watcher) + #---------------------------------------------------------------- # Model API #---------------------------------------------------------------- + def _init_properties(self): + properties = {k: v for k, v in self.param.get_param_values() + if v is not None} + del properties['objects'] + return self._process_param_change(properties) + def _get_objects(self, model, old_objects, doc, root, comm=None): """ Returns new child models for the layout while reusing unchanged diff --git a/panel/models/state.py b/panel/models/state.py new file mode 100644 index 0000000000..3e17566025 --- /dev/null +++ b/panel/models/state.py @@ -0,0 +1,19 @@ +import os + +from bokeh.models import Model +from bokeh.core.properties import Dict, Any, List + +from ..util import CUSTOM_MODELS + +class State(Model): + + __implementation__ = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'state.ts') + + state = Dict(Any, Any, help="Contains the recorded state") + + widgets = Dict(Any, Any) + + values = List(Any) + + +CUSTOM_MODELS['panel.models.state.State'] = State diff --git a/panel/models/state.ts b/panel/models/state.ts new file mode 100644 index 0000000000..24750d2f7c --- /dev/null +++ b/panel/models/state.ts @@ -0,0 +1,55 @@ +import * as p from "core/properties" +import {View} from "core/view" +import {copy} from "core/util/array" +import {Model} from "model" + +export class StateView extends View { + model: State + + renderTo(): void { + } +} + +export namespace State { + export type Attrs = p.AttrsOf + + export type Props = Model.Props & { + state: p.Property + values: p.Property + widgets: p.Property<{[key: string]: number}> + } +} + +export interface State extends State.Attrs {} + +export class State extends Model { + properties: State.Props + + constructor(attrs?: Partial) { + super(attrs) + } + + get_state(widget: any): void { + let values: any[] = copy(this.values) + const index: any = this.widgets[widget.id] + values[index] = widget.value + let state: any = this.state + for (const i of values) { + state = state[i] + } + this.values = values + return state + } + + static initClass(): void { + this.prototype.type = "State" + this.prototype.default_view = StateView + + this.define({ + state: [ p.Any, {} ], + widgets: [ p.Any, {} ], + values: [ p.Any, [] ], + }) + } +} +State.initClass() diff --git a/panel/util.py b/panel/util.py index 29bde35163..d1b9a40aa6 100644 --- a/panel/util.py +++ b/panel/util.py @@ -14,18 +14,24 @@ from collections import defaultdict, MutableSequence, MutableMapping, OrderedDict from contextlib import contextmanager from datetime import datetime +from itertools import product from six import string_types import param import bokeh import bokeh.embed.notebook +from bokeh.core.templates import DOC_NB_JS +from bokeh.core.json_encoder import serialize_json from bokeh.document import Document +from bokeh.embed.elements import div_for_render_item +from bokeh.embed.util import standalone_docs_json_and_render_items from bokeh.io.notebook import load_notebook as bk_load_notebook from bokeh.models import Model, LayoutDOM, Box from bokeh.protocol import Protocol from bokeh.resources import CDN, INLINE from bokeh.util.string import encode_utf8 + from pyviz_comms import (PYVIZ_PROXY, JupyterCommManager, bokeh_msg_handler, nb_mime_js, embed_js) @@ -285,6 +291,91 @@ def add_to_doc(obj, doc, hold=False): doc.hold() +def record_events(doc): + events = list(doc._held_events) + if not events: + return None + msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=False) + return {'header': msg.header_json, 'metadata': msg.metadata_json, + 'content': msg.content_json} + + +def embed_state(panel, model, doc, max_states=1000): + """ + Embeds the state of the application on a State model which allows + exporting a static version of an app. This works by finding all + widgets with a predefined set of options and evaluating the cross + product of the widget values and recording the resulting events to + be replayed when exported. The state is recorded on a State model + which is attached as an additional root on the Document. + + Parameters + ---------- + panel: panel.viewable.Reactive + The Reactive component being exported + model: bokeh.model.Model + The bokeh model being exported + doc: bokeh.document.Document + The bokeh Document being exported + max_states: int + The maximum number of states to export + """ + from .models.state import State + from .widgets import Widget + + target = model.ref['id'] + model.tags.append('embedded') + widgets = [w for w in panel.select(Widget) if 'options' in w.params()] + + add_to_doc(model, doc, True) + + keys = [] + values = [] + for w in widgets: + w_model = w._models[target].select_one({'type': w._widget_type}) + js_callback = CustomJS(code=""" + var receiver = new Bokeh.protocol.Receiver() + state = cb_obj.document.roots()[1] + msg = state.get_state(cb_obj) + receiver.consume(msg.header) + receiver.consume(msg.metadata) + receiver.consume(msg.content) + if (receiver.message) + cb_obj.document.apply_json_patch(receiver.message.content) + """) + w_model.js_on_change('value', js_callback) + if isinstance(w.options, list): + values.append((w, w_model, w.options)) + else: + values.append((w, w_model, list(w.options.values()))) + doc._held_events = [] + + state_dict = defaultdict(dict) + restore = [w.value for w, _, _ in values] + init_vals = [m.value for _, m, _ in values] + cross_product = list(product(*[vals[::-1] for _, _, vals in values])) + + if len(cross_product) > max_states: + raise RuntimeError('The cross product of different application ' + 'states is too large to explore (N=%d), either reduce ' + 'the number of options on the widgets or increase ' + 'the max_states specified on static export.' % + len(cross_product)) + + for key in cross_product: + sub_dict = state_dict + for i, k in enumerate(key): + w, m = values[i][:2] + w.value = k + sub_dict[m.value] = record_events(doc) + for (w, _, _), v in zip(values, restore): + w.set_param(value=v) + + state = State(state=state_dict, values=init_vals, + widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) + doc.add_root(state) + + LOAD_MIME = 'application/vnd.holoviews_load.v0+json' EXEC_MIME = 'application/vnd.holoviews_exec.v0+json' HTML_MIME = 'text/html' @@ -368,22 +459,37 @@ def render_mimebundle(model, doc, comm): Displays bokeh output inside a notebook using the PyViz display and comms machinery. """ - if not isinstance(model, LayoutDOM): + if not isinstance(model, LayoutDOM): raise ValueError('Can only render bokeh LayoutDOM models') - add_to_doc(model, doc, True) + return render_model(model, comm) + + +def render_model(model, comm=None): + if not isinstance(model, Model): + raise ValueError("notebook_content expects a single Model instance") target = model.ref['id'] - # Publish plot HTML - bokeh_script, bokeh_div, _ = bokeh.embed.notebook.notebook_content(model) - html = "
{html}
".format(id=target, html=encode_utf8(bokeh_div)) + (docs_json, [render_item]) = standalone_docs_json_and_render_items([model]) + div = div_for_render_item(render_item) + render_item = render_item.to_json() + script = DOC_NB_JS.render( + docs_json=serialize_json(docs_json), + render_items=serialize_json([render_item]), + ) + bokeh_script, bokeh_div = encode_utf8(script), encode_utf8(div) + html = "
{html}
".format(id=target, html=bokeh_div) # Publish bokeh plot JS msg_handler = bokeh_msg_handler.format(plot_id=target) - comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler) - bokeh_js = '\n'.join([comm_js, bokeh_script]) - bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=html) + bokeh_js + + if comm: + comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler) + bokeh_js = '\n'.join([comm_js, bokeh_script]) + bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=html) + bokeh_js + else: + bokeh_js = bokeh_script data = {EXEC_MIME: '', 'text/html': html, 'application/javascript': bokeh_js} metadata = {EXEC_MIME: {'id': target}} diff --git a/panel/viewable.py b/panel/viewable.py index c0e468e899..07ab134174 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -8,6 +8,7 @@ import re import signal import uuid + from functools import partial import param @@ -21,8 +22,10 @@ from pyviz_comms import JS_CALLBACK, JupyterCommManager from .io import state +from .models.state import State from .util import (render_mimebundle, add_to_doc, push, param_reprs, - _origin_url, show_server, ABORT_JS) + embed_state, render_model, _origin_url, show_server, + ABORT_JS) class Layoutable(param.Parameterized): @@ -177,7 +180,6 @@ def __init__(self, **params): super(Layoutable, self).__init__(**params) - class Viewable(Layoutable): """ Viewable is the baseclass all objects in the panel library are @@ -264,6 +266,25 @@ def _repr_mimebundle_(self, include=None, exclude=None): model = self._get_root(doc, comm) return render_mimebundle(model, doc, comm) + def embed(self, max_states=1000): + """ + Renders a static version of a panel in a notebook by evaluating + the set of states defined by the widgets in the model. Note + this will only work well for simple apps with a relatively + small state space. + + Parameters + ---------- + max_states: int + The maximum number of states to embed + """ + from IPython.display import publish_display_data + doc = Document() + comm = Comm() + model = self._get_root(doc, comm) + embed_state(self, model, doc, max_states) + publish_display_data(*render_model(model)) + def _server_destroy(self, session_context): """ Server lifecycle hook triggered when session is destroyed. From 1fd63fda24a27bcb511dc87ca81073edec3262a3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 15:37:51 +0000 Subject: [PATCH 02/21] Minor fixes --- panel/util.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/panel/util.py b/panel/util.py index d1b9a40aa6..1077d11801 100644 --- a/panel/util.py +++ b/panel/util.py @@ -292,10 +292,9 @@ def add_to_doc(obj, doc, hold=False): def record_events(doc): - events = list(doc._held_events) - if not events: - return None - msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=False) + msg = diff(doc, False) + if msg is None: + return {} return {'header': msg.header_json, 'metadata': msg.metadata_json, 'content': msg.content_json} @@ -350,7 +349,6 @@ def embed_state(panel, model, doc, max_states=1000): values.append((w, w_model, list(w.options.values()))) doc._held_events = [] - state_dict = defaultdict(dict) restore = [w.value for w, _, _ in values] init_vals = [m.value for _, m, _ in values] cross_product = list(product(*[vals[::-1] for _, _, vals in values])) @@ -362,12 +360,18 @@ def embed_state(panel, model, doc, max_states=1000): 'the max_states specified on static export.' % len(cross_product)) + nested_dict = lambda: defaultdict(nested_dict) + state_dict = nested_dict() for key in cross_product: sub_dict = state_dict for i, k in enumerate(key): w, m = values[i][:2] w.value = k - sub_dict[m.value] = record_events(doc) + sub_dict = sub_dict[m.value] + events = record_events(doc) + if events: + sub_dict.update(events) + for (w, _, _), v in zip(values, restore): w.set_param(value=v) From 2c46ce8be9485cc99b88e1c087b9eda6bda0ec1a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 16:01:57 +0000 Subject: [PATCH 03/21] Further fixes --- panel/models/state.ts | 18 +++++++++--------- panel/util.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/panel/models/state.ts b/panel/models/state.ts index 24750d2f7c..598a8ec782 100644 --- a/panel/models/state.ts +++ b/panel/models/state.ts @@ -30,15 +30,15 @@ export class State extends Model { } get_state(widget: any): void { - let values: any[] = copy(this.values) - const index: any = this.widgets[widget.id] - values[index] = widget.value - let state: any = this.state + let values: any[] = copy(this.values) + const index: any = this.widgets[widget.id] + values[index] = widget.value + let state: any = this.state for (const i of values) { state = state[i] - } - this.values = values - return state + } + this.values = values + return state } static initClass(): void { @@ -47,8 +47,8 @@ export class State extends Model { this.define({ state: [ p.Any, {} ], - widgets: [ p.Any, {} ], - values: [ p.Any, [] ], + widgets: [ p.Any, {} ], + values: [ p.Any, [] ], }) } } diff --git a/panel/util.py b/panel/util.py index 1077d11801..70d709f972 100644 --- a/panel/util.py +++ b/panel/util.py @@ -491,9 +491,9 @@ def render_model(model, comm=None): if comm: comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler) bokeh_js = '\n'.join([comm_js, bokeh_script]) - bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=html) + bokeh_js else: bokeh_js = bokeh_script + bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=bokeh_div) + bokeh_js data = {EXEC_MIME: '', 'text/html': html, 'application/javascript': bokeh_js} metadata = {EXEC_MIME: {'id': target}} From d6ee0d6bf8689a2ec992f25e9c87468fa39b58e4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 22 Feb 2019 16:12:58 +0000 Subject: [PATCH 04/21] Fixed flakes --- panel/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/util.py b/panel/util.py index 70d709f972..1ac84f559e 100644 --- a/panel/util.py +++ b/panel/util.py @@ -328,7 +328,6 @@ def embed_state(panel, model, doc, max_states=1000): add_to_doc(model, doc, True) - keys = [] values = [] for w in widgets: w_model = w._models[target].select_one({'type': w._widget_type}) From a1052a1e5ae460f829c737c2da59ef67fed7881f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 01:27:40 +0000 Subject: [PATCH 05/21] Various fixes after rebase --- panel/layout.py | 28 --------------------------- panel/pane/base.py | 2 +- panel/pane/holoviews.py | 2 +- panel/viewable.py | 42 +++++++++++++++++++++-------------------- 4 files changed, 24 insertions(+), 50 deletions(-) diff --git a/panel/layout.py b/panel/layout.py index 1218c98a45..6e68053d6a 100644 --- a/panel/layout.py +++ b/panel/layout.py @@ -68,34 +68,6 @@ def _update_model(self, events, msg, root, model, doc, comm=None): model.update(**msg) self._preprocess(root) #preprocess links between new elements - def _link_params(self, model, params, doc, root, comm=None): - def set_value(*events): - msg = {event.name: event.new for event in events} - events = {event.name: event for event in events} - - def update_model(): - if 'objects' in msg: - old = events['objects'].old - msg['objects'] = self._get_objects(model, old, doc, root, comm) - for pane in old: - if pane not in self.objects: - pane._cleanup(root) - self._preprocess(root) #preprocess links between new elements - processed = self._process_param_change(msg) - model.update(**processed) - - if comm: - update_model() - if 'embedded' not in root.tags: - push(doc, comm) - else: - doc.add_next_tick_callback(update_model) - - ref = root.ref['id'] - if ref not in self._callbacks: - watcher = self.param.watch(set_value, params) - self._callbacks[ref].append(watcher) - #---------------------------------------------------------------- # Model API #---------------------------------------------------------------- diff --git a/panel/pane/base.py b/panel/pane/base.py index 2104063c65..67bf8979d2 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -115,7 +115,7 @@ def _update_pane(self, event): viewable, root, doc, comm = state._views[ref] if comm or doc is state.curdoc: self._update_object(model, doc, root, parent, comm) - if comm: + if comm and 'embedded' not in root.tags: push(doc, comm) else: cb = partial(self._update_object, model, doc, root, parent, comm) diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index f0abedc927..6f0811570f 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -94,7 +94,7 @@ def _update_plot(self, plot, pane, event): if isinstance(plot, BokehPlot): if plot.comm or plot.document is state.curdoc: plot.update(key) - if plot.comm: + if plot.comm and 'embedded' not in plot.root.tags: plot.push() else: plot.document.add_next_tick_callback(partial(plot.update, key)) diff --git a/panel/viewable.py b/panel/viewable.py index 07ab134174..a90c7f2f6c 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -266,25 +266,6 @@ def _repr_mimebundle_(self, include=None, exclude=None): model = self._get_root(doc, comm) return render_mimebundle(model, doc, comm) - def embed(self, max_states=1000): - """ - Renders a static version of a panel in a notebook by evaluating - the set of states defined by the widgets in the model. Note - this will only work well for simple apps with a relatively - small state space. - - Parameters - ---------- - max_states: int - The maximum number of states to embed - """ - from IPython.display import publish_display_data - doc = Document() - comm = Comm() - model = self._get_root(doc, comm) - embed_state(self, model, doc, max_states) - publish_display_data(*render_model(model)) - def _server_destroy(self, session_context): """ Server lifecycle hook triggered when session is destroyed. @@ -354,6 +335,25 @@ def app(self, notebook_url="localhost:8888", port=0): show_server(server, notebook_url, server_id) return server + def embed(self, max_states=1000): + """ + Renders a static version of a panel in a notebook by evaluating + the set of states defined by the widgets in the model. Note + this will only work well for simple apps with a relatively + small state space. + + Parameters + ---------- + max_states: int + The maximum number of states to embed + """ + from IPython.display import publish_display_data + doc = Document() + comm = Comm() + model = self._get_root(doc, comm) + embed_state(self, model, doc, max_states) + publish_display_data(*render_model(model)) + def get_server(self, port=0, websocket_origin=None, loop=None, show=False, start=False, **kwargs): """ @@ -613,7 +613,7 @@ def param_change(*events): viewable, root, doc, comm = state._views[ref] if comm or doc is state.curdoc: self._update_model(events, msg, root, model, doc, comm) - if comm: + if comm and 'embedded' not in root.tags: push(doc, comm) else: cb = partial(self._update_model, events, msg, root, model, doc, comm) @@ -628,6 +628,8 @@ def _link_props(self, model, properties, doc, root, comm=None): if comm is None: for p in properties: model.on_change(p, partial(self._server_change, doc)) + elif type(comm) is Comm: + pass else: client_comm = state._comm_manager.get_client_comm(on_msg=self._comm_change) for p in properties: From 6248a04b69a475ea7d3090b847d09ba0ec8612b0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 02:54:34 +0000 Subject: [PATCH 06/21] Various fixes for embedding --- panel/io.py | 303 +++++++++++++++++++++++++++++++++++- panel/links.py | 3 +- panel/models/__init__.py | 1 + panel/pane/base.py | 4 +- panel/pane/plot.py | 2 +- panel/tests/test_util.py | 5 +- panel/tests/test_widgets.py | 2 +- panel/util.py | 289 +--------------------------------- panel/viewable.py | 40 +++-- panel/widgets/base.py | 20 +++ panel/widgets/slider.py | 49 +++--- 11 files changed, 386 insertions(+), 332 deletions(-) diff --git a/panel/io.py b/panel/io.py index 5f0cab907d..960355a0b6 100644 --- a/panel/io.py +++ b/panel/io.py @@ -4,17 +4,36 @@ """ from __future__ import absolute_import, division, unicode_literals +import json import sys +from collections import defaultdict +from contextlib import contextmanager +from itertools import product + import param +import bokeh +import bokeh.embed.notebook from bokeh.document import Document +from bokeh.core.templates import DOC_NB_JS +from bokeh.core.json_encoder import serialize_json +from bokeh.embed.elements import div_for_render_item +from bokeh.embed.util import standalone_docs_json_and_render_items +from bokeh.io.notebook import load_notebook as bk_load_notebook +from bokeh.models import CustomJS, LayoutDOM, Model +from bokeh.protocol import Protocol +from bokeh.resources import CDN, INLINE +from bokeh.util.string import encode_utf8 from pyviz_comms import ( CommManager as _CommManager, JupyterCommManager as _JupyterCommManager, - extension as _pyviz_extension) + extension as _pyviz_extension, PYVIZ_PROXY, bokeh_msg_handler, + nb_mime_js, embed_js) -from .util import load_notebook +#--------------------------------------------------------------------- +# Public API +#--------------------------------------------------------------------- class state(param.Parameterized): """ @@ -26,6 +45,12 @@ class state(param.Parameterized): The bokeh Document for which a server event is currently being processed.""") + embed = param.Boolean(default=False, doc=""" + Whether plot data will be embedded.""") + + # Whether to hold comm events + _hold = False + _comm_manager = _CommManager # An index of all currently active views @@ -76,6 +101,280 @@ def __call__(self, *args, **params): hv.Store.current_backend = 'bokeh' +#--------------------------------------------------------------------- +# Private API +#--------------------------------------------------------------------- + +LOAD_MIME = 'application/vnd.holoviews_load.v0+json' +EXEC_MIME = 'application/vnd.holoviews_exec.v0+json' +HTML_MIME = 'text/html' + +ABORT_JS = """ +if (!window.PyViz) {{ + return; +}} +var receiver = window.PyViz.receivers['{plot_id}']; +var events = receiver ? receiver._partial.content.events : []; +for (var event of events) {{ + if ((event.kind == 'ModelChanged') && (event.attr == '{change}') && + (cb_obj.id == event.model.id) && + (cb_obj['{change}'] == event.new)) {{ + events.pop(events.indexOf(event)) + return; + }} +}} +""" + +def diff(doc, binary=True, events=None): + """ + Returns a json diff required to update an existing plot with + the latest plot data. + """ + events = list(doc._held_events) if events is None else events + if not events or state._hold: + return None + msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) + doc._held_events = [e for e in doc._held_events if e not in events] + return msg + + +@contextmanager +def block_comm(): + """ + Context manager to temporarily block comm push + """ + state._hold = True + yield + state._hold = False + + +def push(doc, comm, binary=True): + """ + Pushes events stored on the document across the provided comm. + """ + msg = diff(doc, binary=binary) + if msg is None: + return + comm.send(msg.header_json) + comm.send(msg.metadata_json) + comm.send(msg.content_json) + for header, payload in msg.buffers: + comm.send(json.dumps(header)) + comm.send(buffers=[payload]) + + +def remove_root(obj, replace=None): + """ + Removes the document from any previously displayed bokeh object + """ + for model in obj.select({'type': Model}): + prev_doc = model.document + model._document = None + if prev_doc: + prev_doc.remove_root(model) + if replace: + model._document = replace + + +def add_to_doc(obj, doc, hold=False): + """ + Adds a model to the supplied Document removing it from any existing Documents. + """ + # Add new root + remove_root(obj) + doc.add_root(obj) + if doc._hold is None and hold: + doc.hold() + + +def record_events(doc): + msg = diff(doc, False) + if msg is None: + return {} + return {'header': msg.header_json, 'metadata': msg.metadata_json, + 'content': msg.content_json} + + +def embed_state(panel, model, doc, max_states=1000): + """ + Embeds the state of the application on a State model which allows + exporting a static version of an app. This works by finding all + widgets with a predefined set of options and evaluating the cross + product of the widget values and recording the resulting events to + be replayed when exported. The state is recorded on a State model + which is attached as an additional root on the Document. + + Parameters + ---------- + panel: panel.viewable.Reactive + The Reactive component being exported + model: bokeh.model.Model + The bokeh model being exported + doc: bokeh.document.Document + The bokeh Document being exported + max_states: int + The maximum number of states to export + """ + from .models.state import State + from .widgets import Widget + + target = model.ref['id'] + model.tags.append('embedded') + discrete_widgets = [w for w in panel.select(Widget) if 'options' in w.params()] + + add_to_doc(model, doc, True) + + values = [] + for w in widgets: + w_model = w._models[target][0].select_one({'type': w._widget_type}) + js_callback = CustomJS(code=""" + var receiver = new Bokeh.protocol.Receiver() + state = cb_obj.document.roots()[1] + msg = state.get_state(cb_obj) + receiver.consume(msg.header) + receiver.consume(msg.metadata) + receiver.consume(msg.content) + if (receiver.message) + cb_obj.document.apply_json_patch(receiver.message.content) + """) + w_model.js_on_change('value', js_callback) + if isinstance(w.options, list): + values.append((w, w_model, w.options)) + else: + values.append((w, w_model, list(w.options.values()))) + doc._held_events = [] + + restore = [w.value for w, _, _ in values] + init_vals = [m.value for _, m, _ in values] + cross_product = list(product(*[vals[::-1] for _, _, vals in values])) + + if len(cross_product) > max_states: + raise RuntimeError('The cross product of different application ' + 'states is too large to explore (N=%d), either reduce ' + 'the number of options on the widgets or increase ' + 'the max_states specified on static export.' % + len(cross_product)) + + nested_dict = lambda: defaultdict(nested_dict) + state_dict = nested_dict() + for key in cross_product: + sub_dict = state_dict + for i, k in enumerate(key): + w, m = values[i][:2] + w.value = k + sub_dict = sub_dict[m.value] + events = record_events(doc) + if events: + sub_dict.update(events) + + for (w, _, _), v in zip(values, restore): + w.set_param(value=v) + + state = State(state=state_dict, values=init_vals, + widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) + doc.add_root(state) + + +def load_notebook(inline=True): + from IPython.display import publish_display_data + + # Create a message for the logo (if shown) + LOAD_MIME_TYPE = bokeh.io.notebook.LOAD_MIME_TYPE + bokeh.io.notebook.LOAD_MIME_TYPE = LOAD_MIME + bk_load_notebook(hide_banner=True, resources=INLINE if inline else CDN) + bokeh.io.notebook.LOAD_MIME_TYPE = LOAD_MIME_TYPE + bokeh.io.notebook.curstate().output_notebook() + + # Publish comm manager + JS = '\n'.join([PYVIZ_PROXY, _JupyterCommManager.js_manager, nb_mime_js]) + publish_display_data(data={LOAD_MIME: JS, 'application/javascript': JS}) + + +def _origin_url(url): + if url.startswith("http"): + url = url.split("//")[1] + return url + +def _server_url(url, port): + if url.startswith("http"): + return '%s:%d%s' % (url.rsplit(':', 1)[0], port, "/") + else: + return 'http://%s:%d%s' % (url.split(':')[0], port, "/") + + +def show_server(server, notebook_url, server_id): + """ + Displays a bokeh server inline in the notebook. + + Parameters + ---------- + server: bokeh.server.server.Server + Bokeh server instance which is already running + notebook_url: str + The URL of the running Jupyter notebook server + server_id: str + Unique ID to identify the server with + """ + from bokeh.embed import server_document + from IPython.display import publish_display_data + + if callable(notebook_url): + url = notebook_url(server.port) + else: + url = _server_url(notebook_url, server.port) + + script = server_document(url, resources=None) + + publish_display_data({ + HTML_MIME: script, + EXEC_MIME: "" + }, metadata={ + EXEC_MIME: {"server_id": server_id} + }) + + +def render_mimebundle(model, doc, comm): + """ + Displays bokeh output inside a notebook using the PyViz display + and comms machinery. + """ + if not isinstance(model, LayoutDOM): + raise ValueError('Can only render bokeh LayoutDOM models') + add_to_doc(model, doc, True) + return render_model(model, comm) + + +def render_model(model, comm=None): + if not isinstance(model, Model): + raise ValueError("notebook_content expects a single Model instance") + + target = model.ref['id'] + + (docs_json, [render_item]) = standalone_docs_json_and_render_items([model]) + div = div_for_render_item(render_item) + render_item = render_item.to_json() + script = DOC_NB_JS.render( + docs_json=serialize_json(docs_json), + render_items=serialize_json([render_item]), + ) + bokeh_script, bokeh_div = encode_utf8(script), encode_utf8(div) + html = "
{html}
".format(id=target, html=bokeh_div) + + # Publish bokeh plot JS + msg_handler = bokeh_msg_handler.format(plot_id=target) + + if comm: + comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler) + bokeh_js = '\n'.join([comm_js, bokeh_script]) + else: + bokeh_js = bokeh_script + bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=bokeh_div) + bokeh_js + + data = {EXEC_MIME: '', 'text/html': html, 'application/javascript': bokeh_js} + metadata = {EXEC_MIME: {'id': target}} + return data, metadata + + def _cleanup_panel(msg_id): """ A cleanup action which is called when a plot is deleted in the notebook diff --git a/panel/links.py b/panel/links.py index e106382bc1..ef90a276f5 100644 --- a/panel/links.py +++ b/panel/links.py @@ -11,6 +11,7 @@ from .layout import Panel from .pane.holoviews import HoloViews, generate_panel_bokeh_map, is_bokeh_element_plot from .util import unicode_repr +from .widgets import CompositeWidget from bokeh.models import (CustomJS, Model as BkModel) @@ -94,7 +95,7 @@ def unlink(self): @classmethod def _process_links(cls, root_view, root_model): - if not isinstance(root_view, Panel) or not root_model: + if not isinstance(root_view, (Panel, CompositeWidget)) or not root_model: return linkable = root_view.select(Viewable) diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 023ad0e05d..19f3dab5de 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -6,4 +6,5 @@ """ from .plots import PlotlyPlot, VegaPlot # noqa +from .state import State # noqa from .widgets import Audio, FileInput, Player # noqa diff --git a/panel/pane/base.py b/panel/pane/base.py index 67bf8979d2..44b0be97b7 100644 --- a/panel/pane/base.py +++ b/panel/pane/base.py @@ -8,10 +8,10 @@ import param -from ..io import state +from ..io import push, state from ..layout import Panel, Row from ..viewable import Viewable, Reactive, Layoutable -from ..util import param_reprs, push +from ..util import param_reprs def Pane(obj, **kwargs): diff --git a/panel/pane/plot.py b/panel/pane/plot.py index 303e7a691e..560339582a 100644 --- a/panel/pane/plot.py +++ b/panel/pane/plot.py @@ -11,7 +11,7 @@ from bokeh.models import LayoutDOM, CustomJS, Spacer as BkSpacer -from ..util import remove_root +from ..io import remove_root from .base import PaneBase from .markup import HTML from .image import PNG diff --git a/panel/tests/test_util.py b/panel/tests/test_util.py index e5750510a5..ea1767c625 100644 --- a/panel/tests/test_util.py +++ b/panel/tests/test_util.py @@ -2,10 +2,9 @@ from bokeh.models import Div +from panel.io import render_mimebundle from panel.pane import PaneBase -from panel.util import ( - render_mimebundle, get_method_owner,abbreviated_repr -) +from panel.util import get_method_owner, abbreviated_repr def test_get_method_owner_class(): diff --git a/panel/tests/test_widgets.py b/panel/tests/test_widgets.py index 5d7f5cad8d..e033852603 100644 --- a/panel/tests/test_widgets.py +++ b/panel/tests/test_widgets.py @@ -7,8 +7,8 @@ from bokeh.layouts import Column from bokeh.models import Div as BkDiv, Slider as BkSlider +from panel.io import block_comm from panel.models.widgets import Player as BkPlayer, FileInput as BkFileInput -from panel.util import block_comm from panel.widgets import ( TextInput, StaticText, FloatSlider, IntSlider, RangeSlider, LiteralInput, Checkbox, Select, MultiSelect, Button, Toggle, diff --git a/panel/util.py b/panel/util.py index 1ac84f559e..5c0f394e2b 100644 --- a/panel/util.py +++ b/panel/util.py @@ -12,32 +12,16 @@ import textwrap from collections import defaultdict, MutableSequence, MutableMapping, OrderedDict -from contextlib import contextmanager from datetime import datetime -from itertools import product from six import string_types import param -import bokeh -import bokeh.embed.notebook -from bokeh.core.templates import DOC_NB_JS -from bokeh.core.json_encoder import serialize_json from bokeh.document import Document -from bokeh.embed.elements import div_for_render_item -from bokeh.embed.util import standalone_docs_json_and_render_items -from bokeh.io.notebook import load_notebook as bk_load_notebook -from bokeh.models import Model, LayoutDOM, Box -from bokeh.protocol import Protocol -from bokeh.resources import CDN, INLINE -from bokeh.util.string import encode_utf8 - -from pyviz_comms import (PYVIZ_PROXY, JupyterCommManager, bokeh_msg_handler, - nb_mime_js, embed_js) +from bokeh.models import Model, Box # Global variables CUSTOM_MODELS = {} -BLOCKED = False if sys.version_info.major > 2: unicode = str @@ -228,277 +212,6 @@ def stopped(self): ################################ -def diff(doc, binary=True, events=None): - """ - Returns a json diff required to update an existing plot with - the latest plot data. - """ - events = list(doc._held_events) if events is None else events - if not events or BLOCKED: - return None - msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) - doc._held_events = [e for e in doc._held_events if e not in events] - return msg - - -@contextmanager -def block_comm(): - """ - Context manager to temporarily block comm push - """ - global BLOCKED - BLOCKED = True - yield - BLOCKED = False - - -def push(doc, comm, binary=True): - """ - Pushes events stored on the document across the provided comm. - """ - msg = diff(doc, binary=binary) - if msg is None: - return - comm.send(msg.header_json) - comm.send(msg.metadata_json) - comm.send(msg.content_json) - for header, payload in msg.buffers: - comm.send(json.dumps(header)) - comm.send(buffers=[payload]) - - -def remove_root(obj, replace=None): - """ - Removes the document from any previously displayed bokeh object - """ - for model in obj.select({'type': Model}): - prev_doc = model.document - model._document = None - if prev_doc: - prev_doc.remove_root(model) - if replace: - model._document = replace - - -def add_to_doc(obj, doc, hold=False): - """ - Adds a model to the supplied Document removing it from any existing Documents. - """ - # Add new root - remove_root(obj) - doc.add_root(obj) - if doc._hold is None and hold: - doc.hold() - - -def record_events(doc): - msg = diff(doc, False) - if msg is None: - return {} - return {'header': msg.header_json, 'metadata': msg.metadata_json, - 'content': msg.content_json} - - -def embed_state(panel, model, doc, max_states=1000): - """ - Embeds the state of the application on a State model which allows - exporting a static version of an app. This works by finding all - widgets with a predefined set of options and evaluating the cross - product of the widget values and recording the resulting events to - be replayed when exported. The state is recorded on a State model - which is attached as an additional root on the Document. - - Parameters - ---------- - panel: panel.viewable.Reactive - The Reactive component being exported - model: bokeh.model.Model - The bokeh model being exported - doc: bokeh.document.Document - The bokeh Document being exported - max_states: int - The maximum number of states to export - """ - from .models.state import State - from .widgets import Widget - - target = model.ref['id'] - model.tags.append('embedded') - widgets = [w for w in panel.select(Widget) if 'options' in w.params()] - - add_to_doc(model, doc, True) - - values = [] - for w in widgets: - w_model = w._models[target].select_one({'type': w._widget_type}) - js_callback = CustomJS(code=""" - var receiver = new Bokeh.protocol.Receiver() - state = cb_obj.document.roots()[1] - msg = state.get_state(cb_obj) - receiver.consume(msg.header) - receiver.consume(msg.metadata) - receiver.consume(msg.content) - if (receiver.message) - cb_obj.document.apply_json_patch(receiver.message.content) - """) - w_model.js_on_change('value', js_callback) - if isinstance(w.options, list): - values.append((w, w_model, w.options)) - else: - values.append((w, w_model, list(w.options.values()))) - doc._held_events = [] - - restore = [w.value for w, _, _ in values] - init_vals = [m.value for _, m, _ in values] - cross_product = list(product(*[vals[::-1] for _, _, vals in values])) - - if len(cross_product) > max_states: - raise RuntimeError('The cross product of different application ' - 'states is too large to explore (N=%d), either reduce ' - 'the number of options on the widgets or increase ' - 'the max_states specified on static export.' % - len(cross_product)) - - nested_dict = lambda: defaultdict(nested_dict) - state_dict = nested_dict() - for key in cross_product: - sub_dict = state_dict - for i, k in enumerate(key): - w, m = values[i][:2] - w.value = k - sub_dict = sub_dict[m.value] - events = record_events(doc) - if events: - sub_dict.update(events) - - for (w, _, _), v in zip(values, restore): - w.set_param(value=v) - - state = State(state=state_dict, values=init_vals, - widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) - doc.add_root(state) - - -LOAD_MIME = 'application/vnd.holoviews_load.v0+json' -EXEC_MIME = 'application/vnd.holoviews_exec.v0+json' -HTML_MIME = 'text/html' - -ABORT_JS = """ -if (!window.PyViz) {{ - return; -}} -var receiver = window.PyViz.receivers['{plot_id}']; -var events = receiver ? receiver._partial.content.events : []; -for (var event of events) {{ - if ((event.kind == 'ModelChanged') && (event.attr == '{change}') && - (cb_obj.id == event.model.id) && - (cb_obj['{change}'] == event.new)) {{ - events.pop(events.indexOf(event)) - return; - }} -}} -""" - -def load_notebook(inline=True): - from IPython.display import publish_display_data - - # Create a message for the logo (if shown) - LOAD_MIME_TYPE = bokeh.io.notebook.LOAD_MIME_TYPE - bokeh.io.notebook.LOAD_MIME_TYPE = LOAD_MIME - bk_load_notebook(hide_banner=True, resources=INLINE if inline else CDN) - bokeh.io.notebook.LOAD_MIME_TYPE = LOAD_MIME_TYPE - bokeh.io.notebook.curstate().output_notebook() - - # Publish comm manager - JS = '\n'.join([PYVIZ_PROXY, JupyterCommManager.js_manager, nb_mime_js]) - publish_display_data(data={LOAD_MIME: JS, 'application/javascript': JS}) - - -def _origin_url(url): - if url.startswith("http"): - url = url.split("//")[1] - return url - -def _server_url(url, port): - if url.startswith("http"): - return '%s:%d%s' % (url.rsplit(':', 1)[0], port, "/") - else: - return 'http://%s:%d%s' % (url.split(':')[0], port, "/") - - -def show_server(server, notebook_url, server_id): - """ - Displays a bokeh server inline in the notebook. - - Parameters - ---------- - server: bokeh.server.server.Server - Bokeh server instance which is already running - notebook_url: str - The URL of the running Jupyter notebook server - server_id: str - Unique ID to identify the server with - """ - from bokeh.embed import server_document - from IPython.display import publish_display_data - - if callable(notebook_url): - url = notebook_url(server.port) - else: - url = _server_url(notebook_url, server.port) - - script = server_document(url, resources=None) - - publish_display_data({ - HTML_MIME: script, - EXEC_MIME: "" - }, metadata={ - EXEC_MIME: {"server_id": server_id} - }) - - -def render_mimebundle(model, doc, comm): - """ - Displays bokeh output inside a notebook using the PyViz display - and comms machinery. - """ - if not isinstance(model, LayoutDOM): - raise ValueError('Can only render bokeh LayoutDOM models') - add_to_doc(model, doc, True) - return render_model(model, comm) - - -def render_model(model, comm=None): - if not isinstance(model, Model): - raise ValueError("notebook_content expects a single Model instance") - - target = model.ref['id'] - - (docs_json, [render_item]) = standalone_docs_json_and_render_items([model]) - div = div_for_render_item(render_item) - render_item = render_item.to_json() - script = DOC_NB_JS.render( - docs_json=serialize_json(docs_json), - render_items=serialize_json([render_item]), - ) - bokeh_script, bokeh_div = encode_utf8(script), encode_utf8(div) - html = "
{html}
".format(id=target, html=bokeh_div) - - # Publish bokeh plot JS - msg_handler = bokeh_msg_handler.format(plot_id=target) - - if comm: - comm_js = comm.js_template.format(plot_id=target, comm_id=comm.id, msg_handler=msg_handler) - bokeh_js = '\n'.join([comm_js, bokeh_script]) - else: - bokeh_js = bokeh_script - bokeh_js = embed_js.format(widget_id=target, plot_id=target, html=bokeh_div) + bokeh_js - - data = {EXEC_MIME: '', 'text/html': html, 'application/javascript': bokeh_js} - metadata = {EXEC_MIME: {'id': target}} - return data, metadata - - def bokeh_repr(obj, depth=0, ignored=['children', 'text', 'name', 'toolbar', 'renderers', 'below', 'center', 'left', 'right']): from .viewable import Viewable if isinstance(obj, Viewable): diff --git a/panel/viewable.py b/panel/viewable.py index a90c7f2f6c..50f205a434 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -19,13 +19,12 @@ from bokeh.resources import CDN as _CDN from bokeh.models import CustomJS from bokeh.server.server import Server -from pyviz_comms import JS_CALLBACK, JupyterCommManager +from pyviz_comms import JS_CALLBACK, JupyterCommManager, Comm as _Comm -from .io import state -from .models.state import State -from .util import (render_mimebundle, add_to_doc, push, param_reprs, - embed_state, render_model, _origin_url, show_server, - ABORT_JS) +from .io import ( + add_to_doc, push, render_mimebundle, state, embed_state, + render_model, _origin_url, show_server, ABORT_JS) +from .util import param_reprs class Layoutable(param.Parameterized): @@ -264,6 +263,9 @@ def _repr_mimebundle_(self, include=None, exclude=None): doc = _Document() comm = state._comm_manager.get_server_comm() model = self._get_root(doc, comm) + if state.embed: + embed_state(self, model, doc) + return render_model(model) return render_mimebundle(model, doc, comm) def _server_destroy(self, session_context): @@ -348,9 +350,14 @@ def embed(self, max_states=1000): The maximum number of states to embed """ from IPython.display import publish_display_data - doc = Document() - comm = Comm() - model = self._get_root(doc, comm) + doc = _Document() + comm = _Comm() + try: + embed = state.embed + state.embed = True + model = self._get_root(doc, comm) + finally: + state.embed = embed embed_state(self, model, doc, max_states) publish_display_data(*render_model(model)) @@ -408,6 +415,13 @@ def show_callback(): server.show('/') server.io_loop.add_callback(show_callback) + def sig_exit(*args, **kwargs): + server.io_loop.add_callback_from_signal(do_stop) + + def do_stop(*args, **kwargs): + server.io_loop.stop() + signal.signal(signal.SIGINT, sig_exit) + if start: server.start() try: @@ -519,12 +533,6 @@ def show(self, port=0, websocket_origin=None, threaded=False): server.start() else: server = self.get_server(port, websocket_origin, show=True, start=True) - def sig_exit(*args, **kwargs): - server.io_loop.add_callback_from_signal(do_stop) - - def do_stop(*args, **kwargs): - server.io_loop.stop() - signal.signal(signal.SIGINT, sig_exit) return server @@ -628,7 +636,7 @@ def _link_props(self, model, properties, doc, root, comm=None): if comm is None: for p in properties: model.on_change(p, partial(self._server_change, doc)) - elif type(comm) is Comm: + elif state.embed: pass else: client_comm = state._comm_manager.get_client_comm(on_msg=self._comm_change) diff --git a/panel/widgets/base.py b/panel/widgets/base.py index 6be08ea78b..dd3757cbda 100644 --- a/panel/widgets/base.py +++ b/panel/widgets/base.py @@ -55,3 +55,23 @@ class CompositeWidget(Widget): """ __abstract = True + + def select(self, selector=None): + """ + Iterates over the Viewable and any potential children in the + applying the Selector. + + Arguments + --------- + selector: type or callable or None + The selector allows selecting a subset of Viewables by + declaring a type or callable function to filter by. + + Returns + ------- + viewables: list(Viewable) + """ + objects = super(CompositeWidget, self).select(selector) + for obj in self._composite.objects: + objects += obj.select(selector) + return objects diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index e3bf874d65..24ec5a36d3 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -45,6 +45,9 @@ class _SliderBase(Widget): objects=['horizontal', 'vertical'], doc=""" Whether the slider should be oriented horizontally or vertically.""") + show_value = param.Boolean(default=True, doc=""" + Whether to show the widget value""") + tooltips = param.Boolean(default=True, doc=""" Whether the slider handle should display tooltips""") @@ -96,10 +99,13 @@ class DiscreteSlider(CompositeWidget, _SliderBase): _rename = {'formatter': None} + _text_link = """ + var labels = {labels} + target.text = labels[source.value] + """ + def __init__(self, **params): self._syncing = False - self._text = StaticText() - self._slider = IntSlider() super(DiscreteSlider, self).__init__(**params) if 'formatter' not in params and all(isinstance(v, (int, np.int_)) for v in self.values): self.formatter = '%d' @@ -111,19 +117,34 @@ def __init__(self, **params): 'is one of the declared options.' % self.value) + self._text = StaticText(margin=(5, 0, 0, 0)) + self._slider = IntSlider() self._composite = Column(self._text, self._slider) - self._update_value() self._update_options() + self.param.watch(self._update_options, ['options', 'formatter']) + self.param.watch(self._update_value, ['value']) + + def _update_options(self, *events): + values, labels = self.values, self.labels + if self.value not in values: + value = 0 + self.value = values[0] + else: + value = values.index(self.value) + self._slider = IntSlider(start=0, end=len(self.options)-1, value=value, + show_value=False, margin=(0, 5, 5, 5)) + js_code = self._text_link.format(labels=repr(self.labels)) + self._jslink = self._slider.jslink(self._text, code={'value': js_code}) self._slider.param.watch(self._sync_value, 'value') + self._text.value = labels[value] + self._composite[1] = self._slider - @param.depends('value', watch=True) - def _update_value(self): - labels, values = self.labels, self.values + def _update_value(self, event): + values = self.values if self.value not in values: self.value = values[0] return index = self.values.index(self.value) - self._text.value = labels[index] if self._syncing: return try: @@ -132,14 +153,6 @@ def _update_value(self): finally: self._syncing = False - @param.depends('options', watch=True) - def _update_options(self): - slider_msg = {'start': 0, 'end': len(self.options) - 1} - self._slider.set_param(**slider_msg) - values = self.values - if self.value not in values: - self.value = values[0] - def _sync_value(self, event): if self._syncing: return @@ -154,11 +167,11 @@ def _get_model(self, doc, root=None, parent=None, comm=None): @property def labels(self): - title = ('%s: ' % self.name if self.name else '') + title = (self.name + ': ' if self.name else '') if isinstance(self.options, dict): - return [title + o for o in self.options] + return [title + ('%s ' % o) for o in self.options] else: - return [title + (o if isinstance(o, string_types) else (self.formatter % o)) + return [title + ('%s ' % (o if isinstance(o, string_types) else (self.formatter % o))) for o in self.options] @property def values(self): From 48c84201fbceccbd4ff4998c9a6a6b39454328d3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 03:15:50 +0000 Subject: [PATCH 07/21] Fix for DiscreteSlider margin --- panel/widgets/slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 24ec5a36d3..01d2223733 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -117,7 +117,7 @@ def __init__(self, **params): 'is one of the declared options.' % self.value) - self._text = StaticText(margin=(5, 0, 0, 0)) + self._text = StaticText(margin=(5, 0, 0, 5)) self._slider = IntSlider() self._composite = Column(self._text, self._slider) self._update_options() From 555708461298182093e812d84df61c009d54effa Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 03:16:15 +0000 Subject: [PATCH 08/21] Handle DiscreteSlider discretization --- panel/io.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/panel/io.py b/panel/io.py index 960355a0b6..9e920c4b0d 100644 --- a/panel/io.py +++ b/panel/io.py @@ -216,17 +216,20 @@ def embed_state(panel, model, doc, max_states=1000): The maximum number of states to export """ from .models.state import State - from .widgets import Widget + from .widgets import Widget, DiscreteSlider target = model.ref['id'] model.tags.append('embedded') - discrete_widgets = [w for w in panel.select(Widget) if 'options' in w.params()] + discrete_widgets = [w for w in panel.select(Widget) if 'options' in w.param] add_to_doc(model, doc, True) values = [] - for w in widgets: - w_model = w._models[target][0].select_one({'type': w._widget_type}) + for w in discrete_widgets: + if isinstance(w, DiscreteSlider): + w_model = w._composite[1]._models[target][0].select_one({'type': w._widget_type}) + else: + w_model = w._models[target][0].select_one({'type': w._widget_type}) js_callback = CustomJS(code=""" var receiver = new Bokeh.protocol.Receiver() state = cb_obj.document.roots()[1] From 7f5629e7953883d20842adbd0e2842d21c7edba1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 04:41:35 +0000 Subject: [PATCH 09/21] Fixed DiscreteSlider tests --- panel/tests/test_widgets.py | 12 +++--------- panel/widgets/slider.py | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/panel/tests/test_widgets.py b/panel/tests/test_widgets.py index e033852603..a916d0f24b 100644 --- a/panel/tests/test_widgets.py +++ b/panel/tests/test_widgets.py @@ -512,16 +512,14 @@ def test_discrete_slider(document, comm): assert widget.start == 0 assert widget.end == 3 assert widget.step == 1 - assert label.text == 'DiscreteSlider: 1' + assert label.text == 'DiscreteSlider: 1' widget.value = 2 discrete_slider._slider._comm_change({'value': 2}) assert discrete_slider.value == 10 - assert label.text == 'DiscreteSlider: 10' discrete_slider.value = 100 assert widget.value == 3 - assert label.text == 'DiscreteSlider: 100' def test_discrete_date_slider(document, comm): @@ -541,16 +539,14 @@ def test_discrete_date_slider(document, comm): assert widget.start == 0 assert widget.end == 2 assert widget.step == 1 - assert label.text == 'DiscreteSlider: 2016-01-02' + assert label.text == 'DiscreteSlider: 2016-01-02' widget.value = 2 discrete_slider._slider._comm_change({'value': 2}) assert discrete_slider.value == dates['2016-01-03'] - assert label.text == 'DiscreteSlider: 2016-01-03' discrete_slider.value = dates['2016-01-01'] assert widget.value == 0 - assert label.text == 'DiscreteSlider: 2016-01-01' def test_discrete_slider_options_dict(document, comm): @@ -568,16 +564,14 @@ def test_discrete_slider_options_dict(document, comm): assert widget.start == 0 assert widget.end == 3 assert widget.step == 1 - assert label.text == 'DiscreteSlider: 1' + assert label.text == 'DiscreteSlider: 1' widget.value = 2 discrete_slider._slider._comm_change({'value': 2}) assert discrete_slider.value == 10 - assert label.text == 'DiscreteSlider: 10' discrete_slider.value = 100 assert widget.value == 3 - assert label.text == 'DiscreteSlider: 100' def test_cross_select_constructor(document, comm): diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 01d2223733..3789253176 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -169,9 +169,9 @@ def _get_model(self, doc, root=None, parent=None, comm=None): def labels(self): title = (self.name + ': ' if self.name else '') if isinstance(self.options, dict): - return [title + ('%s ' % o) for o in self.options] + return [title + ('%s' % o) for o in self.options] else: - return [title + ('%s ' % (o if isinstance(o, string_types) else (self.formatter % o))) + return [title + ('%s' % (o if isinstance(o, string_types) else (self.formatter % o))) for o in self.options] @property def values(self): From 72c341e3bd803f9b9a4d786e0d8df1b061529c0c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 13:58:24 +0000 Subject: [PATCH 10/21] Added pn.config --- panel/__init__.py | 2 +- panel/io.py | 114 +++++++++++++++++++++++++++++++++++++--------- panel/viewable.py | 14 +++--- 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/panel/__init__.py b/panel/__init__.py index e4712a9ca2..45a52e423b 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -10,7 +10,7 @@ from . import widgets # noqa from .interact import interact # noqa -from .io import panel_extension as extension, state # noqa +from .io import config, panel_extension as extension, state # noqa from .layout import Row, Column, Tabs, Spacer # noqa from .pane import panel, Pane # noqa from .param import Param # noqa diff --git a/panel/io.py b/panel/io.py index 9e920c4b0d..c07e577c6a 100644 --- a/panel/io.py +++ b/panel/io.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, unicode_literals import json +import os import sys from collections import defaultdict @@ -35,6 +36,55 @@ # Public API #--------------------------------------------------------------------- +class _config(param.Parameterized): + """ + Holds global configuration options for Panel. The options can be + set directly on the global config instance, via keyword arguments + in the extension or via environment variables. For example to set + the embed option the following approaches can be used: + + pn.config.embed = True + + pn.extension(embed=True) + + os.environ['PANEL_EMBED'] = 'True' + """ + + _embed = param.Boolean(default=False, doc=""" + Whether plot data will be embedded.""") + + _inline = param.Boolean(default=True, doc=""" + Whether to inline JS and CSS resources. + If disabled, resources are loaded from CDN if one is available.""") + + _truthy = ['True', 'true', '1', True, 1] + + @property + def embed(self): + if self._embed is not None: + return self._embed + else: + return os.environ.get('PANEL_EMBED', _config._embed) in self._truthy + + @embed.setter + def embed(self, value): + self._embed = value + + @property + def inline(self): + if self._inline is not None: + return self._inline + else: + return os.environ.get('PANEL_INLINE', _config._inline) in self._truthy + + @embed.setter + def inline(self, value): + self._inline = value + + +config = _config(**{k: None for k in _config.param if k != 'name'}) + + class state(param.Parameterized): """ Holds global state associated with running apps, allowing running @@ -45,9 +95,6 @@ class state(param.Parameterized): The bokeh Document for which a server event is currently being processed.""") - embed = param.Boolean(default=False, doc=""" - Whether plot data will be embedded.""") - # Whether to hold comm events _hold = False @@ -66,10 +113,6 @@ class panel_extension(_pyviz_extension): bokeh and enable comms. """ - inline = param.Boolean(default=True, doc=""" - Whether to inline JS and CSS resources. - If disabled, resources are loaded from CDN if one is available.""") - _loaded = False def __call__(self, *args, **params): @@ -79,13 +122,15 @@ def __call__(self, *args, **params): except: return - p = param.ParamOverrides(self, params) + for k, v in params.items(): + setattr(config, k, v) + 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") - load_notebook(p.inline) + load_notebook(config.inline) self._loaded = True state._comm_manager = _JupyterCommManager @@ -125,6 +170,26 @@ def __call__(self, *args, **params): }} """ +STATE_JS = """ +var receiver = new Bokeh.protocol.Receiver() +var state = null +for (var root of cb_obj.document.roots()) {{ + if (root.id == '{id}') {{ + state = root; + break; + }} +}} +if (!state) {{ return; }} +msg = state.get_state(cb_obj) +receiver.consume(msg.header) +receiver.consume(msg.metadata) +receiver.consume(msg.content) +if (receiver.message) {{ + cb_obj.document.apply_json_patch(receiver.message.content) +}} +""" + + def diff(doc, binary=True, events=None): """ Returns a json diff required to update an existing plot with @@ -223,6 +288,7 @@ def embed_state(panel, model, doc, max_states=1000): discrete_widgets = [w for w in panel.select(Widget) if 'options' in w.param] add_to_doc(model, doc, True) + state = State() values = [] for w in discrete_widgets: @@ -230,16 +296,7 @@ def embed_state(panel, model, doc, max_states=1000): w_model = w._composite[1]._models[target][0].select_one({'type': w._widget_type}) else: w_model = w._models[target][0].select_one({'type': w._widget_type}) - js_callback = CustomJS(code=""" - var receiver = new Bokeh.protocol.Receiver() - state = cb_obj.document.roots()[1] - msg = state.get_state(cb_obj) - receiver.consume(msg.header) - receiver.consume(msg.metadata) - receiver.consume(msg.content) - if (receiver.message) - cb_obj.document.apply_json_patch(receiver.message.content) - """) + js_callback = CustomJS(code=STATE_JS.format(id=state.ref['id'])) w_model.js_on_change('value', js_callback) if isinstance(w.options, list): values.append((w, w_model, w.options)) @@ -273,8 +330,8 @@ def embed_state(panel, model, doc, max_states=1000): for (w, _, _), v in zip(values, restore): w.set_param(value=v) - state = State(state=state_dict, values=init_vals, - widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) + state.update(state=state_dict, values=init_vals, + widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) doc.add_root(state) @@ -347,6 +404,21 @@ def render_mimebundle(model, doc, comm): return render_model(model, comm) +def mimebundle_to_html(bundle): + """ + Converts a MIME bundle into HTML. + """ + if isinstance(bundle, tuple): + data, metadata = bundle + else: + data = bundle + html = data.get('text/html', '') + if 'application/javascript' in data: + js = data['application/javascript'] + html += '\n'.format(js=js) + return html + + def render_model(model, comm=None): if not isinstance(model, Model): raise ValueError("notebook_content expects a single Model instance") diff --git a/panel/viewable.py b/panel/viewable.py index 50f205a434..fec256221b 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -22,8 +22,8 @@ from pyviz_comms import JS_CALLBACK, JupyterCommManager, Comm as _Comm from .io import ( - add_to_doc, push, render_mimebundle, state, embed_state, - render_model, _origin_url, show_server, ABORT_JS) + ABORT_JS, add_to_doc, push, render_mimebundle, state, embed_state, + render_model, _origin_url, show_server, config) from .util import param_reprs @@ -263,7 +263,7 @@ def _repr_mimebundle_(self, include=None, exclude=None): doc = _Document() comm = state._comm_manager.get_server_comm() model = self._get_root(doc, comm) - if state.embed: + if config.embed: embed_state(self, model, doc) return render_model(model) return render_mimebundle(model, doc, comm) @@ -353,11 +353,11 @@ def embed(self, max_states=1000): doc = _Document() comm = _Comm() try: - embed = state.embed - state.embed = True + embed = config.embed + config.embed = True model = self._get_root(doc, comm) finally: - state.embed = embed + config.embed = embed embed_state(self, model, doc, max_states) publish_display_data(*render_model(model)) @@ -636,7 +636,7 @@ def _link_props(self, model, properties, doc, root, comm=None): if comm is None: for p in properties: model.on_change(p, partial(self._server_change, doc)) - elif state.embed: + elif config.embed: pass else: client_comm = state._comm_manager.get_client_comm(on_msg=self._comm_change) From c2054693ff9d079f5e7ae659adbf16f3ac42f60a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 14:09:45 +0000 Subject: [PATCH 11/21] Allow None for config vars --- panel/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/io.py b/panel/io.py index c07e577c6a..166013cffb 100644 --- a/panel/io.py +++ b/panel/io.py @@ -50,10 +50,10 @@ class _config(param.Parameterized): os.environ['PANEL_EMBED'] = 'True' """ - _embed = param.Boolean(default=False, doc=""" + _embed = param.Boolean(default=False, allow_None=True, doc=""" Whether plot data will be embedded.""") - _inline = param.Boolean(default=True, doc=""" + _inline = param.Boolean(default=True, allow_None=True, doc=""" Whether to inline JS and CSS resources. If disabled, resources are loaded from CDN if one is available.""") From 4af8c645dcdcffc699259025a9b104a6a004ab22 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 15:22:44 +0000 Subject: [PATCH 12/21] Minor fixes for widget styling --- panel/widgets/input.py | 3 +++ panel/widgets/slider.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/panel/widgets/input.py b/panel/widgets/input.py index c26707d8da..621b65e5eb 100644 --- a/panel/widgets/input.py +++ b/panel/widgets/input.py @@ -78,6 +78,9 @@ def save(self, filename): class StaticText(Widget): + style = param.Dict(default=None, doc=""" + Dictionary of CSS property:value pairs to apply to this Div.""") + value = param.Parameter(default=None) _widget_type = _BkDiv diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py index 3789253176..2a316e9ab4 100644 --- a/panel/widgets/slider.py +++ b/panel/widgets/slider.py @@ -117,7 +117,7 @@ def __init__(self, **params): 'is one of the declared options.' % self.value) - self._text = StaticText(margin=(5, 0, 0, 5)) + self._text = StaticText(margin=(5, 0, 0, 5), style={'white-space': 'nowrap'}) self._slider = IntSlider() self._composite = Column(self._text, self._slider) self._update_options() From d7e1ec7bac06244d5465467b104890d30f1dd97b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 15:23:30 +0000 Subject: [PATCH 13/21] Embedding discretizes slider widgets --- panel/io.py | 76 +++++++++++++++++++++++++++++++++++++---------- panel/viewable.py | 4 ++- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/panel/io.py b/panel/io.py index 166013cffb..6ccd447af0 100644 --- a/panel/io.py +++ b/panel/io.py @@ -15,6 +15,7 @@ import param import bokeh import bokeh.embed.notebook +import numpy as np from bokeh.document import Document from bokeh.core.templates import DOC_NB_JS @@ -260,7 +261,7 @@ def record_events(doc): 'content': msg.content_json} -def embed_state(panel, model, doc, max_states=1000): +def embed_state(panel, model, doc, max_states=1000, max_opts=3): """ Embeds the state of the application on a State model which allows exporting a static version of an app. This works by finding all @@ -279,29 +280,68 @@ def embed_state(panel, model, doc, max_states=1000): The bokeh Document being exported max_states: int The maximum number of states to export + max_opts: int + The maximum number of options for a single widget """ + from .layout import Panel from .models.state import State from .widgets import Widget, DiscreteSlider + from .widgets.slider import _SliderBase target = model.ref['id'] + _, _, _, comm = state._views[target] + model.tags.append('embedded') - discrete_widgets = [w for w in panel.select(Widget) if 'options' in w.param] + widgets = [w for w in panel.select(Widget) if 'options' in w.param + or isinstance(w, _SliderBase)] - add_to_doc(model, doc, True) - state = State() + state_model = State() values = [] - for w in discrete_widgets: + for w in widgets: if isinstance(w, DiscreteSlider): w_model = w._composite[1]._models[target][0].select_one({'type': w._widget_type}) else: w_model = w._models[target][0].select_one({'type': w._widget_type}) - js_callback = CustomJS(code=STATE_JS.format(id=state.ref['id'])) - w_model.js_on_change('value', js_callback) - if isinstance(w.options, list): - values.append((w, w_model, w.options)) + + if not hasattr(w, 'options'): # Discretize slider + parent = panel.select(lambda x: isinstance(x, Panel) and w in x)[0] + parent_model = parent._models[target][0] + + # Compute sampling + start, end, step = w_model.start, w_model.end, w_model.step + span = end-start + dtype = int if isinstance(step, int) else float + if (span/step) > (max_opts-1): + step = dtype(span/(max_opts-1)) + vals = [dtype(v) for v in np.arange(start, end+step, step)] + + # Replace model + dw = DiscreteSlider(options=vals, name=w.name) + dw.link(w, value='value') + w._models.pop(target) + w = dw + index = parent_model.children.index(w_model) + try: + embed = config.embed + config.embed = True + w_model = w._get_model(doc, model, parent_model, comm) + finally: + config.embed = embed + link = CustomJS(code=dw._jslink.code['value'], args={ + 'source': w_model.children[1], 'target': w_model.children[0]}) + parent_model.children[index] = w_model + w_model = w_model.children[1] + w_model.js_on_change('value', link) + elif isinstance(w.options, list): + vals = w.options else: - values.append((w, w_model, list(w.options.values()))) + vals = list(w.options.values()) + js_callback = CustomJS(code=STATE_JS.format(id=state_model.ref['id'])) + w_model.js_on_change('value', js_callback) + values.append((w, w_model, vals)) + + add_to_doc(model, doc, True) doc._held_events = [] restore = [w.value for w, _, _ in values] @@ -321,18 +361,24 @@ def embed_state(panel, model, doc, max_states=1000): sub_dict = state_dict for i, k in enumerate(key): w, m = values[i][:2] - w.value = k + try: + w.value = k + except: + continue sub_dict = sub_dict[m.value] events = record_events(doc) if events: sub_dict.update(events) for (w, _, _), v in zip(values, restore): - w.set_param(value=v) + try: + w.set_param(value=v) + except: + pass - state.update(state=state_dict, values=init_vals, - widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) - doc.add_root(state) + state_model.update(state=state_dict, values=init_vals, + widgets={m.ref['id']: i for i, (_, m, _) in enumerate(values)}) + doc.add_root(state_model) def load_notebook(inline=True): diff --git a/panel/viewable.py b/panel/viewable.py index fec256221b..f5ab5f9e6f 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -337,7 +337,7 @@ def app(self, notebook_url="localhost:8888", port=0): show_server(server, notebook_url, server_id) return server - def embed(self, max_states=1000): + def embed(self, max_states=1000, max_opts=3): """ Renders a static version of a panel in a notebook by evaluating the set of states defined by the widgets in the model. Note @@ -348,6 +348,8 @@ def embed(self, max_states=1000): ---------- max_states: int The maximum number of states to embed + max_opts: int + The maximum number of states for a single widget """ from IPython.display import publish_display_data doc = _Document() From d98149a770b6101e38654934ebf8ea9814141752 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 15:35:00 +0000 Subject: [PATCH 14/21] Fixed small bug in config --- panel/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/io.py b/panel/io.py index 6ccd447af0..7a765a5b4a 100644 --- a/panel/io.py +++ b/panel/io.py @@ -78,7 +78,7 @@ def inline(self): else: return os.environ.get('PANEL_INLINE', _config._inline) in self._truthy - @embed.setter + @inline.setter def inline(self, value): self._inline = value From 7ad7a2c332406f7b548dcc7de37e0cffd19dfeb6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:04:07 +0000 Subject: [PATCH 15/21] Added contextmanager for config --- panel/io.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/panel/io.py b/panel/io.py index 7a765a5b4a..3f9ae18029 100644 --- a/panel/io.py +++ b/panel/io.py @@ -60,6 +60,16 @@ class _config(param.Parameterized): _truthy = ['True', 'true', '1', True, 1] + @contextmanager + def set(self, **kwargs): + values = [(k, v) for k, v in self.param.get_param_values() if k != 'name'] + for k, v in kwargs.items(): + setattr(self, k, v) + try: + yield + finally: + self.set_param(**dict(values)) + @property def embed(self): if self._embed is not None: From dcd984e514662412e6aa948a0cb88b664d5bb687 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:04:21 +0000 Subject: [PATCH 16/21] Added embed tests --- panel/tests/test_io.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 panel/tests/test_io.py diff --git a/panel/tests/test_io.py b/panel/tests/test_io.py new file mode 100644 index 0000000000..7dc875f87c --- /dev/null +++ b/panel/tests/test_io.py @@ -0,0 +1,46 @@ +import json + +from panel import Row +from panel.io import config, embed_state +from panel.pane import Str +from panel.widgets import Select, FloatSlider + + +def test_embed_discrete(document, comm): + select = Select(options=['A', 'B', 'C']) + string = Str() + select.link(string, value='text') + panel = Row(select, string) + with config.set(embed=True): + model = panel._get_root(document, comm) + embed_state(panel, model, document) + _, state = document.roots + assert set(state.state) == {'A', 'B', 'C'} + for k, v in state.state.items(): + events = json.loads(v['content'])['events'] + assert len(events) == 1 + event = events[0] + assert event['kind'] == 'ModelChanged' + assert event['attr'] == 'value' + assert event['model'] == model.children[0].ref + assert event['new'] == k + + +def test_embed_continuous(document, comm): + select = FloatSlider(start=0, end=10) + string = Str() + select.link(string, value='text') + panel = Row(select, string) + with config.set(embed=True): + model = panel._get_root(document, comm) + embed_state(panel, model, document) + _, state = document.roots + assert set(state.state) == {0, 1, 2} + for k, v in state.state.items(): + events = json.loads(v['content'])['events'] + assert len(events) == 1 + event = events[0] + assert event['kind'] == 'ModelChanged' + assert event['attr'] == 'value' + assert event['model'] == model.children[0].children[1].ref + assert event['new'] == k From 23311a15a89992d6dbfeeb308b4397a6cd149064 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:04:37 +0000 Subject: [PATCH 17/21] Addd setup.cfg --- setup.cfg | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..587704ddba --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[metadata] +license_file = LICENSE + +[flake8] +include = setup.py panel +exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,tests +ignore = F812 From 3de366d9e8c88cc19e4187233518424e6f8e41ec Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:06:43 +0000 Subject: [PATCH 18/21] Use config context manager internally --- panel/io.py | 6 +----- panel/viewable.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/panel/io.py b/panel/io.py index 3f9ae18029..3381706d4d 100644 --- a/panel/io.py +++ b/panel/io.py @@ -332,12 +332,8 @@ def embed_state(panel, model, doc, max_states=1000, max_opts=3): w._models.pop(target) w = dw index = parent_model.children.index(w_model) - try: - embed = config.embed - config.embed = True + with config.set(embed=True): w_model = w._get_model(doc, model, parent_model, comm) - finally: - config.embed = embed link = CustomJS(code=dw._jslink.code['value'], args={ 'source': w_model.children[1], 'target': w_model.children[0]}) parent_model.children[index] = w_model diff --git a/panel/viewable.py b/panel/viewable.py index f5ab5f9e6f..4566cbc942 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -354,13 +354,9 @@ def embed(self, max_states=1000, max_opts=3): from IPython.display import publish_display_data doc = _Document() comm = _Comm() - try: - embed = config.embed - config.embed = True + with config.set(embed=True) model = self._get_root(doc, comm) - finally: - config.embed = embed - embed_state(self, model, doc, max_states) + embed_state(self, model, doc, max_states) publish_display_data(*render_model(model)) def get_server(self, port=0, websocket_origin=None, loop=None, From 6c66a59b21d54f93a2d9db5e2d9c9df224bc3837 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:16:37 +0000 Subject: [PATCH 19/21] Fixed setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 587704ddba..982629ad29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -license_file = LICENSE +license_file = LICENSE.txt [flake8] include = setup.py panel From 223067c5c48f1e553552d106e7c338a5207820da Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:33:35 +0000 Subject: [PATCH 20/21] Fixed syntax --- panel/viewable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/viewable.py b/panel/viewable.py index 4566cbc942..4841bd5339 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -354,7 +354,7 @@ def embed(self, max_states=1000, max_opts=3): from IPython.display import publish_display_data doc = _Document() comm = _Comm() - with config.set(embed=True) + with config.set(embed=True): model = self._get_root(doc, comm) embed_state(self, model, doc, max_states) publish_display_data(*render_model(model)) From f76fb93da578c7653b284d7a9b7ed20dc63ab43b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 12 Mar 2019 16:45:32 +0000 Subject: [PATCH 21/21] Fixed flake ignore --- setup.cfg | 5 ----- tox.ini | 3 ++- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 982629ad29..498ec14ac4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,2 @@ [metadata] license_file = LICENSE.txt - -[flake8] -include = setup.py panel -exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,tests -ignore = F812 diff --git a/tox.ini b/tox.ini index 1aa91f3801..67b9db48f5 100644 --- a/tox.ini +++ b/tox.ini @@ -53,4 +53,5 @@ include = *.py # bug resulting in code being duplicated a couple of times. exclude = .git,__pycache__,.tox,.eggs,*.egg,doc,dist,build,_build,.ipynb_checkpoints,run_test.py ignore = E, - W + W, + F812