1
"""
2
Interact with functions using widgets.
3

4
The interact Pane implemented in this module mirrors
5
ipywidgets.interact in its API and implementation. Large parts of the
6
code were copied directly from ipywidgets:
7

8
Copyright (c) Jupyter Development Team and PyViz Development Team.
9
Distributed under the terms of the Modified BSD License.
10
"""
11 2
from __future__ import absolute_import, division, unicode_literals
12

13 2
import types
14

15 2
from collections import OrderedDict
16 2
from inspect import getcallargs
17 2
from numbers import Real, Integral
18 2
from six import string_types
19

20 2
try:  # Python >= 3.3
21 2
    from inspect import signature, Parameter
22 2
    from collections.abc import Iterable, Mapping
23 2
    empty = Parameter.empty
24 0
except ImportError:
25 0
    from collections import Iterable, Mapping
26 0
    try:
27 0
        from IPython.utils.signatures import signature, Parameter
28 0
        empty = Parameter.empty
29 0
    except Exception:
30 0
        signature, Parameter, empty = None, None, None
31

32 2
try:
33 2
    from inspect import getfullargspec as check_argspec
34 0
except ImportError:
35 0
    from inspect import getargspec as check_argspec # py2
36

37 2
import param
38

39 2
from .layout import Panel, Column, Row
40 2
from .pane import PaneBase, HTML, panel
41 2
from .util import as_unicode
42 2
from .widgets import (Checkbox, TextInput, Widget, IntSlider, FloatSlider,
43
                      Select, DiscreteSlider, Button)
44

45

46 2
def _get_min_max_value(min, max, value=None, step=None):
47
    """Return min, max, value given input values with possible None."""
48
    # Either min and max need to be given, or value needs to be given
49 2
    if value is None:
50 2
        if min is None or max is None:
51 0
            raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
52 2
        diff = max - min
53 2
        value = min + (diff / 2)
54
        # Ensure that value has the same type as diff
55 2
        if not isinstance(value, type(diff)):
56 2
            value = min + (diff // 2)
57
    else:  # value is not None
58 2
        if not isinstance(value, Real):
59 0
            raise TypeError('expected a real number, got: %r' % value)
60
        # Infer min/max from value
61 2
        if value == 0:
62
            # This gives (0, 1) of the correct type
63 2
            vrange = (value, value + 1)
64 2
        elif value > 0:
65 2
            vrange = (-value, 3*value)
66
        else:
67 0
            vrange = (3*value, -value)
68 2
        if min is None:
69 2
            min = vrange[0]
70 2
        if max is None:
71 2
            max = vrange[1]
72 2
    if step is not None:
73
        # ensure value is on a step
74 2
        tick = int((value - min) / step)
75 2
        value = min + tick * step
76 2
    if not min <= value <= max:
77 0
        raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max))
78 2
    return min, max, value
79

80

81 2
def _yield_abbreviations_for_parameter(parameter, kwargs):
82
    """Get an abbreviation for a function parameter."""
83 2
    name = parameter.name
84 2
    kind = parameter.kind
85 2
    ann = parameter.annotation
86 2
    default = parameter.default
87 2
    not_found = (name, empty, empty)
88 2
    if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
89 2
        if name in kwargs:
90 2
            value = kwargs.pop(name)
91 0
        elif ann is not empty:
92 0
            param.main.warning("Using function annotations to implicitly specify interactive controls is deprecated. "
93
                               "Use an explicit keyword argument for the parameter instead.", DeprecationWarning)
94 0
            value = ann
95 0
        elif default is not empty:
96 0
            value = default
97 0
            if isinstance(value, (Iterable, Mapping)):
98 0
                value = fixed(value)
99
        else:
100 0
            yield not_found
101 2
        yield (name, value, default)
102 0
    elif kind == Parameter.VAR_KEYWORD:
103
        # In this case name=kwargs and we yield the items in kwargs with their keys.
104 0
        for k, v in kwargs.copy().items():
105 0
            kwargs.pop(k)
106 0
            yield k, v, empty
107

108

109 2
def _matches(o, pattern):
110
    """Match a pattern of types in a sequence."""
111 2
    if not len(o) == len(pattern):
112 2
        return False
113 2
    comps = zip(o,pattern)
114 2
    return all(isinstance(obj,kind) for obj,kind in comps)
115

116

117 2
class interactive(PaneBase):
118

119 2
    default_layout = param.ClassSelector(default=Column, class_=(Panel),
120
                                         is_instance=False)
121

122 2
    manual_update = param.Boolean(default=False, doc="""
123
        Whether to update manually by clicking on button.""")
124

125 2
    manual_name = param.String(default='Run Interact')
126

127 2
    def __init__(self, object, params={}, **kwargs):
128 2
        if signature is None:
129 0
            raise ImportError('interact requires either recent Python version '
130
                              '(>=3.3 or IPython to inspect function signatures.')
131

132 2
        super(interactive, self).__init__(object, **params)
133

134 2
        new_kwargs = self.find_abbreviations(kwargs)
135
        # Before we proceed, let's make sure that the user has passed a set of args+kwargs
136
        # that will lead to a valid call of the function. This protects against unspecified
137
        # and doubly-specified arguments.
138 2
        try:
139 2
            check_argspec(object)
140 0
        except TypeError:
141
            # if we can't inspect, we can't validate
142 0
            pass
143
        else:
144 2
            getcallargs(object, **{n:v for n,v,_ in new_kwargs})
145

146 2
        widgets = self.widgets_from_abbreviations(new_kwargs)
147 2
        if self.manual_update:
148 2
            widgets.append(('manual', Button(name=self.manual_name)))
149 2
        self._widgets = OrderedDict(widgets)
150 2
        self._pane = panel(self.object(**self.kwargs), name=self.name)
151 2
        self._inner_layout = Row(self._pane)
152 2
        widgets = [widget for _, widget in widgets if isinstance(widget, Widget)]
153 2
        if 'name' in params:
154 2
            widgets.insert(0, HTML('<h2>%s</h2>' % self.name))
155 2
        self.widget_box = Column(*widgets)
156 2
        self.layout.objects = [self.widget_box, self._inner_layout]
157 2
        self._link_widgets()
158

159
    #----------------------------------------------------------------
160
    # Model API
161
    #----------------------------------------------------------------
162

163 2
    def _get_model(self, doc, root=None, parent=None, comm=None):
164 0
        return self._inner_layout._get_model(doc, root, parent, comm)
165

166
    #----------------------------------------------------------------
167
    # Callback API
168
    #----------------------------------------------------------------
169

170 2
    def _synced_params(self):
171 2
        return []
172

173 2
    def _link_widgets(self):
174 2
        if self.manual_update:
175 2
            widgets = [('manual', self._widgets['manual'])]
176
        else:
177 2
            widgets = self._widgets.items()
178

179 2
        for name, widget in widgets:
180 2
            def update_pane(change):
181
                # Try updating existing pane
182 2
                new_object = self.object(**self.kwargs)
183 2
                pane_type = self.get_pane_type(new_object)
184 2
                if type(self._pane) is pane_type:
185 2
                    if isinstance(new_object, (PaneBase, Panel)):
186 0
                        new_params = {
187
                            k: v for k, v in new_object.param.get_param_values()
188
                            if k != 'name'
189
                        }
190 0
                        self._pane.set_param(**new_params)
191
                    else:
192 2
                        self._pane.object = new_object
193 2
                    return
194

195
                # Replace pane entirely
196 2
                self._pane = panel(new_object)
197 2
                self._inner_layout[0] = self._pane
198

199 2
            pname = 'clicks' if name == 'manual' else 'value'
200 2
            watcher = widget.param.watch(update_pane, pname)
201 2
            self._callbacks.append(watcher)
202

203 2
    def _cleanup(self, root):
204 2
        self._inner_layout._cleanup(root)
205 2
        super(interactive, self)._cleanup(root)
206

207
    #----------------------------------------------------------------
208
    # Public API
209
    #----------------------------------------------------------------
210

211 2
    @property
212
    def kwargs(self):
213 2
        return {k: widget.value for k, widget in self._widgets.items()
214
                if k != 'manual'}
215

216 2
    def signature(self):
217 2
        return signature(self.object)
218

219 2
    def find_abbreviations(self, kwargs):
220
        """Find the abbreviations for the given function and kwargs.
221
        Return (name, abbrev, default) tuples.
222
        """
223 2
        new_kwargs = []
224 2
        try:
225 2
            sig = self.signature()
226 0
        except (ValueError, TypeError):
227
            # can't inspect, no info from function; only use kwargs
228 0
            return [ (key, value, value) for key, value in kwargs.items() ]
229

230 2
        for parameter in sig.parameters.values():
231 2
            for name, value, default in _yield_abbreviations_for_parameter(parameter, kwargs):
232 2
                if value is empty:
233 0
                    raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
234 2
                new_kwargs.append((name, value, default))
235 2
        return new_kwargs
236

237 2
    def widgets_from_abbreviations(self, seq):
238
        """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
239 2
        result = []
240 2
        for name, abbrev, default in seq:
241 2
            if isinstance(abbrev, fixed):
242 0
                widget = abbrev
243
            else:
244 2
                widget = self.widget_from_abbrev(abbrev, name, default)
245 2
            if not (isinstance(widget, Widget) or isinstance(widget, fixed)):
246 0
                if widget is None:
247 0
                    continue
248
                else:
249 0
                    raise TypeError("{!r} is not a ValueWidget".format(widget))
250 2
            result.append((name, widget))
251 2
        return result
252

253 2
    @classmethod
254
    def applies(cls, object):
255 2
        return isinstance(object, types.FunctionType)
256

257 2
    @classmethod
258 2
    def widget_from_abbrev(cls, abbrev, name, default=empty):
259
        """Build a ValueWidget instance given an abbreviation or Widget."""
260 2
        if isinstance(abbrev, Widget):
261 0
            return abbrev
262

263 2
        if isinstance(abbrev, tuple):
264 2
            widget = cls.widget_from_tuple(abbrev, name, default)
265 2
            if default is not empty:
266 2
                try:
267 2
                    widget.value = default
268 0
                except Exception:
269
                    # ignore failure to set default
270 0
                    pass
271 2
            return widget
272

273
        # Try single value
274 2
        widget = cls.widget_from_single_value(abbrev, name)
275 2
        if widget is not None:
276 2
            return widget
277

278
        # Something iterable (list, dict, generator, ...). Note that str and
279
        # tuple should be handled before, that is why we check this case last.
280 2
        if isinstance(abbrev, Iterable):
281 2
            widget = cls.widget_from_iterable(abbrev, name)
282 2
            if default is not empty:
283 0
                try:
284 0
                    widget.value = default
285 0
                except Exception:
286
                    # ignore failure to set default
287 0
                    pass
288 2
            return widget
289

290
        # No idea...
291 0
        return fixed(abbrev)
292

293 2
    @staticmethod
294
    def widget_from_single_value(o, name):
295
        """Make widgets from single values, which can be used as parameter defaults."""
296 2
        if isinstance(o, string_types):
297 2
            return TextInput(value=as_unicode(o), name=name)
298 2
        elif isinstance(o, bool):
299 2
            return Checkbox(value=o, name=name)
300 2
        elif isinstance(o, Integral):
301 2
            min, max, value = _get_min_max_value(None, None, o)
302 2
            return IntSlider(value=o, start=min, end=max, name=name)
303 2
        elif isinstance(o, Real):
304 0
            min, max, value = _get_min_max_value(None, None, o)
305 0
            return FloatSlider(value=o, start=min, end=max, name=name)
306
        else:
307 2
            return None
308

309 2
    @staticmethod
310 2
    def widget_from_tuple(o, name, default=empty):
311
        """Make widgets from a tuple abbreviation."""
312 2
        int_default = (default is empty or isinstance(default, int))
313 2
        if _matches(o, (Real, Real)):
314 2
            min, max, value = _get_min_max_value(o[0], o[1])
315 2
            if all(isinstance(_, Integral) for _ in o) and int_default:
316 2
                cls = IntSlider
317
            else:
318 2
                cls = FloatSlider
319 2
            return cls(value=value, start=min, end=max, name=name)
320 2
        elif _matches(o, (Real, Real, Real)):
321 2
            step = o[2]
322 2
            if step <= 0:
323 0
                raise ValueError("step must be >= 0, not %r" % step)
324 2
            min, max, value = _get_min_max_value(o[0], o[1], step=step)
325 2
            if all(isinstance(_, Integral) for _ in o) and int_default:
326 2
                cls = IntSlider
327
            else:
328 2
                cls = FloatSlider
329 2
            return cls(value=value, start=min, end=max, step=step, name=name)
330 2
        elif _matches(o, (Real, Real, Real, Real)):
331 2
            step = o[2]
332 2
            if step <= 0:
333 0
                raise ValueError("step must be >= 0, not %r" % step)
334 2
            min, max, value = _get_min_max_value(o[0], o[1], value=o[3], step=step)
335 2
            if all(isinstance(_, Integral) for _ in o):
336 2
                cls = IntSlider
337
            else:
338 2
                cls = FloatSlider
339 2
            return cls(value=value, start=min, end=max, step=step, name=name)
340 2
        elif len(o) == 4:
341 2
            min, max, value = _get_min_max_value(o[0], o[1], value=o[3])
342 2
            if all(isinstance(_, Integral) for _ in [o[0], o[1], o[3]]):
343 2
                cls = IntSlider
344
            else:
345 0
                cls = FloatSlider
346 2
            return cls(value=value, start=min, end=max, name=name)
347

348 2
    @staticmethod
349
    def widget_from_iterable(o, name):
350
        """Make widgets from an iterable. This should not be done for
351
        a string or tuple."""
352
        # Select expects a dict or list, so we convert an arbitrary
353
        # iterable to either of those.
354 2
        values = list(o.values()) if isinstance(o, Mapping) else list(o)
355 2
        widget_type = DiscreteSlider if all(param._is_number(v) for v in values) else Select
356 2
        if isinstance(o, (list, dict)):
357 2
            return widget_type(options=o, name=name)
358 0
        elif isinstance(o, Mapping):
359 0
            return widget_type(options=list(o.items()), name=name)
360
        else:
361 0
            return widget_type(options=list(o), name=name)
362

363
    # Return a factory for interactive functions
364 2
    @classmethod
365
    def factory(cls):
366 2
        options = dict(manual_update=False, manual_name="Run Interact")
367 2
        return _InteractFactory(cls, options)
368

369

370 2
class _InteractFactory(object):
371
    """
372
    Factory for instances of :class:`interactive`.
373

374
    Arguments
375
    ---------
376
    cls: class
377
      The subclass of :class:`interactive` to construct.
378
    options: dict
379
      A dict of options used to construct the interactive
380
      function. By default, this is returned by
381
      ``cls.default_options()``.
382
    kwargs: dict
383
      A dict of **kwargs to use for widgets.
384
    """
385 2
    def __init__(self, cls, options, kwargs=None):
386 2
        self.cls = cls
387 2
        self.opts = options
388 2
        self.kwargs = kwargs or {}
389

390 2
    def widget(self, f):
391
        """
392
        Return an interactive function widget for the given function.
393
        The widget is only constructed, not displayed nor attached to
394
        the function.
395
        Returns
396
        -------
397
        An instance of ``self.cls`` (typically :class:`interactive`).
398
        Parameters
399
        ----------
400
        f : function
401
            The function to which the interactive widgets are tied.
402
        """
403 0
        return self.cls(f, self.opts, **self.kwargs)
404

405 2
    def __call__(self, __interact_f=None, **kwargs):
406
        """
407
        Make the given function interactive by adding and displaying
408
        the corresponding :class:`interactive` widget.
409
        Expects the first argument to be a function. Parameters to this
410
        function are widget abbreviations passed in as keyword arguments
411
        (``**kwargs``). Can be used as a decorator (see examples).
412
        Returns
413
        -------
414
        f : __interact_f with interactive widget attached to it.
415
        Parameters
416
        ----------
417
        __interact_f : function
418
            The function to which the interactive widgets are tied. The `**kwargs`
419
            should match the function signature. Passed to :func:`interactive()`
420
        **kwargs : various, optional
421
            An interactive widget is created for each keyword argument that is a
422
            valid widget abbreviation. Passed to :func:`interactive()`
423
        Examples
424
        --------
425
        Render an interactive text field that shows the greeting with the passed in
426
        text::
427
            # 1. Using interact as a function
428
            def greeting(text="World"):
429
                print("Hello {}".format(text))
430
            interact(greeting, text="IPython Widgets")
431
            # 2. Using interact as a decorator
432
            @interact
433
            def greeting(text="World"):
434
                print("Hello {}".format(text))
435
            # 3. Using interact as a decorator with named parameters
436
            @interact(text="IPython Widgets")
437
            def greeting(text="World"):
438
                print("Hello {}".format(text))
439
        Render an interactive slider widget and prints square of number::
440
            # 1. Using interact as a function
441
            def square(num=1):
442
                print("{} squared is {}".format(num, num*num))
443
            interact(square, num=5)
444
            # 2. Using interact as a decorator
445
            @interact
446
            def square(num=2):
447
                print("{} squared is {}".format(num, num*num))
448
            # 3. Using interact as a decorator with named parameters
449
            @interact(num=5)
450
            def square(num=2):
451
                print("{} squared is {}".format(num, num*num))
452
        """
453
        # If kwargs are given, replace self by a new
454
        # _InteractFactory with the updated kwargs
455 0
        if kwargs:
456 0
            params = list(interactive.param)
457 0
            kw = dict(self.kwargs)
458 0
            kw.update({k: v for k, v in kwargs.items() if k not in params})
459 0
            opts = dict(self.opts, **{k: v for k, v in kwargs.items() if k in params})
460 0
            self = type(self)(self.cls, opts, kw)
461

462 0
        f = __interact_f
463 0
        if f is None:
464
            # This branch handles the case 3
465
            # @interact(a=30, b=40)
466
            # def f(*args, **kwargs):
467
            #     ...
468
            #
469
            # Simply return the new factory
470 0
            return self
471

472
        # positional arg support in: https://gist.github.com/8851331
473
        # Handle the cases 1 and 2
474
        # 1. interact(f, **kwargs)
475
        # 2. @interact
476
        #    def f(*args, **kwargs):
477
        #        ...
478 0
        w = self.widget(f)
479 0
        try:
480 0
            f.widget = w
481 0
        except AttributeError:
482
            # some things (instancemethods) can't have attributes attached,
483
            # so wrap in a lambda
484 0
            f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
485 0
            f.widget = w
486 0
        return w.layout
487

488 2
    def options(self, **kwds):
489
        """
490
        Change options for interactive functions.
491
        Returns
492
        -------
493
        A new :class:`_InteractFactory` which will apply the
494
        options when called.
495
        """
496 2
        opts = dict(self.opts)
497 2
        for k in kwds:
498 2
            if k not in opts:
499 0
                raise ValueError("invalid option {!r}".format(k))
500 2
            opts[k] = kwds[k]
501 2
        return type(self)(self.cls, opts, self.kwargs)
502

503

504 2
interact = interactive.factory()
505 2
interact_manual = interact.options(manual_update=True, manual_name="Run Interact")
506

507

508 2
class fixed(param.Parameterized):
509
    """A pseudo-widget whose value is fixed and never synced to the client."""
510 2
    value = param.Parameter(doc="Any Python object")
511 2
    description = param.String(default='')
512

513 2
    def __init__(self, value, **kwargs):
514 0
        super(fixed, self).__init__(value=value, **kwargs)
515

516 2
    def get_interact_value(self):
517
        """Return the value for this widget which should be passed to
518
        interactive functions. Custom widgets can change this method
519
        to process the raw value ``self.value``.
520
        """
521 0
        return self.value

Read our documentation on viewing source code .

Loading