diff --git a/panel/interact.py b/panel/interact.py
index fdb6e1948d..9dc47606aa 100644
--- a/panel/interact.py
+++ b/panel/interact.py
@@ -146,8 +146,7 @@ def __init__(self, object, params={}, **kwargs):
if self.manual_update:
widgets.append(('manual', Button(name=self.manual_name)))
self._widgets = OrderedDict(widgets)
- self._pane = Pane(self.object(**self.kwargs), name=self.name,
- _temporary=True)
+ self._pane = Pane(self.object(**self.kwargs), name=self.name)
self._inner_layout = Row(self._pane)
widgets = [widget for _, widget in widgets if isinstance(widget, Widget)]
if 'name' in params:
@@ -156,35 +155,20 @@ def __init__(self, object, params={}, **kwargs):
self.layout.objects = [self.widget_box, self._inner_layout]
self._link_widgets()
- @property
- def kwargs(self):
- return {k: widget.value for k, widget in self._widgets.items()
- if k != 'manual'}
-
- def signature(self):
- return signature(self.object)
-
- def find_abbreviations(self, kwargs):
- """Find the abbreviations for the given function and kwargs.
- Return (name, abbrev, default) tuples.
- """
- new_kwargs = []
- try:
- sig = self.signature()
- except (ValueError, TypeError):
- # can't inspect, no info from function; only use kwargs
- return [ (key, value, value) for key, value in kwargs.items() ]
-
- for parameter in sig.parameters.values():
- for name, value, default in _yield_abbreviations_for_parameter(parameter, kwargs):
- if value is empty:
- raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
- new_kwargs.append((name, value, default))
- return new_kwargs
+ #----------------------------------------------------------------
+ # Model API
+ #----------------------------------------------------------------
def _get_model(self, doc, root=None, parent=None, comm=None):
return self._inner_layout._get_model(doc, root, parent, comm)
+ #----------------------------------------------------------------
+ # Callback API
+ #----------------------------------------------------------------
+
+ def _synced_params(self):
+ return []
+
def _link_widgets(self):
if self.manual_update:
widgets = [('manual', self._widgets['manual'])]
@@ -200,27 +184,52 @@ def update_pane(change):
if isinstance(new_object, (PaneBase, Panel)):
new_params = {k: v for k, v in new_object.get_param_values()
if k != 'name'}
- try:
- self._pane.set_param(**new_params)
- except:
- raise
- finally:
- new_object._cleanup(final=new_object._temporary)
+ self._pane.set_param(**new_params)
else:
self._pane.object = new_object
return
# Replace pane entirely
- self._pane = Pane(new_object, _temporary=True)
+ self._pane = Pane(new_object)
self._inner_layout[0] = self._pane
pname = 'clicks' if name == 'manual' else 'value'
watcher = widget.param.watch(update_pane, pname)
- self._callbacks['instance'].append(watcher)
+ self._callbacks.append(watcher)
- def _cleanup(self, root=None, final=False):
- self._inner_layout._cleanup(root, final)
- super(interactive, self)._cleanup(root, final)
+ def _cleanup(self, root):
+ self._inner_layout._cleanup(root)
+ super(interactive, self)._cleanup(root)
+
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
+ @property
+ def kwargs(self):
+ return {k: widget.value for k, widget in self._widgets.items()
+ if k != 'manual'}
+
+ def signature(self):
+ return signature(self.object)
+
+ def find_abbreviations(self, kwargs):
+ """Find the abbreviations for the given function and kwargs.
+ Return (name, abbrev, default) tuples.
+ """
+ new_kwargs = []
+ try:
+ sig = self.signature()
+ except (ValueError, TypeError):
+ # can't inspect, no info from function; only use kwargs
+ return [ (key, value, value) for key, value in kwargs.items() ]
+
+ for parameter in sig.parameters.values():
+ for name, value, default in _yield_abbreviations_for_parameter(parameter, kwargs):
+ if value is empty:
+ raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
+ new_kwargs.append((name, value, default))
+ return new_kwargs
def widgets_from_abbreviations(self, seq):
"""Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
diff --git a/panel/io.py b/panel/io.py
index 5da9c2ec9b..5f0cab907d 100644
--- a/panel/io.py
+++ b/panel/io.py
@@ -82,7 +82,7 @@ def _cleanup_panel(msg_id):
"""
if msg_id not in state._views:
return
- viewable, model = state._views.pop(msg_id)
+ viewable, model, _, _ = state._views.pop(msg_id)
viewable._cleanup(model)
diff --git a/panel/layout.py b/panel/layout.py
index b9419a8b08..417f754b2b 100644
--- a/panel/layout.py
+++ b/panel/layout.py
@@ -10,8 +10,7 @@
Spacer as BkSpacer)
from bokeh.models.widgets import Tabs as BkTabs, Panel as BkPanel
-from .io import state
-from .util import param_name, param_reprs, push
+from .util import param_name, param_reprs
from .viewable import Reactive
@@ -36,60 +35,42 @@ def __init__(self, *objects, **params):
objects = [panel(pane) for pane in objects]
super(Panel, self).__init__(objects=objects, **params)
- 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()
- push(doc, comm)
- elif state.curdoc:
- update_model()
- 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)
-
- def _cleanup(self, root=None, final=False):
- super(Panel, self)._cleanup(root, final)
- if root is not None:
- for p in self.objects:
- p._cleanup(root, final)
-
- 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(Panel, self).select(selector)
- for obj in self.objects:
- objects += obj.select(selector)
- return objects
+ def __repr__(self, depth=0, max_depth=10):
+ if depth > max_depth:
+ return '...'
+ spacer = '\n' + (' ' * (depth+1))
+ cls = type(self).__name__
+ params = param_reprs(self, ['objects'])
+ objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
+ if not params and not objs:
+ return super(Panel, self).__repr__(depth+1)
+ elif not params:
+ template = '{cls}{spacer}{objs}'
+ elif not objs:
+ template = '{cls}({params})'
+ else:
+ template = '{cls}({params}){spacer}{objs}'
+ return template.format(
+ cls=cls, params=', '.join(params),
+ objs=('%s' % spacer).join(objs), spacer=spacer)
+
+ #----------------------------------------------------------------
+ # Callback API
+ #----------------------------------------------------------------
+
+ def _update_model(self, events, msg, root, model, doc, comm=None):
+ if self._rename['objects'] in msg:
+ old = events['objects'].old
+ msg[self._rename['objects']] = self._get_objects(model, old, doc, root, comm)
+ for pane in old:
+ if pane not in self.objects:
+ pane._cleanup(root)
+ model.update(**msg)
+ self._preprocess(root) #preprocess links between new elements
+
+ #----------------------------------------------------------------
+ # Model API
+ #----------------------------------------------------------------
def _get_objects(self, model, old_objects, doc, root, comm=None):
"""
@@ -102,10 +83,13 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
pane = panel(pane)
self.objects[i] = pane
if pane in old_objects:
- child = pane._models[root.ref['id']]
+ child, _ = pane._models[root.ref['id']]
else:
child = pane._get_model(doc, root, model, comm)
new_models.append(child)
+ for obj in old_objects:
+ if obj not in self.objects:
+ obj._cleanup(root)
return new_models
def _get_model(self, doc, root=None, parent=None, comm=None):
@@ -115,12 +99,19 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
objects = self._get_objects(model, [], doc, root, comm)
props = dict(self._init_properties(), objects=objects)
model.update(**self._process_param_change(props))
- params = [p for p in self.param if p != 'name']
- self._models[root.ref['id']] = model
- self._link_params(model, params, doc, root, comm)
+ self._models[root.ref['id']] = (model, parent)
self._link_props(model, self._linked_props, doc, root, comm)
return model
+ def _cleanup(self, root):
+ super(Panel, self)._cleanup(root)
+ for p in self.objects:
+ p._cleanup(root)
+
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
def __getitem__(self, index):
return self.objects[index]
@@ -170,25 +161,25 @@ def __setitem__(self, index, panes):
new_objects[i] = panel(pane)
self.objects = new_objects
- def __repr__(self, depth=0, max_depth=10):
- if depth > max_depth:
- return '...'
- spacer = '\n' + (' ' * (depth+1))
- cls = type(self).__name__
- params = param_reprs(self, ['objects'])
- objs = ['[%d] %s' % (i, obj.__repr__(depth+1)) for i, obj in enumerate(self)]
- if not params and not objs:
- return super(Panel, self).__repr__(depth+1)
- elif not params:
- template = '{cls}{spacer}{objs}'
- elif not objs:
- template = '{cls}({params})'
- else:
- template = '{cls}({params}){spacer}{objs}'
- return template.format(
- cls=cls, params=', '.join(params),
- objs=('%s' % spacer).join(objs), spacer=spacer
- )
+ 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(Panel, self).select(selector)
+ for obj in self.objects:
+ objects += obj.select(selector)
+ return objects
def append(self, pane):
from .pane import panel
@@ -270,6 +261,9 @@ def __init__(self, *items, **params):
objects, self._names = self._to_objects_and_names(items)
super(Tabs, self).__init__(*objects, **params)
self.param.watch(self._update_names, 'objects')
+ # ALERT: Ensure that name update happens first, should be
+ # replaced by watch precedence support in param
+ self._param_watchers['objects']['value'].reverse()
def _to_object_and_name(self, item):
from .pane import panel
@@ -289,6 +283,10 @@ def _to_objects_and_names(self, items):
names.append(name)
return objects, names
+ #----------------------------------------------------------------
+ # Callback API
+ #----------------------------------------------------------------
+
def _update_names(self, event):
if len(event.new) == len(self._names):
return
@@ -302,6 +300,10 @@ def _update_names(self, event):
names.append(name)
self._names = names
+ #----------------------------------------------------------------
+ # Model API
+ #----------------------------------------------------------------
+
def _get_objects(self, model, old_objects, doc, root, comm=None):
"""
Returns new child models for the layout while reusing unchanged
@@ -318,13 +320,20 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
pane = panel(pane, name=name)
self.objects[i] = pane
if pane in old_objects:
- child = pane._models[root.ref['id']]
+ child, _ = pane._models[root.ref['id']]
else:
child = pane._get_model(doc, root, model, comm)
child = BkPanel(title=name, name=pane.name, child=child)
new_models.append(child)
+ for obj in old_objects:
+ if obj not in self.objects:
+ obj._cleanup(root)
return new_models
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
def __setitem__(self, index, panes):
new_objects = list(self)
if not isinstance(index, slice):
@@ -415,11 +424,11 @@ class Spacer(Reactive):
_bokeh_model = BkSpacer
def _get_model(self, doc, root=None, parent=None, comm=None):
- model = self._bokeh_model(**self._process_param_change(self._init_properties()))
+ properties = self._process_param_change(self._init_properties())
+ model = self._bokeh_model(**properties)
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_params(model, ['width', 'height'], doc, root, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
diff --git a/panel/links.py b/panel/links.py
index cca215dd88..e106382bc1 100644
--- a/panel/links.py
+++ b/panel/links.py
@@ -180,7 +180,7 @@ def _resolve_model(cls, root_model, obj, model_spec):
model_spec = None
model = obj.handles[handle_spec]
elif isinstance(obj, Viewable):
- model = obj._models[root_model.ref['id']]
+ model, _ = obj._models[root_model.ref['id']]
elif isinstance(obj, BkModel):
model = obj
if model_spec is not None:
diff --git a/panel/models/plotly.ts b/panel/models/plotly.ts
index e0fe372f10..f3f824cd16 100644
--- a/panel/models/plotly.ts
+++ b/panel/models/plotly.ts
@@ -44,6 +44,8 @@ export class PlotlyPlotView extends HTMLBoxView {
}
_plot(): void {
+ if (this.model.data == null)
+ return
for (let i = 0; i < this.model.data.data.length; i++) {
const trace = this.model.data.data[i]
const cds = this.model.data_sources[i]
diff --git a/panel/models/vega.ts b/panel/models/vega.ts
index 06c5b81032..66f7969d84 100644
--- a/panel/models/vega.ts
+++ b/panel/models/vega.ts
@@ -76,6 +76,8 @@ export class VegaPlotView extends HTMLBoxView {
}
_plot(): void {
+ if (this.model.data == null)
+ return
if (!('datasets' in this.model.data)) {
const datasets = this._fetch_datasets()
if ('data' in datasets) {
diff --git a/panel/pane/base.py b/panel/pane/base.py
index b7b8751318..b2b9fc5c2c 100644
--- a/panel/pane/base.py
+++ b/panel/pane/base.py
@@ -4,6 +4,8 @@
"""
from __future__ import absolute_import, division, unicode_literals
+from functools import partial
+
import param
from ..io import state
@@ -55,55 +57,26 @@ class PaneBase(Reactive):
# Whether the Pane layout can be safely unpacked
_unpack = True
- __abstract = True
-
- @classmethod
- def applies(cls, obj):
- """
- Given the object return a boolean indicating whether the Pane
- can render the object. If the priority of the pane is set to
- None, this method may also be used to define a priority
- depending on the object being rendered.
- """
- return None
+ # List of parameters that trigger a rerender of the Bokeh model
+ _rerender_params = ['object']
- @classmethod
- def get_pane_type(cls, obj):
- if isinstance(obj, Viewable):
- return type(obj)
- descendents = []
- for p in param.concrete_descendents(PaneBase).values():
- priority = p.applies(obj) if p.priority is None else p.priority
- if isinstance(priority, bool) and priority:
- raise ValueError('If a Pane declares no priority '
- 'the applies method should return a '
- 'priority value specific to the '
- 'object type or False, but the %s pane '
- 'declares no priority.' % p.__name__)
- elif priority is None or priority is False:
- continue
- descendents.append((priority, p))
- pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
- for _, pane_type in pane_types:
- applies = pane_type.applies(obj)
- if isinstance(applies, bool) and not applies: continue
- return pane_type
- raise TypeError('%s type could not be rendered.' % type(obj).__name__)
+ __abstract = True
- def __init__(self, object, **params):
+ def __init__(self, object=None, **params):
applies = self.applies(object)
- if isinstance(applies, bool) and not applies:
+ if (isinstance(applies, bool) and not applies) and object is not None :
raise ValueError("%s pane does not support objects of type '%s'" %
(type(self).__name__, type(object).__name__))
super(PaneBase, self).__init__(object=object, **params)
kwargs = {k: v for k, v in params.items() if k in Layoutable.param}
self.layout = self.default_layout(self, **kwargs)
+ self.param.watch(self._update_pane, self._rerender_params)
def __repr__(self, depth=0):
cls = type(self).__name__
params = param_reprs(self, ['object'])
- obj = type(self.object).__name__
+ obj = 'Empty' if self.object is None else type(self.object).__name__
template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
return template.format(cls=cls, params=', '.join(params), obj=obj)
@@ -113,63 +86,109 @@ def __getitem__(self, index):
"""
return self.layout[index]
+ #----------------------------------------------------------------
+ # Callback API
+ #----------------------------------------------------------------
+
+ def _synced_params(self):
+ ignored_params = ['name', 'default_layout']+self._rerender_params
+ return [p for p in self.param if p not in ignored_params]
+
+ def _update_object(self, old_model, doc, root, parent, comm):
+ if self._updates:
+ self._update(old_model)
+ else:
+ new_model = self._get_model(doc, root, parent, comm)
+ try:
+ index = parent.children.index(old_model)
+ except IndexError:
+ self.warning('%s pane model %s could not be replaced '
+ 'with new model %s, ensure that the '
+ 'parent is not modified at the same '
+ 'time the panel is being updated.' %
+ (type(self).__name__, old_model, new_model))
+ else:
+ parent.children[index] = new_model
+
+ def _update_pane(self, event):
+ for ref, (model, parent) in self._models.items():
+ viewable, root, doc, comm = state._views[ref]
+ if comm or state.curdoc:
+ self._update_object(model, doc, root, parent, comm)
+ if comm:
+ push(doc, comm)
+ else:
+ cb = partial(self._update_object, model, doc, root, parent, comm)
+ doc.add_next_tick_callback(cb)
+
+ def _update(self, model):
+ """
+ If _updates=True this method is used to update an existing
+ Bokeh model instead of replacing the model entirely. The
+ supplied model should be updated with the current state.
+ """
+ raise NotImplementedError
+
+ #----------------------------------------------------------------
+ # Model API
+ #----------------------------------------------------------------
+
def _get_root(self, doc, comm=None):
if self._updates:
root = self._get_model(doc, comm=comm)
else:
root = self.layout._get_model(doc, comm=comm)
self._preprocess(root)
+ ref = root.ref['id']
+ state._views[ref] = (self, root, doc, comm)
return root
- def _cleanup(self, root=None, final=False):
- super(PaneBase, self)._cleanup(root, final)
- if final:
- self.object = None
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
- def _update(self, model):
+ @classmethod
+ def applies(cls, obj):
"""
- If _updates=True this method is used to update an existing Bokeh
- model instead of replacing the model entirely. The supplied model
- should be updated with the current state.
+ Given the object return a boolean indicating whether the Pane
+ can render the object. If the priority of the pane is set to
+ None, this method may also be used to define a priority
+ depending on the object being rendered.
"""
- raise NotImplementedError
+ return None
- 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.
+ @classmethod
+ def get_pane_type(cls, obj):
"""
- ref = root.ref['id']
-
- def update_pane(change):
- old_model = self._models[ref]
+ Returns the applicable Pane type given an object by resolving
+ the precedence of all types whose applies method declares that
+ the object is supported.
- if self._updates:
- # Pane supports model updates
- def update_models():
- self._update(old_model)
- else:
- # Otherwise replace the whole model
- new_model = self._get_model(doc, root, parent, comm)
- def update_models():
- try:
- index = parent.children.index(old_model)
- except IndexError:
- self.warning('%s pane model %s could not be replaced '
- 'with new model %s, ensure that the '
- 'parent is not modified at the same '
- 'time the panel is being updated.' %
- (type(self).__name__, old_model, new_model))
- else:
- parent.children[index] = new_model
-
- if comm:
- update_models()
- push(doc, comm)
- elif state.curdoc:
- update_models()
- else:
- doc.add_next_tick_callback(update_models)
+ Parameters
+ ----------
+ obj (object): The object type to return a Pane for
- if ref not in self._callbacks:
- self._callbacks[ref].append(self.param.watch(update_pane, 'object'))
+ Returns
+ -------
+ The applicable Pane type with the highest precedence.
+ """
+ if isinstance(obj, Viewable):
+ return type(obj)
+ descendents = []
+ for p in param.concrete_descendents(PaneBase).values():
+ priority = p.applies(obj) if p.priority is None else p.priority
+ if isinstance(priority, bool) and priority:
+ raise ValueError('If a Pane declares no priority '
+ 'the applies method should return a '
+ 'priority value specific to the '
+ 'object type or False, but the %s pane '
+ 'declares no priority.' % p.__name__)
+ elif priority is None or priority is False:
+ continue
+ descendents.append((priority, p))
+ pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
+ for _, pane_type in pane_types:
+ applies = pane_type.applies(obj)
+ if isinstance(applies, bool) and not applies: continue
+ return pane_type
+ raise TypeError('%s type could not be rendered.' % type(obj).__name__)
diff --git a/panel/pane/equation.py b/panel/pane/equation.py
index 648137993e..a4a4e18ca0 100644
--- a/panel/pane/equation.py
+++ b/panel/pane/equation.py
@@ -80,6 +80,8 @@ class LaTeX(PNG):
dpi = param.Number(default=72, bounds=(1, 1900), doc="""
Resolution per inch for the rendered equation.""")
+ _rerender_params = ['object', 'size', 'dpi']
+
@classmethod
def applies(cls, obj):
if is_sympy_expr(obj) or hasattr(obj, '_repr_latex_'):
@@ -100,7 +102,7 @@ def _imgshape(self, data):
return int(w*72), int(h*72)
def _img(self):
- obj=self.object # Default: LaTeX string
+ obj = self.object # Default: LaTeX string
if hasattr(obj, '_repr_latex_'):
obj = obj._repr_latex_()
diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py
index 573bd9a576..128311d639 100644
--- a/panel/pane/holoviews.py
+++ b/panel/pane/holoviews.py
@@ -5,7 +5,9 @@
from __future__ import absolute_import, division, unicode_literals
import sys
+
from collections import OrderedDict, defaultdict
+from functools import partial
import param
@@ -14,6 +16,8 @@
from ..viewable import Viewable
from ..widgets import Player
from .base import PaneBase, Pane
+from .plot import Bokeh, Matplotlib
+from .plotly import Plotly
class HoloViews(PaneBase):
@@ -37,27 +41,39 @@ class HoloViews(PaneBase):
priority = 0.8
+ _rerender_params = ['object', 'widgets', 'backend', 'widget_type']
+
+ _panes = {'bokeh': Bokeh, 'matplotlib': Matplotlib, 'plotly': Plotly}
+
def __init__(self, object, **params):
super(HoloViews, self).__init__(object, **params)
self.widget_box = Column()
self._update_widgets()
self._plots = {}
+ self.param.watch(self._update_widgets, self._rerender_params)
+
+ #----------------------------------------------------------------
+ # Callback API
+ #----------------------------------------------------------------
- @param.depends('object', 'widgets', watch=True)
- def _update_widgets(self):
+ def _update_widgets(self, *events):
if self.object is None:
- widgets, values = []
+ widgets, values = [], []
else:
widgets, values = self.widgets_from_dimensions(self.object, self.widgets,
self.widget_type)
self._values = values
# Clean up anything models listening to the previous widgets
- for _, cbs in self._callbacks.items():
- for cb in list(cbs):
- if cb.inst in self.widget_box.objects:
- cb.inst.param.unwatch(cb)
- cbs.remove(cb)
+ for cb in list(self._callbacks):
+ if cb.inst in self.widget_box.objects:
+ cb.inst.param.unwatch(cb)
+ self._callbacks.remove(cb)
+
+ # Add new widget callbacks
+ for widget in widgets:
+ watcher = widget.param.watch(self._widget_callback, 'value')
+ self._callbacks.append(watcher)
self.widget_box.objects = widgets
if widgets and not self.widget_box in self.layout.objects:
@@ -65,23 +81,49 @@ def _update_widgets(self):
elif not widgets and self.widget_box in self.layout.objects:
self.layout.pop(self.widget_box)
- @classmethod
- def applies(cls, obj):
- if 'holoviews' not in sys.modules:
- return False
- from holoviews.core.dimension import Dimensioned
- return isinstance(obj, Dimensioned)
+ def _update_plot(self, plot, pane, event):
+ from holoviews.core.util import cross_index
+ from holoviews.plotting.bokeh.plot import BokehPlot
- def _cleanup(self, root=None, final=False):
- """
- Traverses HoloViews object to find and clean up any streams
- connected to existing plots.
- """
- if root is not None:
- old_plot = self._plots.pop(root.ref['id'], None)
- if old_plot:
- old_plot.cleanup()
- super(HoloViews, self)._cleanup(root, final)
+ widgets = self.widget_box.objects
+ if self.widget_type == 'scrubber':
+ key = cross_index([v for v in self._values.values()], widgets[0].value)
+ else:
+ key = tuple(w.value for w in widgets)
+
+ if isinstance(plot, BokehPlot):
+ if plot.comm or state.curdoc:
+ plot.update(key)
+ if plot.comm:
+ plot.push()
+ else:
+ plot.document.add_next_tick_callback(partial(plot.update, key))
+ else:
+ plot.update(key)
+ pane.object = plot.state
+
+ def _widget_callback(self, event):
+ for ref, (plot, pane) in self._plots.items():
+ self._update_plot(plot, pane, event)
+
+ #----------------------------------------------------------------
+ # Model API
+ #----------------------------------------------------------------
+
+ def _get_model(self, doc, root=None, parent=None, comm=None):
+ if root is None:
+ return self._get_root(doc, comm)
+ ref = root.ref['id']
+ plot = self._render(doc, comm, root)
+ child_pane = self._panes.get(self.backend, Pane)(plot.state)
+ model = child_pane._get_model(doc, root, parent, comm)
+ if ref in self._plots:
+ old_plot, old_pane = self._plots[ref]
+ old_plot.comm = None # Ensure comm does not cleaned up
+ old_plot.cleanup()
+ self._plots[ref] = (plot, child_pane)
+ self._models[ref] = (model, parent)
+ return model
def _render(self, doc, comm, root):
from holoviews import Store, renderer
@@ -96,72 +138,46 @@ def _render(self, doc, comm, root):
kwargs = {'doc': doc, 'root': root} if backend == 'bokeh' else {}
if comm:
kwargs['comm'] = comm
- plot = renderer.get_plot(self.object, **kwargs)
- ref = root.ref['id']
- if ref in self._plots:
- old_plot = self._plots[ref]
- old_plot.comm = None
- old_plot.cleanup()
- self._plots[root.ref['id']] = plot
- return plot
-
- def _get_model(self, doc, root=None, parent=None, comm=None):
- if root is None:
- return self._get_root(doc, comm)
- ref = root.ref['id']
- plot = self._render(doc, comm, root)
- child_pane = Pane(plot.state, _temporary=True)
- model = child_pane._get_model(doc, root, parent, comm)
- self._models[ref] = model
- self._link_object(doc, root, parent, comm)
- if self.widget_box.objects:
- self._link_widgets(child_pane, root, comm)
- return model
-
- def _link_widgets(self, pane, root, comm):
- def update_plot(change):
- from holoviews.core.util import cross_index
- from holoviews.plotting.bokeh.plot import BokehPlot
+ return renderer.get_plot(self.object, **kwargs)
- widgets = self.widget_box.objects
- if self.widget_type == 'scrubber':
- key = cross_index([v for v in self._values.values()], widgets[0].value)
- else:
- key = tuple(w.value for w in widgets)
+ def _cleanup(self, root):
+ """
+ Traverses HoloViews object to find and clean up any streams
+ connected to existing plots.
+ """
+ old_plot, old_pane = self._plots.pop(root.ref['id'], None)
+ if old_plot:
+ old_plot.cleanup()
+ if old_pane:
+ old_pane._cleanup(root)
+ super(HoloViews, self)._cleanup(root)
- plot = self._plots[root.ref['id']]
- if isinstance(plot, BokehPlot):
- 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)
- pane.object = plot.state
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
- ref = root.ref['id']
- for w in self.widget_box.objects:
- watcher = w.param.watch(update_plot, 'value')
- self._callbacks[ref].append(watcher)
+ @classmethod
+ def applies(cls, obj):
+ if 'holoviews' not in sys.modules:
+ return False
+ from holoviews.core.dimension import Dimensioned
+ return isinstance(obj, Dimensioned)
@classmethod
def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individual'):
from holoviews.core import Dimension
from holoviews.core.util import isnumeric, unicode, datetime_types
from holoviews.core.traversal import unique_dimkeys
+ from holoviews.plotting.util import get_dynamic_mode
from ..widgets import Widget, DiscreteSlider, Select, FloatSlider, DatetimeInput
+ dynamic, bounded = get_dynamic_mode(object)
dims, keys = unique_dimkeys(object)
if dims == [Dimension('Frame')] and keys == [(0,)]:
return [], {}
nframes = 1
- values = dict(zip(dims, zip(*keys)))
+ values = dict() if dynamic else dict(zip(dims, zip(*keys)))
dim_values = OrderedDict()
widgets = []
for dim in dims:
@@ -182,7 +198,7 @@ def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individu
else:
raise ValueError('Explicit widget definitions expected '
'to be a widget instance or type, %s '
- 'dimension widget declared as %s.' %
+ 'dimension widget declared as %s.' %
(dim, widget))
if vals:
if all(isnumeric(v) or isinstance(v, datetime_types) for v in vals) and len(vals) > 1:
@@ -230,11 +246,11 @@ def generate_panel_bokeh_map(root_model, panel_views):
"""
map_hve_bk = defaultdict(list)
for pane in panel_views:
- if root_model.ref['id'] in pane._models:
- bk_plots = pane._plots[root_model.ref['id']].traverse(lambda x: x, [is_bokeh_element_plot])
+ if root_model.ref['id'] in pane._models:
+ bk_plots = pane._plots[root_model.ref['id']][0].traverse(lambda x: x, [is_bokeh_element_plot])
for plot in bk_plots:
for hv_elem in plot.link_sources:
- map_hve_bk[hv_elem].append(plot)
+ map_hve_bk[hv_elem].append(plot)
return map_hve_bk
@@ -247,7 +263,7 @@ def find_links(root_view, root_model):
return
hv_views = root_view.select(HoloViews)
- root_plots = [plot for view in hv_views for plot in view._plots.values()
+ root_plots = [plot for view in hv_views for plot, _ in view._plots.values()
if getattr(plot, 'root', None) is root_model]
if not root_plots:
diff --git a/panel/pane/image.py b/panel/pane/image.py
index 0c737835fa..40c525871c 100644
--- a/panel/pane/image.py
+++ b/panel/pane/image.py
@@ -58,6 +58,8 @@ def _imgshape(self, data):
def _get_properties(self):
p = super(ImageBase, self)._get_properties()
+ if self.object is None:
+ return dict(p, text='')
data = self._img()
if isinstance(data, str):
data = base64.b64decode(data)
@@ -144,6 +146,8 @@ def _imgshape(self, data):
def _get_properties(self):
p = super(ImageBase, self)._get_properties()
+ if self.object is None:
+ return dict(p, text='
')
data = self._img()
width, height = self._imgshape(data)
if not isinstance(data, bytes):
diff --git a/panel/pane/markup.py b/panel/pane/markup.py
index 2ed249aa11..0bbb62ae1c 100644
--- a/panel/pane/markup.py
+++ b/panel/pane/markup.py
@@ -36,15 +36,14 @@ class DivPaneBase(PaneBase):
_rename = {'object': 'text'}
def _get_properties(self):
- return {p : getattr(self,p) for p in list(Layoutable.param) + ['style']
+ return {p : getattr(self, p) for p in list(Layoutable.param) + ['style']
if getattr(self, p) is not None}
def _get_model(self, doc, root=None, parent=None, comm=None):
model = _BkDiv(**self._get_properties())
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_object(doc, root, parent, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
def _update(self, model):
@@ -74,9 +73,9 @@ def applies(cls, obj):
def _get_properties(self):
properties = super(HTML, self)._get_properties()
- text=self.object
+ text = '' if self.object is None else self.object
if hasattr(text, '_repr_html_'):
- text=text._repr_html_()
+ text = text._repr_html_()
return dict(properties, text=text)
@@ -97,7 +96,11 @@ def applies(cls, obj):
def _get_properties(self):
properties = super(Str, self)._get_properties()
- return dict(properties, text='
'+escape(str(self.object))+'') + if self.object is None: + text = '' + else: + text = '
'+escape(str(self.object))+'' + return dict(properties, text=text) class Markdown(DivPaneBase): @@ -123,11 +126,12 @@ def applies(cls, obj): def _get_properties(self): import markdown data = self.object - if not isinstance(data, string_types): + if data is None: + data = '' + elif not isinstance(data, string_types): data = data._repr_markdown_() properties = super(Markdown, self)._get_properties() properties['style'] = properties.get('style', {}) extensions = ['markdown.extensions.extra', 'markdown.extensions.smarty'] - html = markdown.markdown(self.object, extensions=extensions, - output_format='html5') + html = markdown.markdown(data, extensions=extensions, output_format='html5') return dict(properties, text=html) diff --git a/panel/pane/plot.py b/panel/pane/plot.py index e9ab910623..303e7a691e 100644 --- a/panel/pane/plot.py +++ b/panel/pane/plot.py @@ -9,7 +9,7 @@ import param -from bokeh.models import LayoutDOM, CustomJS +from bokeh.models import LayoutDOM, CustomJS, Spacer as BkSpacer from ..util import remove_root from .base import PaneBase @@ -32,7 +32,10 @@ def _get_model(self, doc, root=None, parent=None, comm=None): if root is None: return self._get_root(doc, comm) - model = self.object + if self.object is None: + model = BkSpacer() + else: + model = self.object ref = root.ref['id'] for js in model.select({'type': CustomJS}): js.code = js.code.replace(model.ref['id'], ref) @@ -40,8 +43,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None): if model._document and doc is not model._document: remove_root(model, doc) - self._models[ref] = model - self._link_object(doc, root, parent, comm) + self._models[ref] = (model, parent) return model @@ -57,6 +59,8 @@ class Matplotlib(PNG): dpi = param.Integer(default=144, bounds=(1, None), doc=""" Scales the dpi of the matplotlib figure.""") + _rerender_params = ['object', 'dpi'] + @classmethod def applies(cls, obj): if 'matplotlib' not in sys.modules: @@ -92,6 +96,8 @@ class RGGPlot(PNG): dpi = param.Integer(default=144, bounds=(1, None)) + _rerender_params = ['object', 'dpi', 'width', 'height'] + @classmethod def applies(cls, obj): return type(obj).__name__ == 'GGPlot' and hasattr(obj, 'r_repr') @@ -124,6 +130,8 @@ def applies(cls, obj): def _get_properties(self): p = super(YT, self)._get_properties() + if self.object is None: + return p width = height = 0 if self.width is None or self.height is None: diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index 3ba19bc2f5..d7955cc7e0 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -4,7 +4,6 @@ """ from __future__ import absolute_import, division, unicode_literals -import param import numpy as np from bokeh.models import ColumnDataSource @@ -22,53 +21,61 @@ class Plotly(PaneBase): the figure on bokeh server and via Comms. """ - plotly_layout = param.Dict() - _updates = True priority = 0.8 - def __init__(self, object, layout=None, **params): - super(Plotly, self).__init__(self._to_figure(object, layout), - plotly_layout=layout, **params) - @classmethod def applies(cls, obj): return ((isinstance(obj, list) and obj and all(cls.applies(o) for o in obj)) or - hasattr(obj, 'to_plotly_json')) + hasattr(obj, 'to_plotly_json') or (isinstance(obj, dict) + and 'data' in obj and 'layout' in obj)) - def _to_figure(self, obj, layout={}): + def _to_figure(self, obj): import plotly.graph_objs as go if isinstance(obj, go.Figure): - fig = obj + return obj + elif isinstance(obj, dict): + data, layout = obj['data'], obj['layout'] + elif isinstance(obj, tuple): + data, layout = obj else: - data = obj if isinstance(obj, list) else [obj] - fig = go.Figure(data=data, layout=layout) - return fig + data, layout = obj, {} + data = data if isinstance(data, list) else [data] + return go.Figure(data=data, layout=layout) - def _get_model(self, doc, root=None, parent=None, comm=None): - """ - Should return the bokeh model to be rendered. - """ - fig = self._to_figure(self.object, self.plotly_layout) - json = fig.to_plotly_json() - traces = json['data'] + def _get_sources(self, json): sources = [] + traces = json['data'] for trace in traces: data = {} for key, value in list(trace.items()): if isinstance(value, np.ndarray): data[key] = trace.pop(key) sources.append(ColumnDataSource(data)) + return sources + + def _get_model(self, doc, root=None, parent=None, comm=None): + """ + Should return the bokeh model to be rendered. + """ + if self.object is None: + json, sources = None, [] + else: + fig = self._to_figure(self.object) + json = fig.to_plotly_json() + sources = self._get_sources(json) model = PlotlyPlot(data=json, data_sources=sources) if root is None: root = model - self._models[root.ref['id']] = model - self._link_object(doc, root, parent, comm) + self._models[root.ref['id']] = (model, parent) return model def _update(self, model): - fig = self._to_figure(self.object, self.plotly_layout) + if self.object is None: + model.data = None + return + fig = self._to_figure(self.object) json = fig.to_plotly_json() traces = json['data'] new_sources = [] diff --git a/panel/pane/vega.py b/panel/pane/vega.py index 199c8b8626..b3fa6d36c7 100644 --- a/panel/pane/vega.py +++ b/panel/pane/vega.py @@ -37,9 +37,6 @@ class Vega(PaneBase): _updates = True - def __init__(self, object, **params): - super(Vega, self).__init__(object, **params) - @classmethod def is_altair(cls, obj): if 'altair' in sys.modules: @@ -81,19 +78,24 @@ def _get_sources(self, json, sources): sources['data'] = ColumnDataSource(data=ds_as_cds(data)) def _get_model(self, doc, root=None, parent=None, comm=None): - json = self._to_json(self.object) - json['data'] = dict(json['data']) sources = {} - self._get_sources(json, sources) + if self.object is None: + json = None + else: + json = self._to_json(self.object) + json['data'] = dict(json['data']) + self._get_sources(json, sources) model = VegaPlot(data=json, data_sources=sources) if root is None: root = model - self._models[root.ref['id']] = model - self._link_object(doc, root, parent, comm) + self._models[root.ref['id']] = (model, parent) return model def _update(self, model): - json = self._to_json(self.object) - self._get_sources(json, model.data_sources) + if self.object is None: + json = None + else: + json = self._to_json(self.object) + self._get_sources(json, model.data_sources) model.data = json diff --git a/panel/param.py b/panel/param.py index 6cb2cd14ec..4b30a87322 100644 --- a/panel/param.py +++ b/panel/param.py @@ -18,6 +18,7 @@ from param.parameterized import classlist +from .io import state from .layout import Row, Panel, Tabs, Column from .links import Link from .pane.base import Pane, PaneBase @@ -28,7 +29,7 @@ from .widgets import ( LiteralInput, Select, Checkbox, FloatSlider, IntSlider, RangeSlider, MultiSelect, StaticText, Button, Toggle, TextInput, DiscreteSlider, - DatetimeInput, DateRangeSlider, ColorPicker) + DatetimeInput, DateRangeSlider, ColorPicker, Widget) def ObjectSelector(pobj): @@ -91,7 +92,7 @@ class Param(PaneBase): usually to update the default Parameter values of the underlying parameterized object.""") - parameters = param.List(default=None, doc=""" + parameters = param.List(default=[], doc=""" If set this serves as a whitelist of parameters to display on the supplied Parameterized object.""") @@ -130,27 +131,19 @@ class Param(PaneBase): param.DateRange: DateRangeSlider } - @classmethod - def applies(cls, obj): - return (is_parameterized(obj) or isinstance(obj, param.parameterized.Parameters)) + _rerender_params = [] - def __init__(self, object, **params): + def __init__(self, object=None, **params): if isinstance(object, param.parameterized.Parameters): object = object.cls if object.self is None else object.self - if 'name' not in params: - params['name'] = object.name - if 'parameters' not in params: + if 'parameters' not in params and object is not None: params['parameters'] = [p for p in object.param if p != 'name'] super(Param, self).__init__(object, **params) self._updating = False - # Construct widgets - self._widgets = self._get_widgets() - widgets = [widget for widgets in self._widgets.values() for widget in widgets] - # Construct Layout kwargs = {p: v for p, v in self.get_param_values() if p in Layoutable.param} - self._widget_box = self.default_layout(*widgets, **kwargs) + self._widget_box = self.default_layout(**kwargs) layout = self.expand_layout if isinstance(layout, Panel): @@ -164,9 +157,10 @@ def __init__(self, object, **params): raise ValueError('expand_layout expected to be a panel.layout.Panel' 'type or instance, found %s type.' % type(layout).__name__) - - if not (self.expand_button == False and not self.expand): - self._link_subobjects() + self.param.watch(self._update_widgets, [ + 'object', 'parameters', 'display_threshold', 'expand_button', + 'expand', 'expand_layout', 'widgets', 'show_labels', 'show_name']) + self._update_widgets() def __repr__(self, depth=0): cls = type(self).__name__ @@ -178,7 +172,7 @@ def __repr__(self, depth=0): if v is self.param[p].default: continue elif v is None: continue elif isinstance(v, string_types) and v == '': continue - elif p == 'object' or (p == 'name' and v.startswith(obj_cls)): continue + elif p == 'object' or (p == 'name' and (v.startswith(obj_cls) or v.startswith(cls))): continue elif p == 'parameters' and v == parameters: continue try: params.append('%s=%s' % (p, abbreviated_repr(v))) @@ -188,6 +182,47 @@ def __repr__(self, depth=0): template = '{cls}({obj}, {params})' if params else '{cls}({obj})' return template.format(cls=cls, params=', '.join(params), obj=obj) + #---------------------------------------------------------------- + # Callback API + #---------------------------------------------------------------- + + def _synced_params(self): + ignored_params = ['name', 'default_layout'] + return [p for p in Layoutable.param if p not in ignored_params] + + def _update_widgets(self, *events): + parameters = [] + for event in sorted(events, key=lambda x: x.name): + if event.name == 'object': + if isinstance(event.new, param.parameterized.Parameters): + self.object = object.cls if object.self is None else object.self + return + if event.new is None: + parameters = None + else: + parameters = [p for p in event.new.param if p != 'name'] + if event.name == 'parameters': + parameters = None if event.new == [] else event.new + + if parameters != [] and parameters != self.parameters: + self.parameters = parameters + return + + for cb in list(self._callbacks): + if cb.inst in self._widget_box.objects: + cb.inst.param.unwatch(cb) + self._callbacks.remove(cb) + + # Construct widgets + if self.object is None: + self._widgets = {} + else: + self._widgets = self._get_widgets() + widgets = [widget for widgets in self._widgets.values() for widget in widgets] + self._widget_box.objects = widgets + if not (self.expand_button == False and not self.expand): + self._link_subobjects() + def _link_subobjects(self): for pname, widgets in self._widgets.items(): if not any(is_parameterized(getattr(w, 'value', None)) or @@ -205,13 +240,12 @@ def toggle_pane(change, parameter=pname): if existing: old_panel = existing[0] if not change.new: - old_panel._cleanup(final=old_panel._temporary) - self._expand_layout.pop(old_panel) + self._expand_layout.remove(old_panel) elif change.new: kwargs = {k: v for k, v in self.get_param_values() if k not in ['name', 'object', 'parameters']} pane = Param(parameterized, name=parameterized.name, - _temporary=True, **kwargs) + **kwargs) if isinstance(self._expand_layout, Tabs): title = self.object.param[pname].label pane = (title, pane) @@ -232,7 +266,7 @@ def update_pane(change, parameter=pname): kwargs = {k: v for k, v in self.get_param_values() if k not in ['name', 'object', 'parameters']} pane = Param(parameterized, name=parameterized.name, - _temporary=True, **kwargs) + **kwargs) layout[layout.objects.index(existing[0])] = pane else: layout.pop(existing[0]) @@ -240,7 +274,7 @@ def update_pane(change, parameter=pname): watchers = [selector.param.watch(update_pane, 'value')] if toggle: watchers.append(toggle.param.watch(toggle_pane, 'value')) - self._callbacks['instance'] += watchers + self._callbacks += watchers if self.expand: if self.expand_button: @@ -248,14 +282,6 @@ def update_pane(change, parameter=pname): else: toggle_pane(namedtuple('Change', 'new')(True)) - def widget_type(cls, pobj): - ptype = type(pobj) - for t in classlist(ptype)[::-1]: - if t in cls._mapping: - if isinstance(cls._mapping[t], types.FunctionType): - return cls._mapping[t](pobj) - return cls._mapping[t] - def widget(self, p_name): """Get widget for param_name""" p_obj = self.object.param[p_name] @@ -266,7 +292,8 @@ def widget(self, p_name): widget_class = self.widgets[p_name] value = getattr(self.object, p_name) - kw = dict(value=value, disabled=p_obj.constant, name=p_obj.label) + label = p_obj.label if self.show_labels else '' + kw = dict(value=value, disabled=p_obj.constant, name=label) if hasattr(p_obj, 'get_range'): options = p_obj.get_range() @@ -284,8 +311,13 @@ def widget(self, p_name): widget_class = LiteralInput kwargs = {k: v for k, v in kw.items() if k in widget_class.param} - widget = widget_class(**kwargs) - watchers = self._callbacks['instance'] + + if isinstance(widget_class, Widget): + widget = widget_class + else: + widget = widget_class(**kwargs) + + watchers = self._callbacks if isinstance(p_obj, param.Action): widget.button_type = 'success' def action(change): @@ -356,9 +388,9 @@ def link(change): else: return [widget] - def _cleanup(self, root=None, final=False): - self.layout._cleanup(root, final) - super(Param, self)._cleanup(root, final) + #---------------------------------------------------------------- + # Model API + #---------------------------------------------------------------- def _get_widgets(self): """Return name,widget boxes for all parameters (i.e., a property sheet)""" @@ -389,14 +421,36 @@ def _get_widgets(self): def _get_root(self, doc, comm=None): root = self.layout._get_root(doc, comm) - self._models[root.ref['id']] = root + ref = root.ref['id'] + self._models[ref] = (root, None) + state._views[ref] = (self, root, doc, comm) return root def _get_model(self, doc, root=None, parent=None, comm=None): model = self.layout._get_model(doc, root, parent, comm) - self._models[root.ref['id']] = model + self._models[root.ref['id']] = (model, parent) return model + def _cleanup(self, root): + self.layout._cleanup(root) + super(Param, self)._cleanup(root) + + #---------------------------------------------------------------- + # Public API + #---------------------------------------------------------------- + + @classmethod + def applies(cls, obj): + return (is_parameterized(obj) or isinstance(obj, param.parameterized.Parameters)) + + @classmethod + def widget_type(cls, pobj): + ptype = type(pobj) + for t in classlist(ptype)[::-1]: + if t in cls._mapping: + if isinstance(cls._mapping[t], types.FunctionType): + return cls._mapping[t](pobj) + return cls._mapping[t] class ParamMethod(PaneBase): @@ -413,17 +467,17 @@ def __init__(self, object, **params): self._kwargs = {p: params.pop(p) for p in list(params) if p not in self.param} super(ParamMethod, self).__init__(object, **params) - kwargs = dict(self.get_param_values(), **dict(self._kwargs, _temporary=True)) + kwargs = dict(self.get_param_values(), **self._kwargs) del kwargs['object'] self._pane = Pane(self.object(), **kwargs) self._inner_layout = Row(self._pane, **{k: v for k, v in params.items() if k in Row.param}) + self._link_object_params() - @classmethod - def applies(cls, obj): - return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized) + #---------------------------------------------------------------- + # Callback API + #---------------------------------------------------------------- - def _link_object_params(self, doc, root, parent, comm): - ref = root.ref['id'] + def _link_object_params(self): parameterized = get_method_owner(self.object) params = parameterized.param.params_depended_on(self.object.__name__) deps = params @@ -434,7 +488,7 @@ def update_pane(*events): new_deps = parameterized.param.params_depended_on(self.object.__name__) for p in list(deps): if p in new_deps: continue - watchers = self._callbacks.get(ref, []) + watchers = self._callbacks for w in list(watchers): if (w.inst is p.inst and w.cls is p.cls and p.name in w.parameter_names): @@ -449,7 +503,7 @@ def update_pane(*events): pobj = p.cls if p.inst is None else p.inst ps = [_p.name for _p in params] watcher = pobj.param.watch(update_pane, ps, p.what) - self._callbacks[ref].append(watcher) + self._callbacks.append(watcher) for p in params: deps.append(p) @@ -465,18 +519,13 @@ def update_pane(*events): pvals = dict(self._pane.get_param_values()) new_params = {k: v for k, v in new_object.get_param_values() if k != 'name' and v is not pvals[k]} - try: - self._pane.set_param(**new_params) - except: - raise - finally: - new_object._cleanup(final=new_object._temporary) + self._pane.set_param(**new_params) else: self._pane.object = new_object return # Replace pane entirely - kwargs = dict(self.get_param_values(), _temporary=True, **self._kwargs) + kwargs = dict(self.get_param_values(), **self._kwargs) del kwargs['object'] self._pane = Pane(new_object, **kwargs) self._inner_layout[0] = self._pane @@ -486,23 +535,34 @@ def update_pane(*events): pobj = (p.inst or p.cls) ps = [_p.name for _p in params] watcher = pobj.param.watch(update_pane, ps, p.what) - self._callbacks[ref].append(watcher) + self._callbacks.append(watcher) + + #---------------------------------------------------------------- + # Model API + #---------------------------------------------------------------- def _get_model(self, doc, root=None, parent=None, comm=None): if root is None: return self._get_root(doc, comm) ref = root.ref['id'] - if ref in self._callbacks: + if ref in self._models: self._cleanup(root) model = self._inner_layout._get_model(doc, root, parent, comm) - self._link_object_params(doc, root, parent, comm) - self._models[ref] = model + self._models[ref] = (model, parent) return model - def _cleanup(self, root=None, final=False): - self._inner_layout._cleanup(root, final) - super(ParamMethod, self)._cleanup(root, final) + def _cleanup(self, root=None): + self._inner_layout._cleanup(root) + super(ParamMethod, self)._cleanup(root) + + #---------------------------------------------------------------- + # Public API + #---------------------------------------------------------------- + + @classmethod + def applies(cls, obj): + return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized) class JSONInit(param.Parameterized): diff --git a/panel/tests/test_holoviews.py b/panel/tests/test_holoviews.py index 56c1d7840c..9eca3a8c8c 100644 --- a/panel/tests/test_holoviews.py +++ b/panel/tests/test_holoviews.py @@ -6,7 +6,8 @@ import pytest from bokeh.models import (Row as BkRow, Column as BkColumn, GlyphRenderer, - Scatter, Line, GridBox) + Scatter, Line, GridBox, Select as BkSelect, + Slider as BkSlider) from bokeh.plotting import Figure from panel.layout import Column, Row @@ -19,7 +20,6 @@ hv = None hv_available = pytest.mark.skipif(hv is None, reason="requires holoviews") -from .test_layout import get_div from .test_panes import mpl_available @@ -40,22 +40,44 @@ def test_holoviews_pane_mpl_renderer(document, comm): row = pane._get_root(document, comm=comm) assert isinstance(row, BkRow) assert len(row.children) == 1 - assert len(pane._callbacks) == 1 model = row.children[0] - assert pane._models[row.ref['id']] is model - div = get_div(model) - assert '
True' @@ -158,19 +156,17 @@ def test(a): return a if a else BkDiv(text='Test') interact_pane = interactive(test, a=False) - pane = interact_pane._pane widget = interact_pane._widgets['a'] assert isinstance(widget, widgets.Checkbox) assert widget.value == False - column = interact_pane.layout._get_model(document, comm=comm) + column = interact_pane.layout._get_root(document, comm=comm) assert isinstance(column, BkColumn) - div = get_div(column.children[1].children[0]) + div = column.children[1].children[0] assert div.text == 'Test' - + widget.value = True - assert pane._callbacks == {} - div = get_div(column.children[1].children[0]) + div = column.children[1].children[0] assert div.text == '
True' def test_interact_replaces_model(document, comm): @@ -183,27 +179,20 @@ def test(a): assert isinstance(widget, widgets.Checkbox) assert widget.value == False - column = interact_pane.layout._get_model(document, comm=comm) + column = interact_pane.layout._get_root(document, comm=comm) assert isinstance(column, BkColumn) div = column.children[1].children[0] assert isinstance(div, BkDiv) assert div.text == 'Test' - assert len(interact_pane._callbacks['instance']) == 1 - assert column.ref['id'] in pane._callbacks - assert pane._models[column.ref['id']] is div - + assert pane._models[column.ref['id']][0] is div + widget.value = True - assert column.ref['id'] not in pane._callbacks - assert pane._callbacks == {} new_pane = interact_pane._pane assert new_pane is not pane new_div = column.children[1].children[0] assert isinstance(new_div, BkDiv) assert new_div.text == '
ABC
' - assert len(interact_pane._callbacks['instance']) == 1 - assert column.ref['id'] in new_pane._callbacks - assert new_pane._models[column.ref['id']] is new_div + assert new_pane._models[column.ref['id']][0] is new_div interact_pane._cleanup(column) assert len(interact_pane._callbacks) == 1 - assert pane._callbacks == {} diff --git a/panel/tests/test_layout.py b/panel/tests/test_layout.py index 622ec1f73d..6ad7a0d1b6 100644 --- a/panel/tests/test_layout.py +++ b/panel/tests/test_layout.py @@ -8,6 +8,7 @@ from panel.pane import Bokeh, Pane from panel.param import Param + @pytest.fixture def tabs(document, comm): """Set up a tabs instance""" @@ -21,15 +22,18 @@ def assert_tab_is_similar(tab1, tab2): assert tab1.name == tab2.name assert tab1.title == tab2.title -def get_div(box): - # Temporary utilities to unpack widget boxes - if isinstance(box, Div): - return box - return box.children[0] +@pytest.mark.parametrize('layout', [Column, Row, Tabs, Spacer]) +def test_layout_model_cache_cleanup(layout, document, comm): + l = layout() + + model = l._get_root(document, comm) -def get_divs(children): - return [get_div(c) for c in children] + assert model.ref['id'] in l._models + assert l._models[model.ref['id']] == (model, None) + + l._cleanup(model) + assert l._models == {} @pytest.mark.parametrize('panel', [Column, Row]) @@ -158,12 +162,10 @@ def test_layout_setitem(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] div3 = Div() layout[0] = div3 assert model.children == [div3, div2] - assert p1._callbacks == {} assert p1._models == {} @@ -188,15 +190,12 @@ def test_layout_setitem_replace_all(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] div3 = Div() div4 = Div() layout[:] = [div3, div4] assert model.children == [div3, div4] - assert p1._callbacks == {} assert p1._models == {} - assert p2._callbacks == {} assert p2._models == {} @@ -222,15 +221,12 @@ def test_layout_setitem_replace_slice(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] div3 = Div() div4 = Div() layout[1:] = [div3, div4] assert model.children == [div1, div3, div4] - assert p2._callbacks == {} assert p2._models == {} - assert p3._callbacks == {} assert p3._models == {} @@ -269,11 +265,9 @@ def test_layout_pop(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] layout.pop(0) assert model.children == [div2] - assert p1._callbacks == {} assert p1._models == {} @@ -286,11 +280,9 @@ def test_layout_remove(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] layout.remove(p1) assert model.children == [div2] - assert p1._callbacks == {} assert p1._models == {} @@ -303,11 +295,9 @@ def test_layout_clear(panel, document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.children[0] + assert p1._models[model.ref['id']][0] is model.children[0] layout.clear() assert model.children == [] - assert p1._callbacks == p2._callbacks == {} assert p1._models == p2._models == {} @@ -324,6 +314,7 @@ def test_tabs_basic_constructor(document, comm): assert 'plain' in tab1.child.text assert 'text' in tab2.child.text + def test_tabs_constructor(document, comm): div1 = Div() div2 = Div() @@ -636,15 +627,13 @@ def test_tabs_setitem(document, comm): model = tabs._get_root(document, comm=comm) tab1, tab2 = model.tabs - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is tab1.child + assert p1._models[model.ref['id']][0] is tab1.child div3 = Div() tabs[0] = ('C', div3) tab1, tab2 = model.tabs assert tab1.child is div3 assert tab1.title == 'C' assert tab2.child is div2 - assert p1._callbacks == {} assert p1._models == {} @@ -667,8 +656,7 @@ def test_tabs_setitem_replace_all(document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.tabs[0].child + assert p1._models[model.ref['id']][0] is model.tabs[0].child div3 = Div() div4 = Div() layout[:] = [('B', div3), ('C', div4)] @@ -677,9 +665,7 @@ def test_tabs_setitem_replace_all(document, comm): assert tab1.title == 'B' assert tab2.child is div4 assert tab2.title == 'C' - assert p1._callbacks == {} assert p1._models == {} - assert p2._callbacks == {} assert p2._models == {} @@ -703,8 +689,7 @@ def test_tabs_setitem_replace_slice(document, comm): model = layout._get_root(document, comm=comm) - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is model.tabs[0].child + assert p1._models[model.ref['id']][0] is model.tabs[0].child div3 = Div() div4 = Div() layout[1:] = [('D', div3), ('E', div4)] @@ -715,9 +700,7 @@ def test_tabs_setitem_replace_slice(document, comm): assert tab2.title == 'D' assert tab3.child is div4 assert tab3.title == 'E' - assert p2._callbacks == {} assert p2._models == {} - assert p3._callbacks == {} assert p3._models == {} @@ -754,13 +737,11 @@ def test_tabs_pop(document, comm): model = tabs._get_root(document, comm=comm) tab1 = model.tabs[0] - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is tab1.child + assert p1._models[model.ref['id']][0] is tab1.child tabs.pop(0) assert len(model.tabs) == 1 tab1 = model.tabs[0] assert tab1.child is div2 - assert p1._callbacks == {} assert p1._models == {} @@ -773,13 +754,11 @@ def test_tabs_remove(document, comm): model = tabs._get_root(document, comm=comm) tab1 = model.tabs[0] - assert model.ref['id'] in p1._callbacks - assert p1._models[model.ref['id']] is tab1.child + assert p1._models[model.ref['id']][0] is tab1.child tabs.remove(p1) assert len(model.tabs) == 1 tab1 = model.tabs[0] assert tab1.child is div2 - assert p1._callbacks == {} assert p1._models == {} @@ -794,7 +773,6 @@ def test_tabs_clear(document, comm): tabs.clear() assert tabs._names == [] assert len(model.tabs) == 0 - assert p1._callbacks == p2._callbacks == {} assert p1._models == p2._models == {} diff --git a/panel/tests/test_layoutable.py b/panel/tests/test_layoutable.py new file mode 100644 index 0000000000..ecfd5eb40a --- /dev/null +++ b/panel/tests/test_layoutable.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +import pytest + +from panel.layout import Column, Row, Tabs, Spacer +from panel.pane import (HTML, Str, PNG, JPG, GIF, SVG, Markdown, LaTeX, + Matplotlib, RGGPlot, YT, Plotly, Vega) + +from .test_widgets import all_widgets + + +def check_layoutable_properties(layoutable, model): + layoutable.background = '#ffffff' + assert model.background == '#ffffff' + + layoutable.css_classes = ['custom_class'] + assert model.css_classes == ['custom_class'] + + layoutable.width = 500 + assert model.width == 500 + + layoutable.height = 450 + assert model.height == 450 + + layoutable.min_height = 300 + assert model.min_height == 300 + + layoutable.min_width = 250 + assert model.min_width == 250 + + layoutable.max_height = 600 + assert model.max_height == 600 + + layoutable.max_width = 550 + assert model.max_width == 550 + + layoutable.margin = 10 + assert model.margin == (10, 10, 10, 10) + + layoutable.sizing_mode = 'stretch_width' + assert model.sizing_mode == 'stretch_width' + + layoutable.width_policy = 'max' + assert model.width_policy == 'max' + + layoutable.height_policy = 'min' + assert model.height_policy == 'min' + + +@pytest.mark.parametrize('pane', [Str, Markdown, HTML, PNG, JPG, SVG, + GIF, LaTeX, Matplotlib, RGGPlot, YT, + Plotly, Vega]) +def test_pane_layout_properties(pane, document, comm): + p = pane() + model = p._get_root(document, comm) + check_layoutable_properties(p, model) + + +@pytest.mark.parametrize('widget', all_widgets) +def test_widget_layout_properties(widget, document, comm): + w = widget() + model = w._get_root(document, comm) + check_layoutable_properties(w, model) + + +@pytest.mark.parametrize('layout', [Column, Row, Tabs, Spacer]) +def test_layout_properties(layout, document, comm): + l = layout() + model = l._get_root(document, comm) + check_layoutable_properties(l, model) diff --git a/panel/tests/test_links.py b/panel/tests/test_links.py index f835cd1d0a..a360cf9344 100644 --- a/panel/tests/test_links.py +++ b/panel/tests/test_links.py @@ -28,13 +28,12 @@ def test_pnwidget_hvplot_links(document, comm): assert len(hv_views) == 1 assert len(widg_views) == 1 - slider = widg_views[0]._models[model.ref['id']] - scatter = hv_views[0]._plots[model.ref['id']].handles['glyph'] + slider = widg_views[0]._models[model.ref['id']][0] + scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph'] link_customjs = slider.js_property_callbacks['change:value'][-1] assert link_customjs.args['source'] is slider assert link_customjs.args['target'] is scatter - code = ("value = source['value'];" "try { property = target.properties['size'];" @@ -58,7 +57,7 @@ def test_bkwidget_hvplot_links(document, comm): assert len(hv_views) == 1 slider = bokeh_widget - scatter = hv_views[0]._plots[model.ref['id']].handles['glyph'] + scatter = hv_views[0]._plots[model.ref['id']][0].handles['glyph'] link_customjs = slider.js_property_callbacks['change:value'][-1] assert link_customjs.args['source'] is slider @@ -113,8 +112,8 @@ def test_link_with_customcode(document, comm): assert len(hv_views) == 1 assert len(widg_views) == 1 - range_slider = widg_views[0]._models[model.ref['id']] - x_range = hv_views[0]._plots[model.ref['id']].handles['x_range'] + range_slider = widg_views[0]._models[model.ref['id']][0] + x_range = hv_views[0]._plots[model.ref['id']][0].handles['x_range'] link_customjs = range_slider.js_property_callbacks['change:value'][-1] assert link_customjs.args['source'] is range_slider diff --git a/panel/tests/test_panes.py b/panel/tests/test_panes.py index 96addfc68f..2820132f35 100644 --- a/panel/tests/test_panes.py +++ b/panel/tests/test_panes.py @@ -33,7 +33,6 @@ markdown_available = pytest.mark.skipif(markdown is None, reason="requires markdown") from .fixtures import mpl_figure -from .test_layout import get_div def test_get_bokeh_pane_type(): @@ -50,21 +49,18 @@ def test_bokeh_pane(document, comm): assert isinstance(row, BkRow) assert len(row.children) == 1 model = row.children[0] - assert row.ref['id'] in pane._callbacks - assert get_div(model) is div - assert pane._models[row.ref['id']] is model + assert model is div + assert pane._models[row.ref['id']][0] is model # Replace Pane.object div2 = Div() pane.object = div2 new_model = row.children[0] - assert get_div(new_model) is div2 - assert row.ref['id'] in pane._callbacks - assert pane._models[row.ref['id']] is new_model + assert new_model is div2 + assert pane._models[row.ref['id']][0] is new_model # Cleanup pane._cleanup(row) - assert pane._callbacks == {} assert pane._models == {} @@ -84,20 +80,17 @@ def test_matplotlib_pane(document, comm): # Create pane model = pane._get_root(document, comm=comm) - assert model.ref['id'] in pane._callbacks assert 'Markdown
" # Cleanup pane._cleanup(model) - assert pane._callbacks == {} assert pane._models == {} @@ -133,19 +123,16 @@ def test_html_pane(document, comm): # Create pane model = pane._get_root(document, comm=comm) - assert model.ref['id'] in pane._callbacks - assert pane._models[model.ref['id']] is model + assert pane._models[model.ref['id']][0] is model assert model.text == "<h2>Test</h2>" # Cleanup pane._cleanup(model) - assert pane._callbacks == {} assert pane._models == {} @@ -198,8 +181,7 @@ def test_svg_pane(document, comm): # Create pane model = pane._get_root(document, comm=comm) - assert model.ref['id'] in pane._callbacks - assert pane._models[model.ref['id']] is model + assert pane._models[model.ref['id']][0] is model assert model.text.startswith('