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 7
import types
12

13 7
from collections import OrderedDict
14 7
from inspect import getcallargs
15 7
from numbers import Real, Integral
16 7
from six import string_types
17

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

30 7
try:
31 7
    from inspect import getfullargspec as check_argspec
32 0
except ImportError:
33 0
    from inspect import getargspec as check_argspec # py2
34

35 7
import param
36

37 7
from .layout import Panel, Column, Row
38 7
from .pane import PaneBase, HTML, panel
39 7
from .pane.base import ReplacementPane
40 7
from .util import as_unicode
41 7
from .viewable import Viewable
42 7
from .widgets import (Checkbox, TextInput, Widget, IntSlider, FloatSlider,
43
                      Select, DiscreteSlider, Button)
44

45

46 7
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 7
    if value is None:
50 7
        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 7
        diff = max - min
53 7
        value = min + (diff / 2)
54
        # Ensure that value has the same type as diff
55 7
        if not isinstance(value, type(diff)):
56 7
            value = min + (diff // 2)
57
    else:  # value is not None
58 7
        if not isinstance(value, Real):
59 0
            raise TypeError('expected a real number, got: %r' % value)
60
        # Infer min/max from value
61 7
        if value == 0:
62
            # This gives (0, 1) of the correct type
63 7
            vrange = (value, value + 1)
64 7
        elif value > 0:
65 7
            vrange = (-value, 3*value)
66
        else:
67 0
            vrange = (3*value, -value)
68 7
        if min is None:
69 7
            min = vrange[0]
70 7
        if max is None:
71 7
            max = vrange[1]
72 7
    if step is not None:
73
        # ensure value is on a step
74 7
        tick = int((value - min) / step)
75 7
        value = min + tick * step
76 7
    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 7
    return min, max, value
79

80

81 7
def _yield_abbreviations_for_parameter(parameter, kwargs):
82
    """Get an abbreviation for a function parameter."""
83 7
    name = parameter.name
84 7
    kind = parameter.kind
85 7
    ann = parameter.annotation
86 7
    default = parameter.default
87 7
    not_found = (name, empty, empty)
88 7
    if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
89 7
        if name in kwargs:
90 7
            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 7
        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 7
def _matches(o, pattern):
110
    """Match a pattern of types in a sequence."""
111 7
    if not len(o) == len(pattern):
112 7
        return False
113 7
    comps = zip(o,pattern)
114 7
    return all(isinstance(obj,kind) for obj,kind in comps)
115

116

117 7
class interactive(PaneBase):
118

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

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

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

127 7
    def __init__(self, object, params={}, **kwargs):
128 7
        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 7
        super().__init__(object, **params)
133

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

147 7
        widgets = self.widgets_from_abbreviations(new_kwargs)
148 7
        if self.manual_update:
149 7
            widgets.append(('manual', Button(name=self.manual_name)))
150 7
        self._widgets = OrderedDict(widgets)
151 7
        pane = self.object(**self.kwargs)
152 7
        if isinstance(pane, Viewable):
153 0
            self._pane = pane
154 0
            self._internal = False
155
        else:
156 7
            self._pane = panel(pane, name=self.name)
157 7
            self._internal = True
158 7
        self._inner_layout = Row(self._pane)
159 7
        widgets = [widget for _, widget in widgets if isinstance(widget, Widget)]
160 7
        if 'name' in params:
161 7
            widgets.insert(0, HTML('<h2>%s</h2>' % self.name))
162 7
        self.widget_box = Column(*widgets)
163 7
        self.layout.objects = [self.widget_box, self._inner_layout]
164 7
        self._link_widgets()
165

166
    #----------------------------------------------------------------
167
    # Model API
168
    #----------------------------------------------------------------
169

170 7
    def _get_model(self, doc, root=None, parent=None, comm=None):
171 0
        return self._inner_layout._get_model(doc, root, parent, comm)
172

173
    #----------------------------------------------------------------
174
    # Callback API
175
    #----------------------------------------------------------------
176

177 7
    @property
178 2
    def _synced_params(self):
179 7
        return []
180

181 7
    def _link_widgets(self):
182 7
        if self.manual_update:
183 7
            widgets = [('manual', self._widgets['manual'])]
184
        else:
185 7
            widgets = self._widgets.items()
186

187 7
        for name, widget in widgets:
188 7
            def update_pane(change):
189
                # Try updating existing pane
190 7
                new_object = self.object(**self.kwargs)
191 7
                new_pane, internal = ReplacementPane._update_from_object(
192
                    new_object, self._pane, self._internal
193
                )
194 7
                if new_pane is None:
195 7
                    return
196

197
                # Replace pane entirely
198 7
                self._pane = new_pane
199 7
                self._inner_layout[0] = new_pane
200 7
                self._internal = internal
201

202 7
            if self.throttled and hasattr(widget, 'value_throttled'):
203 7
                v = 'value_throttled'
204
            else:
205 7
                v = 'value'
206

207 7
            pname = 'clicks' if name == 'manual' else v
208 7
            watcher = widget.param.watch(update_pane, pname)
209 7
            self._callbacks.append(watcher)
210

211 7
    def _cleanup(self, root):
212 7
        self._inner_layout._cleanup(root)
213 7
        super()._cleanup(root)
214

215
    #----------------------------------------------------------------
216
    # Public API
217
    #----------------------------------------------------------------
218

219 7
    @property
220 2
    def kwargs(self):
221 7
        return {k: widget.value for k, widget in self._widgets.items()
222
                if k != 'manual'}
223

224 7
    def signature(self):
225 7
        return signature(self.object)
226

227 7
    def find_abbreviations(self, kwargs):
228
        """Find the abbreviations for the given function and kwargs.
229
        Return (name, abbrev, default) tuples.
230
        """
231 7
        new_kwargs = []
232 7
        try:
233 7
            sig = self.signature()
234 0
        except (ValueError, TypeError):
235
            # can't inspect, no info from function; only use kwargs
236 0
            return [ (key, value, value) for key, value in kwargs.items() ]
237

238 7
        for parameter in sig.parameters.values():
239 7
            for name, value, default in _yield_abbreviations_for_parameter(parameter, kwargs):
240 7
                if value is empty:
241 0
                    raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
242 7
                new_kwargs.append((name, value, default))
243 7
        return new_kwargs
244

245 7
    def widgets_from_abbreviations(self, seq):
246
        """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
247 7
        result = []
248 7
        for name, abbrev, default in seq:
249 7
            if isinstance(abbrev, fixed):
250 0
                widget = abbrev
251
            else:
252 7
                widget = self.widget_from_abbrev(abbrev, name, default)
253 7
            if not (isinstance(widget, Widget) or isinstance(widget, fixed)):
254 0
                if widget is None:
255 0
                    continue
256
                else:
257 0
                    raise TypeError("{!r} is not a ValueWidget".format(widget))
258 7
            result.append((name, widget))
259 7
        return result
260

261 7
    @classmethod
262 2
    def applies(cls, object):
263 7
        return isinstance(object, types.FunctionType)
264

265 7
    @classmethod
266 7
    def widget_from_abbrev(cls, abbrev, name, default=empty):
267
        """Build a ValueWidget instance given an abbreviation or Widget."""
268 7
        if isinstance(abbrev, Widget):
269 7
            return abbrev
270

271 7
        if isinstance(abbrev, tuple):
272 7
            widget = cls.widget_from_tuple(abbrev, name, default)
273 7
            if default is not empty:
274 7
                try:
275 7
                    widget.value = default
276 0
                except Exception:
277
                    # ignore failure to set default
278 0
                    pass
279 7
            return widget
280

281
        # Try single value
282 7
        widget = cls.widget_from_single_value(abbrev, name)
283 7
        if widget is not None:
284 7
            return widget
285

286
        # Something iterable (list, dict, generator, ...). Note that str and
287
        # tuple should be handled before, that is why we check this case last.
288 7
        if isinstance(abbrev, Iterable):
289 7
            widget = cls.widget_from_iterable(abbrev, name)
290 7
            if default is not empty:
291 0
                try:
292 0
                    widget.value = default
293 0
                except Exception:
294
                    # ignore failure to set default
295 0
                    pass
296 7
            return widget
297

298
        # No idea...
299 0
        return fixed(abbrev)
300

301 7
    @staticmethod
302 2
    def widget_from_single_value(o, name):
303
        """Make widgets from single values, which can be used as parameter defaults."""
304 7
        if isinstance(o, string_types):
305 7
            return TextInput(value=as_unicode(o), name=name)
306 7
        elif isinstance(o, bool):
307 7
            return Checkbox(value=o, name=name)
308 7
        elif isinstance(o, Integral):
309 7
            min, max, value = _get_min_max_value(None, None, o)
310 7
            return IntSlider(value=o, start=min, end=max, name=name)
311 7
        elif isinstance(o, Real):
312 0
            min, max, value = _get_min_max_value(None, None, o)
313 0
            return FloatSlider(value=o, start=min, end=max, name=name)
314
        else:
315 7
            return None
316

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

356 7
    @staticmethod
357 2
    def widget_from_iterable(o, name):
358
        """Make widgets from an iterable. This should not be done for
359
        a string or tuple."""
360
        # Select expects a dict or list, so we convert an arbitrary
361
        # iterable to either of those.
362 7
        values = list(o.values()) if isinstance(o, Mapping) else list(o)
363 7
        widget_type = DiscreteSlider if all(param._is_number(v) for v in values) else Select
364 7
        if isinstance(o, (list, dict)):
365 7
            return widget_type(options=o, name=name)
366 0
        elif isinstance(o, Mapping):
367 0
            return widget_type(options=list(o.items()), name=name)
368
        else:
369 0
            return widget_type(options=list(o), name=name)
370

371
    # Return a factory for interactive functions
372 7
    @classmethod
373 2
    def factory(cls):
374 7
        options = dict(manual_update=False, manual_name="Run Interact")
375 7
        return _InteractFactory(cls, options)
376

377

378 7
class _InteractFactory(object):
379
    """
380
    Factory for instances of :class:`interactive`.
381

382
    Arguments
383
    ---------
384
    cls: class
385
      The subclass of :class:`interactive` to construct.
386
    options: dict
387
      A dict of options used to construct the interactive
388
      function. By default, this is returned by
389
      ``cls.default_options()``.
390
    kwargs: dict
391
      A dict of **kwargs to use for widgets.
392
    """
393 7
    def __init__(self, cls, options, kwargs=None):
394 7
        self.cls = cls
395 7
        self.opts = options
396 7
        self.kwargs = kwargs or {}
397

398 7
    def widget(self, f):
399
        """
400
        Return an interactive function widget for the given function.
401
        The widget is only constructed, not displayed nor attached to
402
        the function.
403
        Returns
404
        -------
405
        An instance of ``self.cls`` (typically :class:`interactive`).
406
        Parameters
407
        ----------
408
        f : function
409
            The function to which the interactive widgets are tied.
410
        """
411 0
        return self.cls(f, self.opts, **self.kwargs)
412

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

470 0
        f = __interact_f
471 0
        if f is None:
472
            # This branch handles the case 3
473
            # @interact(a=30, b=40)
474
            # def f(*args, **kwargs):
475
            #     ...
476
            #
477
            # Simply return the new factory
478 0
            return self
479 0
        elif 'throttled' in check_argspec(f).args:
480 0
            raise ValueError('A function cannot have "throttled" as an argument')
481

482
        # positional arg support in: https://gist.github.com/8851331
483
        # Handle the cases 1 and 2
484
        # 1. interact(f, **kwargs)
485
        # 2. @interact
486
        #    def f(*args, **kwargs):
487
        #        ...
488 0
        w = self.widget(f)
489 0
        try:
490 0
            f.widget = w
491 0
        except AttributeError:
492
            # some things (instancemethods) can't have attributes attached,
493
            # so wrap in a lambda
494 0
            f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
495 0
            f.widget = w
496 0
        return w.layout
497

498 7
    def options(self, **kwds):
499
        """
500
        Change options for interactive functions.
501
        Returns
502
        -------
503
        A new :class:`_InteractFactory` which will apply the
504
        options when called.
505
        """
506 7
        opts = dict(self.opts)
507 7
        for k in kwds:
508 7
            if k not in opts:
509 0
                raise ValueError("invalid option {!r}".format(k))
510 7
            opts[k] = kwds[k]
511 7
        return type(self)(self.cls, opts, self.kwargs)
512

513

514 7
interact = interactive.factory()
515 7
interact_manual = interact.options(manual_update=True, manual_name="Run Interact")
516

517

518 7
class fixed(param.Parameterized):
519
    """A pseudo-widget whose value is fixed and never synced to the client."""
520 7
    value = param.Parameter(doc="Any Python object")
521 7
    description = param.String(default='')
522

523 7
    def __init__(self, value, **kwargs):
524 0
        super().__init__(value=value, **kwargs)
525

526 7
    def get_interact_value(self):
527
        """Return the value for this widget which should be passed to
528
        interactive functions. Custom widgets can change this method
529
        to process the raw value ``self.value``.
530
        """
531 0
        return self.value

Read our documentation on viewing source code .

Loading