1
"""
2
Defines the PaneBase class defining the API for panes which convert
3
objects to a visual representation expressed as a bokeh model.
4
"""
5 6
from __future__ import absolute_import, division, unicode_literals
6

7 6
from functools import partial
8

9 6
import param
10

11 6
from bokeh.models.layouts import GridBox as _BkGridBox
12

13 6
from ..io import init_doc, push, state, unlocked
14 6
from ..layout import Panel, Row
15 6
from ..links import Link
16 6
from ..reactive import Reactive
17 6
from ..viewable import Layoutable, Viewable
18 6
from ..util import param_reprs
19

20

21 6
def Pane(obj, **kwargs):
22
    """
23
    Converts any object to a Pane if a matching Pane class exists.
24
    """
25 6
    if isinstance(obj, Viewable):
26 6
        return obj
27 6
    return PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
28

29

30 6
def panel(obj, **kwargs):
31
    """
32
    Creates a panel from any supplied object by wrapping it in a pane
33
    and returning a corresponding Panel.
34

35
    Arguments
36
    ---------
37
    obj: object
38
       Any object to be turned into a Panel
39
    **kwargs: dict
40
       Any keyword arguments to be passed to the applicable Pane
41

42
    Returns
43
    -------
44
    layout: Viewable
45
       A Viewable representation of the input object
46
    """
47 6
    if isinstance(obj, Viewable):
48 6
        return obj
49 6
    if kwargs.get('name', False) is None:
50 6
        kwargs.pop('name')
51 6
    pane = PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
52 6
    if len(pane.layout) == 1 and pane._unpack:
53 6
        return pane.layout[0]
54 6
    return pane.layout
55

56

57 6
class RerenderError(RuntimeError):
58
    """
59
    Error raised when a pane requests re-rendering during initial render.
60
    """
61

62

63 6
class PaneBase(Reactive):
64
    """
65
    PaneBase is the abstract baseclass for all atomic displayable units
66
    in the panel library. Pane defines an extensible interface for
67
    wrapping arbitrary objects and transforming them into Bokeh models.
68

69
    Panes are reactive in the sense that when the object they are
70
    wrapping is changed any dashboard containing the pane will update
71
    in response.
72

73
    To define a concrete Pane type subclass this class and implement
74
    the applies classmethod and the _get_model private method.
75
    """
76

77 6
    default_layout = param.ClassSelector(default=Row, class_=(Panel),
78
                                         is_instance=False, doc="""
79
        Defines the layout the model(s) returned by the pane will
80
        be placed in.""")
81

82 6
    object = param.Parameter(default=None, doc="""
83
        The object being wrapped, which will be converted to a
84
        Bokeh model.""")
85

86
    # When multiple Panes apply to an object, the one with the highest
87
    # numerical priority is selected. The default is an intermediate value.
88
    # If set to None, applies method will be called to get a priority
89
    # value for a specific object type.
90 6
    priority = 0.5
91

92
    # Whether applies requires full set of keywords
93 6
    _applies_kw = False
94

95
    # Whether the Pane layout can be safely unpacked
96 6
    _unpack = True
97

98
    # Declares whether Pane supports updates to the Bokeh model
99 6
    _updates = False
100

101
    # List of parameters that trigger a rerender of the Bokeh model
102 6
    _rerender_params = ['object']
103

104 6
    __abstract = True
105

106 6
    def __init__(self, object=None, **params):
107 6
        applies = self.applies(object, **(params if self._applies_kw else {}))
108 6
        if (isinstance(applies, bool) and not applies) and object is not None :
109 0
            self._type_error(object)
110

111 6
        super(PaneBase, self).__init__(object=object, **params)
112 6
        kwargs = {k: v for k, v in params.items() if k in Layoutable.param}
113 6
        self.layout = self.default_layout(self, **kwargs)
114 6
        watcher = self.param.watch(self._update_pane, self._rerender_params)
115 6
        self._callbacks.append(watcher)
116

117 6
    def _type_error(self, object):
118 0
        raise ValueError("%s pane does not support objects of type '%s'." %
119
                         (type(self).__name__, type(object).__name__))
120

121 6
    def __repr__(self, depth=0):
122 6
        cls = type(self).__name__
123 6
        params = param_reprs(self, ['object'])
124 6
        obj = 'None' if self.object is None else type(self.object).__name__
125 6
        template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
126 6
        return template.format(cls=cls, params=', '.join(params), obj=obj)
127

128 6
    def __getitem__(self, index):
129
        """
130
        Allows pane objects to behave like the underlying layout
131
        """
132 6
        return self.layout[index]
133

134
    #----------------------------------------------------------------
135
    # Callback API
136
    #----------------------------------------------------------------
137

138 6
    @property
139 1
    def _linkable_params(self):
140 0
        return [p for p in self._synced_params() if self._rename.get(p, False) is not None]
141

142 6
    def _synced_params(self):
143 6
        ignored_params = ['name', 'default_layout']+self._rerender_params
144 6
        return [p for p in self.param if p not in ignored_params]
145

146 6
    def _init_properties(self):
147 6
        return {k: v for k, v in self.param.get_param_values()
148
                if v is not None and k not in ['default_layout', 'object']}
149

150 6
    def _update_object(self, ref, doc, root, parent, comm):
151 6
        old_model = self._models[ref][0]
152 6
        if self._updates:
153 6
            self._update(ref, old_model)
154
        else:
155 6
            new_model = self._get_model(doc, root, parent, comm)
156 6
            try:
157 6
                if isinstance(parent, _BkGridBox):
158 0
                    indexes = [i for i, child in enumerate(parent.children)
159
                               if child[0] is old_model]
160 0
                    if indexes:
161 0
                        index = indexes[0]
162
                    else:
163 0
                        raise ValueError
164 0
                    new_model = (new_model,) + parent.children[index][1:]
165
                else:
166 6
                    index = parent.children.index(old_model)
167 0
            except ValueError:
168 0
                self.warning('%s pane model %s could not be replaced '
169
                             'with new model %s, ensure that the '
170
                             'parent is not modified at the same '
171
                             'time the panel is being updated.' %
172
                             (type(self).__name__, old_model, new_model))
173
            else:
174 6
                parent.children[index] = new_model
175

176 6
        from ..io import state
177 6
        ref = root.ref['id']
178 6
        if ref in state._views:
179 6
            state._views[ref][0]._preprocess(root)
180

181 6
    def _update_pane(self, *events):
182 6
        for ref, (_, parent) in self._models.items():
183 6
            if ref not in state._views or ref in state._fake_roots:
184 0
                continue
185 6
            viewable, root, doc, comm = state._views[ref]
186 6
            if comm or state._unblocked(doc):
187 6
                with unlocked():
188 6
                    self._update_object(ref, doc, root, parent, comm)
189 6
                if comm and 'embedded' not in root.tags:
190 6
                    push(doc, comm)
191
            else:
192 6
                cb = partial(self._update_object, ref, doc, root, parent, comm)
193 6
                if doc.session_context:
194 6
                    doc.add_next_tick_callback(cb)
195
                else:
196 0
                    cb()
197

198 6
    def _update(self, ref=None, model=None):
199
        """
200
        If _updates=True this method is used to update an existing
201
        Bokeh model instead of replacing the model entirely. The
202
        supplied model should be updated with the current state.
203
        """
204 0
        raise NotImplementedError
205

206
    #----------------------------------------------------------------
207
    # Public API
208
    #----------------------------------------------------------------
209

210 6
    @classmethod
211 1
    def applies(cls, obj):
212
        """
213
        Given the object return a boolean indicating whether the Pane
214
        can render the object. If the priority of the pane is set to
215
        None, this method may also be used to define a priority
216
        depending on the object being rendered.
217
        """
218 0
        return None
219

220 6
    def clone(self, object=None, **params):
221
        """
222
        Makes a copy of the Pane sharing the same parameters.
223

224
        Arguments
225
        ---------
226
        params: Keyword arguments override the parameters on the clone.
227

228
        Returns
229
        -------
230
        Cloned Pane object
231
        """
232 6
        params = dict(self.param.get_param_values(), **params)
233 6
        old_object = params.pop('object')
234 6
        if object is None:
235 6
            object = old_object
236 6
        return type(self)(object, **params)
237

238 6
    def get_root(self, doc=None, comm=None, preprocess=True):
239
        """
240
        Returns the root model and applies pre-processing hooks
241

242
        Arguments
243
        ---------
244
        doc: bokeh.Document
245
          Bokeh document the bokeh model will be attached to.
246
        comm: pyviz_comms.Comm
247
          Optional pyviz_comms when working in notebook
248
        preprocess: boolean (default=True)
249
          Whether to run preprocessing hooks
250

251
        Returns
252
        -------
253
        Returns the bokeh model corresponding to this panel object
254
        """
255 6
        doc = init_doc(doc)
256 6
        if self._updates:
257 6
            root = self._get_model(doc, comm=comm)
258
        else:
259 6
            root = self.layout._get_model(doc, comm=comm)
260 6
        if preprocess:
261 6
            self._preprocess(root)
262 6
        ref = root.ref['id']
263 6
        state._views[ref] = (self, root, doc, comm)
264 6
        return root
265

266 6
    @classmethod
267 1
    def get_pane_type(cls, obj, **kwargs):
268
        """
269
        Returns the applicable Pane type given an object by resolving
270
        the precedence of all types whose applies method declares that
271
        the object is supported.
272

273
        Arguments
274
        ---------
275
        obj (object): The object type to return a Pane for
276

277
        Returns
278
        -------
279
        The applicable Pane type with the highest precedence.
280
        """
281 6
        if isinstance(obj, Viewable):
282 6
            return type(obj)
283 6
        descendents = []
284 6
        for p in param.concrete_descendents(PaneBase).values():
285 6
            if p.priority is None:
286 6
                applies = True
287 6
                try:
288 6
                    priority = p.applies(obj, **(kwargs if p._applies_kw else {}))
289 0
                except Exception:
290 0
                    priority = False
291
            else:
292 6
                applies = None
293 6
                priority = p.priority
294 6
            if isinstance(priority, bool) and priority:
295 0
                raise ValueError('If a Pane declares no priority '
296
                                 'the applies method should return a '
297
                                 'priority value specific to the '
298
                                 'object type or False, but the %s pane '
299
                                 'declares no priority.' % p.__name__)
300 6
            elif priority is None or priority is False:
301 6
                continue
302 6
            descendents.append((priority, applies, p))
303 6
        pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
304 6
        for _, applies, pane_type in pane_types:
305 6
            if applies is None:
306 6
                try:
307 6
                    applies = pane_type.applies(obj, **(kwargs if pane_type._applies_kw else {}))
308 0
                except Exception:
309 0
                    applies = False
310 6
            if not applies:
311 6
                continue
312 6
            return pane_type
313 0
        raise TypeError('%s type could not be rendered.' % type(obj).__name__)
314

315

316

317 6
class ReplacementPane(PaneBase):
318
    """
319
    A Pane type which allows for complete replacement of the underlying
320
    bokeh model by creating an internal layout to replace the children
321
    on.
322
    """
323

324 6
    _updates = True
325

326 6
    __abstract = True
327

328 6
    def __init__(self, object=None, **params):
329 6
        self._kwargs =  {p: params.pop(p) for p in list(params)
330
                         if p not in self.param}
331 6
        super(ReplacementPane, self).__init__(object, **params)
332 6
        self._pane = Pane(None)
333 6
        self._internal = True
334 6
        self._inner_layout = Row(self._pane, **{k: v for k, v in params.items() if k in Row.param})
335 6
        self.param.watch(self._update_inner_layout, list(Layoutable.param))
336

337 6
    def _update_inner_layout(self, *events):
338 6
        for event in events:
339 6
            setattr(self._pane, event.name, event.new)
340 6
            if event.name in ['sizing_mode', 'width_policy', 'height_policy']:
341 6
                setattr(self._inner_layout, event.name, event.new)
342

343 6
    def _update_pane(self, *events):
344
        """
345
        Updating of the object should be handled manually.
346
        """
347

348 6
    @classmethod
349 1
    def _update_from_object(cls, object, old_object, was_internal, **kwargs):
350 6
        pane_type = cls.get_pane_type(object)
351 6
        try:
352 6
            links = Link.registry.get(object)
353 6
        except TypeError:
354 6
            links = []
355 6
        custom_watchers = False
356 6
        if isinstance(object, Reactive):
357 6
            watchers = [
358
                w for pwatchers in object._param_watchers.values()
359
                for awatchers in pwatchers.values() for w in awatchers
360
            ]
361 6
            custom_watchers = [wfn for wfn in watchers if wfn not in object._callbacks]
362

363 6
        pane, internal = None, was_internal
364 6
        if type(old_object) is pane_type and not links and not custom_watchers and was_internal:
365
            # If the object has not external referrers we can update
366
            # it inplace instead of replacing it
367 6
            if isinstance(object, Reactive):
368 6
                pvals = dict(old_object.param.get_param_values())
369 6
                new_params = {k: v for k, v in object.param.get_param_values()
370
                              if k != 'name' and v is not pvals[k]}
371 6
                old_object.param.set_param(**new_params)
372
            else:
373 6
                old_object.object = object
374
        else:
375
            # Replace pane entirely
376 6
            pane = panel(object, **{k: v for k, v in kwargs.items()
377
                                    if k in pane_type.param})
378 6
            if pane is object:
379
                # If all watchers on the object are internal watchers
380
                # we can make a clone of the object and update this
381
                # clone going forward, otherwise we have replace the
382
                # model entirely which is more expensive.
383 6
                if not (custom_watchers or links):
384 6
                    pane = object.clone()
385 6
                    internal = True
386
                else:
387 6
                    internal = False
388
            else:
389 6
                internal = object is not old_object
390 6
        return pane, internal
391

392 6
    def _update_inner(self, new_object):
393 6
        kwargs = dict(self.param.get_param_values(), **self._kwargs)
394 6
        del kwargs['object']
395 6
        new_pane, internal = self._update_from_object(
396
            new_object, self._pane, self._internal, **kwargs
397
        )
398 6
        if new_pane is None:
399 6
            return
400

401 6
        self._pane = new_pane
402 6
        self._inner_layout[0] = self._pane
403 6
        self._internal = internal
404

405 6
    def _get_model(self, doc, root=None, parent=None, comm=None):
406 6
        if root:
407 6
            ref = root.ref['id']
408 6
            if ref in self._models:
409 0
                self._cleanup(root)
410 6
        model = self._inner_layout._get_model(doc, root, parent, comm)
411 6
        if root is None:
412 6
            ref = model.ref['id']
413 6
        self._models[ref] = (model, parent)
414 6
        return model
415

416 6
    def _cleanup(self, root=None):
417 6
        self._inner_layout._cleanup(root)
418 6
        super(ReplacementPane, self)._cleanup(root)
419

420 6
    def select(self, selector=None):
421
        """
422
        Iterates over the Viewable and any potential children in the
423
        applying the Selector.
424

425
        Arguments
426
        ---------
427
        selector: type or callable or None
428
          The selector allows selecting a subset of Viewables by
429
          declaring a type or callable function to filter by.
430

431
        Returns
432
        -------
433
        viewables: list(Viewable)
434
        """
435 6
        selected = super(ReplacementPane, self).select(selector)
436 6
        selected += self._pane.select(selector)
437 6
        return selected

Read our documentation on viewing source code .

Loading