holoviz / panel
1
"""
2
Defines the Param pane which converts Parameterized classes into a
3
set of widgets.
4
"""
5 7
import os
6 7
import sys
7 7
import json
8 7
import types
9 7
import inspect
10 7
import itertools
11

12 7
from collections import OrderedDict, defaultdict, namedtuple
13 7
from contextlib import contextmanager
14 7
from six import string_types
15

16 7
import param
17

18 7
from param.parameterized import classlist, discard_events
19

20 7
from .io import init_doc, state
21 7
from .layout import Column, Panel, Row, Spacer, Tabs
22 7
from .pane.base import PaneBase, ReplacementPane
23 7
from .util import (
24
    abbreviated_repr, full_groupby, get_method_owner, is_parameterized,
25
    param_name, recursive_parameterized
26
)
27 7
from .reactive import Reactive
28 7
from .viewable import Layoutable, Viewable
29 7
from .widgets import (
30
    Button, Checkbox, ColorPicker, DataFrame, DatePicker,
31
    DatetimeInput, DateRangeSlider, DiscreteSlider, FileSelector,
32
    FloatSlider, IntInput, IntSlider, LiteralInput, MultiSelect,
33
    RangeSlider, Select, FloatInput, StaticText, TextInput, Toggle,
34
    Widget
35
)
36 7
from .widgets.button import _ButtonBase
37

38

39 7
def SingleFileSelector(pobj):
40
    """
41
    Determines whether to use a TextInput or Select widget for FileSelector
42
    """
43 0
    if pobj.path:
44 0
        return Select
45
    else:
46 0
        return TextInput
47

48

49 7
def LiteralInputTyped(pobj):
50 7
    if isinstance(pobj, param.Tuple):
51 0
        return type(str('TupleInput'), (LiteralInput,), {'type': tuple})
52 7
    elif isinstance(pobj, param.Number):
53 0
        return type(str('NumberInput'), (LiteralInput,), {'type': (int, float)})
54 7
    elif isinstance(pobj, param.Dict):
55 7
        return type(str('DictInput'), (LiteralInput,), {'type': dict})
56 7
    elif isinstance(pobj, param.List):
57 7
        return type(str('ListInput'), (LiteralInput,), {'type': list})
58 7
    return LiteralInput
59

60

61 7
@contextmanager
62 2
def set_values(*parameterizeds, **param_values):
63
    """
64
    Temporarily sets parameter values to the specified values on all
65
    supplied Parameterized objects.
66

67
    Arguments
68
    ---------
69
    parameterizeds: tuple(param.Parameterized)
70
        Any number of parameterized objects.
71
    param_values: dict
72
        A dictionary of parameter names and temporary values.
73
    """
74 0
    old = []
75 0
    for parameterized in parameterizeds:
76 0
        old_values = {p: getattr(parameterized, p) for p in param_values}
77 0
        old.append((parameterized, old_values))
78 0
        parameterized.param.set_param(**param_values)
79 0
    try:
80 0
        yield
81
    finally:
82 0
        for parameterized, old_values in old:
83 0
            parameterized.param.set_param(**old_values)
84

85

86 7
class Param(PaneBase):
87
    """
88
    Param panes render a Parameterized class to a set of widgets which
89
    are linked to the parameter values on the class.
90
    """
91

92 7
    display_threshold = param.Number(default=0, precedence=-10, doc="""
93
        Parameters with precedence below this value are not displayed.""")
94

95 7
    default_layout = param.ClassSelector(default=Column, class_=Panel,
96
                                         is_instance=False)
97

98 7
    default_precedence = param.Number(default=1e-8, precedence=-10, doc="""
99
        Precedence value to use for parameters with no declared
100
        precedence.  By default, zero predecence is available for
101
        forcing some parameters to the top of the list, and other
102
        values above the default_precedence values can be used to sort
103
        or group parameters arbitrarily.""")
104

105 7
    expand = param.Boolean(default=False, doc="""
106
        Whether parameterized subobjects are expanded or collapsed on
107
        instantiation.""")
108

109 7
    expand_button = param.Boolean(default=None, doc="""
110
        Whether to add buttons to expand and collapse sub-objects.""")
111

112 7
    expand_layout = param.Parameter(default=Column, doc="""
113
        Layout to expand sub-objects into.""")
114

115 7
    height = param.Integer(default=None, bounds=(0, None), doc="""
116
        Height of widgetbox the parameter widgets are displayed in.""")
117

118 7
    initializer = param.Callable(default=None, doc="""
119
        User-supplied function that will be called on initialization,
120
        usually to update the default Parameter values of the
121
        underlying parameterized object.""")
122

123 7
    name = param.String(default='', doc="""
124
        Title of the pane.""")
125

126 7
    parameters = param.List(default=[], allow_None=True, doc="""
127
        If set this serves as a whitelist of parameters to display on
128
        the supplied Parameterized object.""")
129

130 7
    show_labels = param.Boolean(default=True, doc="""
131
        Whether to show labels for each widget""")
132

133 7
    show_name = param.Boolean(default=True, doc="""
134
        Whether to show the parameterized object's name""")
135

136 7
    width = param.Integer(default=300, allow_None=True, bounds=(0, None), doc="""
137
        Width of widgetbox the parameter widgets are displayed in.""")
138

139 7
    widgets = param.Dict(doc="""
140
        Dictionary of widget overrides, mapping from parameter name
141
        to widget class.""")
142

143 7
    priority = 0.1
144

145 7
    _unpack = True
146

147 7
    _mapping = {
148
        param.Action:            Button,
149
        param.Boolean:           Checkbox,
150
        param.CalendarDate:      DatePicker,
151
        param.Color:             ColorPicker,
152
        param.Date:              DatetimeInput,
153
        param.DateRange:         DateRangeSlider,
154
        param.CalendarDateRange: DateRangeSlider,
155
        param.DataFrame:         DataFrame,
156
        param.Dict:              LiteralInputTyped,
157
        param.FileSelector:      SingleFileSelector,
158
        param.Filename:          TextInput,
159
        param.Foldername:        TextInput,
160
        param.Integer:           IntSlider,
161
        param.List:              LiteralInputTyped,
162
        param.MultiFileSelector: FileSelector,
163
        param.ListSelector:      MultiSelect,
164
        param.Number:            FloatSlider,
165
        param.ObjectSelector:    Select,
166
        param.Parameter:         LiteralInputTyped,
167
        param.Range:             RangeSlider,
168
        param.Selector:          Select,
169
        param.String:            TextInput,
170
    }
171

172 7
    if hasattr(param, 'Event'):
173 7
        _mapping[param.Event] = Button
174

175 7
    _rerender_params = []
176

177 7
    def __init__(self, object=None, **params):
178 7
        if isinstance(object, param.Parameter):
179 7
            if not 'show_name' in params:
180 7
                params['show_name'] = False
181 7
            params['parameters'] = [object.name]
182 7
            object = object.owner
183 7
        if isinstance(object, param.parameterized.Parameters):
184 7
            object = object.cls if object.self is None else object.self
185

186 7
        if 'parameters' not in params and object is not None:
187 7
            params['parameters'] = [p for p in object.param if p != 'name']
188 7
            self._explicit_parameters = False
189
        else:
190 7
            self._explicit_parameters = object is not None
191

192 7
        if object and 'name' not in params:
193 7
            params['name'] = param_name(object.name)
194 7
        super().__init__(object, **params)
195 7
        self._updating = []
196

197
        # Construct Layout
198 7
        kwargs = {p: v for p, v in self.param.get_param_values()
199
                  if p in Layoutable.param and v is not None}
200 7
        self._widget_box = self.default_layout(**kwargs)
201

202 7
        layout = self.expand_layout
203 7
        if isinstance(layout, Panel):
204 7
            self._expand_layout = layout
205 7
            self.layout = self._widget_box
206 7
        elif isinstance(self._widget_box, layout):
207 7
            self.layout = self._expand_layout = self._widget_box
208 7
        elif isinstance(layout, type) and issubclass(layout, Panel):
209 7
            self.layout = self._expand_layout = layout(self._widget_box, **kwargs)
210
        else:
211 0
            raise ValueError('expand_layout expected to be a panel.layout.Panel'
212
                             'type or instance, found %s type.' %
213
                             type(layout).__name__)
214 7
        self.param.watch(self._update_widgets, [
215
            'object', 'parameters', 'name', 'display_threshold', 'expand_button',
216
            'expand', 'expand_layout', 'widgets', 'show_labels', 'show_name'])
217 7
        self._update_widgets()
218

219 7
    def __repr__(self, depth=0):
220 7
        cls = type(self).__name__
221 7
        obj_cls = type(self.object).__name__
222 7
        params = [] if self.object is None else list(self.object.param)
223 7
        parameters = [k for k in params if k != 'name']
224 7
        params = []
225 7
        for p, v in sorted(self.param.get_param_values()):
226 7
            if v is self.param[p].default: continue
227 7
            elif v is None: continue
228 7
            elif isinstance(v, string_types) and v == '': continue
229 7
            elif p == 'object' or (p == 'name' and (v.startswith(obj_cls) or v.startswith(cls))): continue
230 7
            elif p == 'parameters' and v == parameters: continue
231 7
            try:
232 7
                params.append('%s=%s' % (p, abbreviated_repr(v)))
233 0
            except RuntimeError:
234 0
                params.append('%s=%s' % (p, '...'))
235 7
        obj = 'None' if self.object is None else '%s' % type(self.object).__name__
236 7
        template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
237 7
        return template.format(cls=cls, params=', '.join(params), obj=obj)
238

239
    #----------------------------------------------------------------
240
    # Callback API
241
    #----------------------------------------------------------------
242

243 7
    @property
244 2
    def _synced_params(self):
245 7
        ignored_params = ['default_layout', 'loading']
246 7
        return [p for p in Layoutable.param if p not in ignored_params]
247

248 7
    def _update_widgets(self, *events):
249 7
        parameters = []
250 7
        for event in sorted(events, key=lambda x: x.name):
251 7
            if event.name == 'object':
252 7
                if isinstance(event.new, param.parameterized.Parameters):
253
                    # Setting object will trigger this method a second time
254 7
                    self.object = event.new.cls if event.new.self is None else event.new.self
255 7
                    return
256
                
257 7
                if self._explicit_parameters:
258 7
                    parameters = self.parameters
259 7
                elif event.new is None:
260 0
                    parameters = []
261
                else:
262 7
                    parameters = [p for p in event.new.param if p != 'name']
263 7
                    self.name = param_name(event.new.name)
264 7
            if event.name == 'parameters':
265 7
                if event.new is None:
266 0
                    self._explicit_parameters = False
267 0
                    if self.object is not None:
268 0
                        parameters = [p for p in self.object.param if p != 'name']
269
                else:
270 7
                    self._explicit_parameters = True
271 7
                    parameters = [] if event.new == [] else event.new
272

273 7
        if parameters != [] and parameters != self.parameters:
274
            # Setting parameters will trigger this method a second time
275 7
            self.parameters = parameters
276 7
            return
277

278 7
        for cb in list(self._callbacks):
279 7
            if cb.inst in self._widget_box.objects:
280 7
                cb.inst.param.unwatch(cb)
281 7
                self._callbacks.remove(cb)
282

283
        # Construct widgets
284 7
        if self.object is None:
285 7
            self._widgets = {}
286
        else:
287 7
            self._widgets = self._get_widgets()
288

289 7
        alias = {'_title': 'name'}
290 7
        widgets = [widget for p, widget in self._widgets.items()
291
                   if (self.object.param[alias.get(p, p)].precedence is None)
292
                   or (self.object.param[alias.get(p, p)].precedence >= self.display_threshold)]
293 7
        self._widget_box.objects = widgets
294 7
        if not (self.expand_button == False and not self.expand):
295 7
            self._link_subobjects()
296

297 7
    def _link_subobjects(self):
298 7
        for pname, widget in self._widgets.items():
299 7
            widgets = [widget] if isinstance(widget, Widget) else widget
300 7
            if not any(is_parameterized(getattr(w, 'value', None)) or
301
                       any(is_parameterized(o) for o in getattr(w, 'options', []))
302
                       for w in widgets):
303 7
                continue
304 7
            if (isinstance(widgets, Row) and isinstance(widgets[1], Toggle)):
305 7
                selector, toggle = (widgets[0], widgets[1])
306
            else:
307 7
                selector, toggle = (widget, None)
308

309 7
            def toggle_pane(change, parameter=pname):
310
                "Adds or removes subpanel from layout"
311 7
                parameterized = getattr(self.object, parameter)
312 7
                existing = [p for p in self._expand_layout.objects
313
                            if isinstance(p, Param) and
314
                            p.object in recursive_parameterized(parameterized)]
315 7
                if not change.new:
316 7
                    self._expand_layout[:] = [
317
                        e for e in self._expand_layout.objects
318
                        if e not in existing
319
                    ]
320 7
                elif change.new:
321 7
                    kwargs = {k: v for k, v in self.param.get_param_values()
322
                              if k not in ['name', 'object', 'parameters']}
323 7
                    pane = Param(parameterized, name=parameterized.name,
324
                                 **kwargs)
325 7
                    if isinstance(self._expand_layout, Tabs):
326 7
                        title = self.object.param[pname].label
327 7
                        pane = (title, pane)
328 7
                    self._expand_layout.append(pane)
329

330 7
            def update_pane(change, parameter=pname):
331
                "Adds or removes subpanel from layout"
332 7
                layout = self._expand_layout
333 7
                existing = [p for p in layout.objects if isinstance(p, Param)
334
                            and p.object is change.old]
335

336 7
                if toggle:
337 7
                    toggle.disabled = not is_parameterized(change.new)
338 7
                if not existing:
339 0
                    return
340 7
                elif is_parameterized(change.new):
341 7
                    parameterized = change.new
342 7
                    kwargs = {k: v for k, v in self.param.get_param_values()
343
                              if k not in ['name', 'object', 'parameters']}
344 7
                    pane = Param(parameterized, name=parameterized.name,
345
                                 **kwargs)
346 7
                    layout[layout.objects.index(existing[0])] = pane
347
                else:
348 0
                    layout.pop(existing[0])
349

350 7
            watchers = [selector.param.watch(update_pane, 'value')]
351 7
            if toggle:
352 7
                watchers.append(toggle.param.watch(toggle_pane, 'value'))
353 7
            self._callbacks += watchers
354

355 7
            if self.expand:
356 7
                if self.expand_button:
357 7
                    toggle.value = True
358
                else:
359 7
                    toggle_pane(namedtuple('Change', 'new')(True))
360

361 7
    def widget(self, p_name):
362
        """Get widget for param_name"""
363 7
        p_obj = self.object.param[p_name]
364 7
        kw_widget = {}
365

366 7
        widget_class_overridden = True
367 7
        if self.widgets is None or p_name not in self.widgets:
368 7
            widget_class_overridden = False
369 7
            widget_class = self.widget_type(p_obj)
370 7
        elif isinstance(self.widgets[p_name], dict):
371 7
            kw_widget = dict(self.widgets[p_name])
372 7
            if 'widget_type' in self.widgets[p_name]:
373 7
                widget_class = kw_widget.pop('widget_type')
374 7
            elif 'type' in self.widgets[p_name]:
375 7
                widget_class = kw_widget.pop('type')
376
            else:
377 7
                widget_class_overridden = False
378 7
                widget_class = self.widget_type(p_obj)
379
        else:
380 7
            widget_class = self.widgets[p_name]
381

382 7
        if not self.show_labels and not issubclass(widget_class, _ButtonBase):
383 7
            label = ''
384
        else:
385 7
            label = p_obj.label
386 7
        kw = dict(disabled=p_obj.constant, name=label)
387

388 7
        value = getattr(self.object, p_name)
389 7
        if value is not None:
390 7
            kw['value'] = value
391

392
        # Update kwargs
393 7
        kw.update(kw_widget)
394

395 7
        if hasattr(p_obj, 'get_range'):
396 7
            options = p_obj.get_range()
397 7
            if not options and value is not None:
398 0
                options = [value]
399 7
            kw['options'] = options
400 7
        if hasattr(p_obj, 'get_soft_bounds'):
401 7
            bounds = p_obj.get_soft_bounds()
402 7
            if bounds[0] is not None:
403 7
                kw['start'] = bounds[0]
404 7
            if bounds[1] is not None:
405 7
                kw['end'] = bounds[1]
406 7
            if ('start' not in kw or 'end' not in kw):
407
                # Do not change widget class if _mapping was overridden
408 7
                if not widget_class_overridden:
409 7
                    if (isinstance(p_obj, param.Number) and
410
                        not isinstance(p_obj, (param.Date, param.CalendarDate))):
411 7
                        widget_class = FloatInput
412 7
                        if isinstance(p_obj, param.Integer):
413 7
                            widget_class = IntInput
414 7
                    elif not issubclass(widget_class, LiteralInput):
415 7
                        widget_class = LiteralInput
416 7
            if hasattr(widget_class, 'step') and getattr(p_obj, 'step', None):
417 7
                kw['step'] = p_obj.step
418

419 7
        kwargs = {k: v for k, v in kw.items() if k in widget_class.param}
420

421 7
        if isinstance(widget_class, type) and issubclass(widget_class, Button):
422 7
            kwargs.pop('value', None)
423

424 7
        if isinstance(widget_class, Widget):
425 7
            widget = widget_class
426
        else:
427 7
            widget = widget_class(**kwargs)
428 7
        widget._param_pane = self
429 7
        widget._param_name = p_name
430

431 7
        watchers = self._callbacks
432

433 7
        def link_widget(change):
434 7
            if p_name in self._updating:
435 7
                return
436 7
            try:
437 7
                self._updating.append(p_name)
438 7
                self.object.param.set_param(**{p_name: change.new})
439
            finally:
440 7
                self._updating.remove(p_name)
441

442 7
        if hasattr(param, 'Event') and isinstance(p_obj, param.Event):
443 7
            def event(change):
444 0
                self.object.param.trigger(p_name)
445 7
            watcher = widget.param.watch(event, 'clicks')
446 7
        elif isinstance(p_obj, param.Action):
447 7
            def action(change):
448 7
                value(self.object)
449 7
            watcher = widget.param.watch(action, 'clicks')
450 7
        elif kw_widget.get('throttled', False) and hasattr(widget, 'value_throttled'):
451 7
            watcher = widget.param.watch(link_widget, 'value_throttled')
452
        else:
453 7
            watcher = widget.param.watch(link_widget, 'value')
454 7
        watchers.append(watcher)
455

456 7
        def link(change, watchers=[watcher]):
457 7
            updates = {}
458 7
            widget = self._widgets[p_name]
459 7
            if change.what == 'constant':
460 7
                updates['disabled'] = change.new
461 7
            elif change.what == 'precedence':
462 7
                if change.new is change.old:
463 0
                    return
464 7
                elif change.new is None:
465 7
                    self._rerender()
466 7
                elif (change.new < self.display_threshold and
467
                      widget in self._widget_box.objects):
468 7
                    self._widget_box.pop(widget)
469 7
                elif change.new >= self.display_threshold:
470 7
                    self._rerender()
471 7
                return
472 7
            elif change.what == 'objects':
473 7
                updates['options'] = p_obj.get_range()
474 7
            elif change.what == 'bounds':
475 7
                start, end = p_obj.get_soft_bounds()
476 7
                supports_bounds = hasattr(widget, 'start')
477 7
                if start is None or end is None:
478 7
                    rerender = supports_bounds
479
                else:
480 7
                    rerender = not supports_bounds
481 7
                if supports_bounds:
482 7
                    updates['start'] = start
483 7
                    updates['end'] = end
484 7
                if rerender:
485 7
                    self._rerender_widget(p_name)
486 7
                    return
487 7
            elif change.what == 'step':
488 7
                updates['step'] = p_obj.step
489 7
            elif change.what == 'label':
490 7
                updates['name'] = p_obj.label
491 7
            elif p_name in self._updating:
492 7
                return
493 7
            elif hasattr(param, 'Event') and isinstance(p_obj, param.Event):
494 0
                return
495 7
            elif isinstance(p_obj, param.Action):
496 7
                prev_watcher = watchers[0]
497 7
                widget.param.unwatch(prev_watcher)
498 7
                def action(event):
499 0
                    change.new(self.object)
500 7
                watchers[0] = widget.param.watch(action, 'clicks')
501 7
                idx = self._callbacks.index(prev_watcher)
502 7
                self._callbacks[idx] = watchers[0]
503 7
                return
504 7
            elif kw_widget.get('throttled', False) and hasattr(widget, 'value_throttled'):
505 0
                updates['value_throttled'] = change.new
506 7
            elif isinstance(widget, Row) and len(widget) == 2:
507 0
                updates['value'] = change.new
508 0
                widget = widget[0]
509
            else:
510 7
                updates['value'] = change.new
511

512 7
            try:
513 7
                self._updating.append(p_name)
514 7
                if change.type == 'triggered':
515 7
                    with discard_events(widget):
516 7
                        widget.param.set_param(**updates)
517 7
                    widget.param.trigger(*updates)
518
                else:
519 7
                    widget.param.set_param(**updates)
520
            finally:
521 7
                self._updating.remove(p_name)
522

523
        # Set up links to parameterized object
524 7
        watchers.append(self.object.param.watch(link, p_name, 'constant'))
525 7
        watchers.append(self.object.param.watch(link, p_name, 'precedence'))
526 7
        watchers.append(self.object.param.watch(link, p_name, 'label'))
527 7
        if hasattr(p_obj, 'get_range'):
528 7
            watchers.append(self.object.param.watch(link, p_name, 'objects'))
529 7
        if hasattr(p_obj, 'get_soft_bounds'):
530 7
            watchers.append(self.object.param.watch(link, p_name, 'bounds'))
531 7
        if 'step' in kw:
532 7
            watchers.append(self.object.param.watch(link, p_name, 'step'))
533 7
        watchers.append(self.object.param.watch(link, p_name))
534

535 7
        options = kwargs.get('options', [])
536 7
        if isinstance(options, dict):
537 7
            options = options.values()
538 7
        if ((is_parameterized(value) or any(is_parameterized(o) for o in options))
539
            and (self.expand_button or (self.expand_button is None and not self.expand))):
540 7
            widget.margin = (5, 0, 5, 10)
541 7
            toggle = Toggle(name='\u22EE', button_type='primary',
542
                            disabled=not is_parameterized(value), max_height=30,
543
                            max_width=20, height_policy='fit', align='end',
544
                            margin=(0, 0, 5, 10))
545 7
            widget.width = self._widget_box.width-60
546 7
            return Row(widget, toggle, width_policy='max', margin=0)
547
        else:
548 7
            return widget
549

550 7
    @property
551 2
    def _ordered_params(self):
552 7
        params = [(p, pobj) for p, pobj in self.object.param.objects('existing').items()
553
                  if p in self.parameters or p == 'name']
554 7
        key_fn = lambda x: x[1].precedence if x[1].precedence is not None else self.default_precedence
555 7
        sorted_precedence = sorted(params, key=key_fn)
556 7
        filtered = [(k, p) for k, p in sorted_precedence]
557 7
        groups = itertools.groupby(filtered, key=key_fn)
558
        # Params preserve definition order in Python 3.6+
559 7
        dict_ordered_py3 = (sys.version_info.major == 3 and sys.version_info.minor >= 6)
560 7
        dict_ordered = dict_ordered_py3 or (sys.version_info.major > 3)
561 7
        ordered_groups = [list(grp) if dict_ordered else sorted(grp) for (_, grp) in groups]
562 7
        ordered_params = [el[0] for group in ordered_groups for el in group
563
                          if (el[0] != 'name' or el[0] in self.parameters)]
564 7
        return ordered_params
565

566
    #----------------------------------------------------------------
567
    # Model API
568
    #----------------------------------------------------------------
569

570 7
    def _rerender(self):
571 7
        precedence = lambda k: self.object.param['name' if k == '_title' else k].precedence
572 7
        params = self._ordered_params
573 7
        if self.show_name:
574 7
            params.insert(0, '_title')
575 7
        widgets = []
576 7
        for k in params:
577 7
            if precedence(k) is None or precedence(k) >= self.display_threshold:
578 7
                widgets.append(self._widgets[k])
579 7
        self._widget_box.objects = widgets
580

581 7
    def _rerender_widget(self, p_name):
582 7
        watchers = []
583 7
        for w in self._callbacks:
584 7
            if w.inst is self._widgets[p_name]:
585 7
                w.inst.param.unwatch(w)
586
            else:
587 7
                watchers.append(w)
588 7
        self._widgets[p_name] = self.widget(p_name)
589 7
        self._rerender()
590
    
591 7
    def _get_widgets(self):
592
        """Return name,widget boxes for all parameters (i.e., a property sheet)"""
593
        # Format name specially
594 7
        if self.expand_layout is Tabs:
595 7
            widgets = []
596 7
        elif self.show_name:
597 7
            widgets = [('_title', StaticText(value='<b>{0}</b>'.format(self.name)))]
598
        else:
599 7
            widgets = []
600 7
        widgets += [(pname, self.widget(pname)) for pname in self._ordered_params]
601 7
        return OrderedDict(widgets)
602

603 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
604 7
        model = self.layout._get_model(doc, root, parent, comm)
605 7
        self._models[root.ref['id']] = (model, parent)
606 7
        return model
607

608 7
    def _cleanup(self, root):
609 7
        self.layout._cleanup(root)
610 7
        super()._cleanup(root)
611

612
    #----------------------------------------------------------------
613
    # Public API
614
    #----------------------------------------------------------------
615

616 7
    @classmethod
617 2
    def applies(cls, obj):
618 7
        return (is_parameterized(obj) or
619
                isinstance(obj, param.parameterized.Parameters) or
620
                (isinstance(obj, param.Parameter) and obj.owner is not None))
621

622 7
    @classmethod
623 2
    def widget_type(cls, pobj):
624 7
        ptype = type(pobj)
625 7
        for t in classlist(ptype)[::-1]:
626 7
            if t in cls._mapping:
627 7
                if isinstance(cls._mapping[t], types.FunctionType):
628 7
                    return cls._mapping[t](pobj)
629 7
                return cls._mapping[t]
630

631 7
    def select(self, selector=None):
632
        """
633
        Iterates over the Viewable and any potential children in the
634
        applying the Selector.
635

636
        Arguments
637
        ---------
638
        selector: type or callable or None
639
          The selector allows selecting a subset of Viewables by
640
          declaring a type or callable function to filter by.
641

642
        Returns
643
        -------
644
        viewables: list(Viewable)
645
        """
646 7
        return super().select(selector) + self.layout.select(selector)
647

648 7
    def get_root(self, doc=None, comm=None, preprocess=True):
649
        """
650
        Returns the root model and applies pre-processing hooks
651

652
        Arguments
653
        ---------
654
        doc: bokeh.Document
655
          Bokeh document the bokeh model will be attached to.
656
        comm: pyviz_comms.Comm
657
          Optional pyviz_comms when working in notebook
658
        preprocess: boolean (default=True)
659
          Whether to run preprocessing hooks
660

661
        Returns
662
        -------
663
        Returns the bokeh model corresponding to this panel object
664
        """
665 7
        doc = init_doc(doc)
666 7
        root = self.layout.get_root(doc, comm, preprocess)
667 7
        ref = root.ref['id']
668 7
        self._models[ref] = (root, None)
669 7
        state._views[ref] = (self, root, doc, comm)
670 7
        return root
671

672

673 7
class ParamMethod(ReplacementPane):
674
    """
675
    ParamMethod panes wrap methods on parameterized classes and
676
    rerenders the plot when any of the method's parameters change. By
677
    default ParamMethod will watch all parameters on the class owning
678
    the method or can be restricted to certain parameters by annotating
679
    the method using the param.depends decorator. The method may
680
    return any object which itself can be rendered as a Pane.
681
    """
682

683 7
    lazy = param.Boolean(default=False, doc="""
684
        Whether to lazily evaluate the contents of the object
685
        only when it is required for rendering.""")
686

687 7
    loading_indicator = param.Boolean(default=False, doc="""
688
        Whether to show loading indicator while pane is updating.""")
689

690 7
    def __init__(self, object=None, **params):
691 7
        super().__init__(object, **params)
692 7
        self._evaled = not self.lazy
693 7
        self._link_object_params()
694 7
        if object is not None:
695 7
            self._validate_object()
696 7
            self._replace_pane(not self.lazy)
697

698 7
    @param.depends('object', watch=True)
699 2
    def _validate_object(self):
700 7
        dependencies = getattr(self.object, '_dinfo', None)
701 7
        if not dependencies or not dependencies.get('watch'):
702 7
            return
703 0
        fn_type = 'method' if type(self) is ParamMethod else 'function'
704 0
        self.param.warning(f"The {fn_type} supplied for Panel to display "
705
                           "was declared with `watch=True`, which will "
706
                           f"cause the {fn_type} to be called twice for "
707
                           "any change in a dependent Parameter. "
708
                           "`watch` should be False when Panel is "
709
                           "responsible for displaying the result "
710
                           f"of the {fn_type} call, while `watch=True` "
711
                           f"should be reserved for {fn_type}s that work "
712
                           "via side-effects, e.g. by modifying internal  "
713
                           "state of a class or global state in an "
714
                           "application's namespace.")
715

716
    #----------------------------------------------------------------
717
    # Callback API
718
    #----------------------------------------------------------------
719

720 7
    @classmethod
721 2
    def eval(self, function):
722 7
        args, kwargs = (), {}
723 7
        if hasattr(function, '_dinfo'):
724 7
            arg_deps = function._dinfo['dependencies']
725 7
            kw_deps = function._dinfo.get('kw', {})
726 7
            if kw_deps or any(isinstance(d, param.Parameter) for d in arg_deps):
727 7
                args = (getattr(dep.owner, dep.name) for dep in arg_deps)
728 7
                kwargs = {n: getattr(dep.owner, dep.name) for n, dep in kw_deps.items()}
729 7
        return function(*args, **kwargs)
730

731 7
    def _replace_pane(self, *args, force=False):
732 7
        self._evaled = bool(self._models) or force or not self.lazy
733 7
        if self._evaled:
734 7
            self._inner_layout.loading = self.loading_indicator
735 7
            try:
736 7
                if self.object is None:
737 0
                    new_object = Spacer()
738
                else:
739 7
                    new_object = self.eval(self.object)
740 7
                self._update_inner(new_object)
741
            finally:
742 7
                self._inner_layout.loading = False
743

744 7
    def _update_pane(self, *events):
745 0
        callbacks = []
746 0
        for watcher in self._callbacks:
747 0
            obj = watcher.inst if watcher.inst is None else watcher.cls
748 0
            if obj is self:
749 0
                callbacks.append(watcher)
750 0
                continue
751 0
            obj.param.unwatch(watcher)
752 0
        self._callbacks = callbacks
753 0
        self._link_object_params()
754 0
        self._replace_pane()
755

756 7
    def _link_object_params(self):
757 7
        parameterized = get_method_owner(self.object)
758 7
        params = parameterized.param.params_depended_on(self.object.__name__)
759 7
        deps = params
760

761 7
        def update_pane(*events):
762
            # Update nested dependencies if parameterized object events
763 7
            if any(is_parameterized(event.new) for event in events):
764 7
                new_deps = parameterized.param.params_depended_on(self.object.__name__)
765 7
                for p in list(deps):
766 7
                    if p in new_deps: continue
767 7
                    watchers = self._callbacks
768 7
                    for w in list(watchers):
769 7
                        if (w.inst is p.inst and w.cls is p.cls and
770
                            p.name in w.parameter_names):
771 7
                            obj = p.cls if p.inst is None else p.inst
772 7
                            obj.param.unwatch(w)
773 7
                            watchers.pop(watchers.index(w))
774 7
                    deps.pop(deps.index(p))
775

776 7
                new_deps = [dep for dep in new_deps if dep not in deps]
777 7
                for _, params in full_groupby(new_deps, lambda x: (x.inst or x.cls, x.what)):
778 7
                    p = params[0]
779 7
                    pobj = p.cls if p.inst is None else p.inst
780 7
                    ps = [_p.name for _p in params]
781 7
                    watcher = pobj.param.watch(update_pane, ps, p.what)
782 7
                    self._callbacks.append(watcher)
783 7
                    for p in params:
784 7
                        deps.append(p)
785 7
            self._replace_pane()
786

787 7
        for _, params in full_groupby(params, lambda x: (x.inst or x.cls, x.what)):
788 7
            p = params[0]
789 7
            pobj = (p.inst or p.cls)
790 7
            ps = [_p.name for _p in params]
791 7
            if isinstance(pobj, Reactive) and self.loading_indicator:
792 0
                props = {p: 'loading' for p in ps if p in pobj._linkable_params}
793 0
                if props:
794 0
                    pobj.jslink(self._inner_layout, **props)
795 7
            watcher = pobj.param.watch(update_pane, ps, p.what)
796 7
            self._callbacks.append(watcher)
797

798 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
799 7
        if not self._evaled:
800 0
            self._replace_pane(force=True)
801 7
        return super()._get_model(doc, root, parent, comm)
802

803
    #----------------------------------------------------------------
804
    # Public API
805
    #----------------------------------------------------------------
806

807 7
    @classmethod
808 2
    def applies(cls, obj):
809 7
        return inspect.ismethod(obj) and isinstance(get_method_owner(obj), param.Parameterized)
810

811

812

813 7
class ParamFunction(ParamMethod):
814
    """
815
    ParamFunction panes wrap functions decorated with the param.depends
816
    decorator and rerenders the output when any of the function's
817
    dependencies change. This allows building reactive components into
818
    a Panel which depend on other parameters, e.g. tying the value of
819
    a widget to some other output.
820
    """
821

822 7
    priority = 0.6
823

824 7
    def _link_object_params(self):
825 7
        deps = self.object._dinfo
826 7
        dep_params = list(deps['dependencies']) + list(deps.get('kw', {}).values())
827 7
        grouped = defaultdict(list)
828 7
        for dep in dep_params:
829 7
            grouped[id(dep.owner)].append(dep)
830 7
        for group in grouped.values():
831 7
            pobj = group[0].owner
832 7
            watcher = pobj.param.watch(self._replace_pane, [dep.name for dep in group])
833 7
            if isinstance(pobj, Reactive) and self.loading_indicator:
834 0
                props = {dep.name: 'loading' for dep in group
835
                         if dep.name in pobj._linkable_params}
836 0
                if props:
837 0
                    pobj.jslink(self._inner_layout, **props)
838 7
            self._callbacks.append(watcher)
839

840
    #----------------------------------------------------------------
841
    # Public API
842
    #----------------------------------------------------------------
843

844 7
    @classmethod
845 2
    def applies(cls, obj):
846 7
        return isinstance(obj, types.FunctionType) and hasattr(obj, '_dinfo')
847

848

849 7
class JSONInit(param.Parameterized):
850
    """
851
    Callable that can be passed to Widgets.initializer to set Parameter
852
    values using JSON. There are three approaches that may be used:
853
    1. If the json_file argument is specified, this takes precedence.
854
    2. The JSON file path can be specified via an environment variable.
855
    3. The JSON can be read directly from an environment variable.
856
    Here is an easy example of setting such an environment variable on
857
    the commandline:
858
    PARAM_JSON_INIT='{"p1":5}' jupyter notebook
859
    This addresses any JSONInit instances that are inspecting the
860
    default environment variable called PARAM_JSON_INIT, instructing it to set
861
    the 'p1' parameter to 5.
862
    """
863

864 7
    varname = param.String(default='PARAM_JSON_INIT', doc="""
865
        The name of the environment variable containing the JSON
866
        specification.""")
867

868 7
    target = param.String(default=None, doc="""
869
        Optional key in the JSON specification dictionary containing the
870
        desired parameter values.""")
871

872 7
    json_file = param.String(default=None, doc="""
873
        Optional path to a JSON file containing the parameter settings.""")
874

875 7
    def __call__(self, parameterized):
876 7
        warnobj = param.main if isinstance(parameterized, type) else parameterized
877 7
        param_class = (parameterized if isinstance(parameterized, type)
878
                       else parameterized.__class__)
879

880 7
        target = self.target if self.target is not None else param_class.__name__
881

882 7
        env_var = os.environ.get(self.varname, None)
883 7
        if env_var is None and self.json_file is None: return
884

885 7
        if self.json_file or env_var.endswith('.json'):
886 0
            try:
887 0
                fname = self.json_file if self.json_file else env_var
888 0
                spec = json.load(open(os.path.abspath(fname), 'r'))
889 0
            except Exception:
890 0
                warnobj.warning('Could not load JSON file %r' % spec)
891
        else:
892 7
            spec = json.loads(env_var)
893

894 7
        if not isinstance(spec, dict):
895 0
            warnobj.warning('JSON parameter specification must be a dictionary.')
896 0
            return
897

898 7
        if target in spec:
899 0
            params = spec[target]
900
        else:
901 7
            params = spec
902

903 7
        for name, value in params.items():
904 7
            try:
905 7
                parameterized.param.set_param(**{name:value})
906 0
            except ValueError as e:
907 0
                warnobj.warning(str(e))
908

909

910 7
def link_param_method(root_view, root_model):
911
    """
912
    This preprocessor jslinks ParamMethod loading parameters to any
913
    widgets generated from those parameters ensuring that the loading
914
    indicator is enabled client side.
915
    """
916 7
    methods = root_view.select(lambda p: isinstance(p, ParamMethod) and p.loading_indicator)
917 7
    widgets = root_view.select(lambda w: isinstance(w, Widget) and getattr(w, '_param_pane', None) is not None)
918

919 7
    for widget in widgets:
920 7
        for method in methods:
921 0
            for cb in method._callbacks:
922 0
                pobj = cb.cls if cb.inst is None else cb.inst
923 0
                if widget._param_pane.object is pobj and widget._param_name in cb.parameter_names:
924 0
                    if isinstance(widget, DiscreteSlider):
925 0
                        w = widget._slider
926
                    else:
927 0
                        w = widget
928 0
                    if 'value' in w._linkable_params:
929 0
                        w.jslink(method._inner_layout, value='loading')
930

931

932 7
Viewable._preprocessing_hooks.insert(0, link_param_method)

Read our documentation on viewing source code .

Loading