1# Copyright (c) Jupyter Development Team.
2# Distributed under the terms of the Modified BSD License.
3
4"""Interact with functions using widgets."""
5
6from __future__ import print_function
7from __future__ import division
8
9try:  # Python >= 3.3
10    from inspect import signature, Parameter
11except ImportError:
12    from IPython.utils.signatures import signature, Parameter
13from inspect import getcallargs
14
15try:
16    from inspect import getfullargspec as check_argspec
17except ImportError:
18    from inspect import getargspec as check_argspec # py2
19import sys
20
21from IPython.core.getipython import get_ipython
22from . import (ValueWidget, Text,
23    FloatSlider, IntSlider, Checkbox, Dropdown,
24    VBox, Button, DOMWidget, Output)
25from IPython.display import display, clear_output
26from ipython_genutils.py3compat import string_types, unicode_type
27from traitlets import HasTraits, Any, Unicode, observe
28from numbers import Real, Integral
29from warnings import warn
30
31try:
32    from collections.abc import Iterable, Mapping
33except ImportError:
34    from collections import Iterable, Mapping # py2
35
36
37empty = Parameter.empty
38
39
40def show_inline_matplotlib_plots():
41    """Show matplotlib plots immediately if using the inline backend.
42
43    With ipywidgets 6.0, matplotlib plots don't work well with interact when
44    using the inline backend that comes with ipykernel. Basically, the inline
45    backend only shows the plot after the entire cell executes, which does not
46    play well with drawing plots inside of an interact function. See
47    https://github.com/jupyter-widgets/ipywidgets/issues/1181/ and
48    https://github.com/ipython/ipython/issues/10376 for more details. This
49    function displays any matplotlib plots if the backend is the inline backend.
50    """
51    if 'matplotlib' not in sys.modules:
52        # matplotlib hasn't been imported, nothing to do.
53        return
54
55    try:
56        import matplotlib as mpl
57        from ipykernel.pylab.backend_inline import flush_figures
58    except ImportError:
59        return
60
61    if mpl.get_backend() == 'module://ipykernel.pylab.backend_inline':
62        flush_figures()
63
64
65def interactive_output(f, controls):
66    """Connect widget controls to a function.
67
68    This function does not generate a user interface for the widgets (unlike `interact`).
69    This enables customisation of the widget user interface layout.
70    The user interface layout must be defined and displayed manually.
71    """
72
73    out = Output()
74    def observer(change):
75        kwargs = {k:v.value for k,v in controls.items()}
76        show_inline_matplotlib_plots()
77        with out:
78            clear_output(wait=True)
79            f(**kwargs)
80            show_inline_matplotlib_plots()
81    for k,w in controls.items():
82        w.observe(observer, 'value')
83    show_inline_matplotlib_plots()
84    observer(None)
85    return out
86
87
88def _matches(o, pattern):
89    """Match a pattern of types in a sequence."""
90    if not len(o) == len(pattern):
91        return False
92    comps = zip(o,pattern)
93    return all(isinstance(obj,kind) for obj,kind in comps)
94
95
96def _get_min_max_value(min, max, value=None, step=None):
97    """Return min, max, value given input values with possible None."""
98    # Either min and max need to be given, or value needs to be given
99    if value is None:
100        if min is None or max is None:
101            raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
102        diff = max - min
103        value = min + (diff / 2)
104        # Ensure that value has the same type as diff
105        if not isinstance(value, type(diff)):
106            value = min + (diff // 2)
107    else:  # value is not None
108        if not isinstance(value, Real):
109            raise TypeError('expected a real number, got: %r' % value)
110        # Infer min/max from value
111        if value == 0:
112            # This gives (0, 1) of the correct type
113            vrange = (value, value + 1)
114        elif value > 0:
115            vrange = (-value, 3*value)
116        else:
117            vrange = (3*value, -value)
118        if min is None:
119            min = vrange[0]
120        if max is None:
121            max = vrange[1]
122    if step is not None:
123        # ensure value is on a step
124        tick = int((value - min) / step)
125        value = min + tick * step
126    if not min <= value <= max:
127        raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max))
128    return min, max, value
129
130def _yield_abbreviations_for_parameter(param, kwargs):
131    """Get an abbreviation for a function parameter."""
132    name = param.name
133    kind = param.kind
134    ann = param.annotation
135    default = param.default
136    not_found = (name, empty, empty)
137    if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
138        if name in kwargs:
139            value = kwargs.pop(name)
140        elif ann is not empty:
141            warn("Using function annotations to implicitly specify interactive controls is deprecated. Use an explicit keyword argument for the parameter instead.", DeprecationWarning)
142            value = ann
143        elif default is not empty:
144            value = default
145        else:
146            yield not_found
147        yield (name, value, default)
148    elif kind == Parameter.VAR_KEYWORD:
149        # In this case name=kwargs and we yield the items in kwargs with their keys.
150        for k, v in kwargs.copy().items():
151            kwargs.pop(k)
152            yield k, v, empty
153
154
155class interactive(VBox):
156    """
157    A VBox container containing a group of interactive widgets tied to a
158    function.
159
160    Parameters
161    ----------
162    __interact_f : function
163        The function to which the interactive widgets are tied. The `**kwargs`
164        should match the function signature.
165    __options : dict
166        A dict of options. Currently, the only supported keys are
167        ``"manual"`` and ``"manual_name"``.
168    **kwargs : various, optional
169        An interactive widget is created for each keyword argument that is a
170        valid widget abbreviation.
171
172    Note that the first two parameters intentionally start with a double
173    underscore to avoid being mixed up with keyword arguments passed by
174    ``**kwargs``.
175    """
176    def __init__(self, __interact_f, __options={}, **kwargs):
177        VBox.__init__(self, _dom_classes=['widget-interact'])
178        self.result = None
179        self.args = []
180        self.kwargs = {}
181
182        self.f = f = __interact_f
183        self.clear_output = kwargs.pop('clear_output', True)
184        self.manual = __options.get("manual", False)
185        self.manual_name = __options.get("manual_name", "Run Interact")
186        self.auto_display = __options.get("auto_display", False)
187
188        new_kwargs = self.find_abbreviations(kwargs)
189        # Before we proceed, let's make sure that the user has passed a set of args+kwargs
190        # that will lead to a valid call of the function. This protects against unspecified
191        # and doubly-specified arguments.
192        try:
193            check_argspec(f)
194        except TypeError:
195            # if we can't inspect, we can't validate
196            pass
197        else:
198            getcallargs(f, **{n:v for n,v,_ in new_kwargs})
199        # Now build the widgets from the abbreviations.
200        self.kwargs_widgets = self.widgets_from_abbreviations(new_kwargs)
201
202        # This has to be done as an assignment, not using self.children.append,
203        # so that traitlets notices the update. We skip any objects (such as fixed) that
204        # are not DOMWidgets.
205        c = [w for w in self.kwargs_widgets if isinstance(w, DOMWidget)]
206
207        # If we are only to run the function on demand, add a button to request this.
208        if self.manual:
209            self.manual_button = Button(description=self.manual_name)
210            c.append(self.manual_button)
211
212        self.out = Output()
213        c.append(self.out)
214        self.children = c
215
216        # Wire up the widgets
217        # If we are doing manual running, the callback is only triggered by the button
218        # Otherwise, it is triggered for every trait change received
219        # On-demand running also suppresses running the function with the initial parameters
220        if self.manual:
221            self.manual_button.on_click(self.update)
222
223            # Also register input handlers on text areas, so the user can hit return to
224            # invoke execution.
225            for w in self.kwargs_widgets:
226                if isinstance(w, Text):
227                    w.on_submit(self.update)
228        else:
229            for widget in self.kwargs_widgets:
230                widget.observe(self.update, names='value')
231
232            self.on_displayed(self.update)
233
234    # Callback function
235    def update(self, *args):
236        """
237        Call the interact function and update the output widget with
238        the result of the function call.
239
240        Parameters
241        ----------
242        *args : ignored
243            Required for this method to be used as traitlets callback.
244        """
245        self.kwargs = {}
246        if self.manual:
247            self.manual_button.disabled = True
248        try:
249            show_inline_matplotlib_plots()
250            with self.out:
251                if self.clear_output:
252                    clear_output(wait=True)
253                for widget in self.kwargs_widgets:
254                    value = widget.get_interact_value()
255                    self.kwargs[widget._kwarg] = value
256                self.result = self.f(**self.kwargs)
257                show_inline_matplotlib_plots()
258                if self.auto_display and self.result is not None:
259                    display(self.result)
260        except Exception as e:
261            ip = get_ipython()
262            if ip is None:
263                self.log.warn("Exception in interact callback: %s", e, exc_info=True)
264            else:
265                ip.showtraceback()
266        finally:
267            if self.manual:
268                self.manual_button.disabled = False
269
270    # Find abbreviations
271    def signature(self):
272        return signature(self.f)
273
274    def find_abbreviations(self, kwargs):
275        """Find the abbreviations for the given function and kwargs.
276        Return (name, abbrev, default) tuples.
277        """
278        new_kwargs = []
279        try:
280            sig = self.signature()
281        except (ValueError, TypeError):
282            # can't inspect, no info from function; only use kwargs
283            return [ (key, value, value) for key, value in kwargs.items() ]
284
285        for param in sig.parameters.values():
286            for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
287                if value is empty:
288                    raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
289                new_kwargs.append((name, value, default))
290        return new_kwargs
291
292    # Abbreviations to widgets
293    def widgets_from_abbreviations(self, seq):
294        """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
295        result = []
296        for name, abbrev, default in seq:
297            widget = self.widget_from_abbrev(abbrev, default)
298            if not (isinstance(widget, ValueWidget) or isinstance(widget, fixed)):
299                if widget is None:
300                    raise ValueError("{!r} cannot be transformed to a widget".format(abbrev))
301                else:
302                    raise TypeError("{!r} is not a ValueWidget".format(widget))
303            if not widget.description:
304                widget.description = name
305            widget._kwarg = name
306            result.append(widget)
307        return result
308
309    @classmethod
310    def widget_from_abbrev(cls, abbrev, default=empty):
311        """Build a ValueWidget instance given an abbreviation or Widget."""
312        if isinstance(abbrev, ValueWidget) or isinstance(abbrev, fixed):
313            return abbrev
314
315        if isinstance(abbrev, tuple):
316            widget = cls.widget_from_tuple(abbrev)
317            if default is not empty:
318                try:
319                    widget.value = default
320                except Exception:
321                    # ignore failure to set default
322                    pass
323            return widget
324
325        # Try single value
326        widget = cls.widget_from_single_value(abbrev)
327        if widget is not None:
328            return widget
329
330        # Something iterable (list, dict, generator, ...). Note that str and
331        # tuple should be handled before, that is why we check this case last.
332        if isinstance(abbrev, Iterable):
333            widget = cls.widget_from_iterable(abbrev)
334            if default is not empty:
335                try:
336                    widget.value = default
337                except Exception:
338                    # ignore failure to set default
339                    pass
340            return widget
341
342        # No idea...
343        return None
344
345    @staticmethod
346    def widget_from_single_value(o):
347        """Make widgets from single values, which can be used as parameter defaults."""
348        if isinstance(o, string_types):
349            return Text(value=unicode_type(o))
350        elif isinstance(o, bool):
351            return Checkbox(value=o)
352        elif isinstance(o, Integral):
353            min, max, value = _get_min_max_value(None, None, o)
354            return IntSlider(value=o, min=min, max=max)
355        elif isinstance(o, Real):
356            min, max, value = _get_min_max_value(None, None, o)
357            return FloatSlider(value=o, min=min, max=max)
358        else:
359            return None
360
361    @staticmethod
362    def widget_from_tuple(o):
363        """Make widgets from a tuple abbreviation."""
364        if _matches(o, (Real, Real)):
365            min, max, value = _get_min_max_value(o[0], o[1])
366            if all(isinstance(_, Integral) for _ in o):
367                cls = IntSlider
368            else:
369                cls = FloatSlider
370            return cls(value=value, min=min, max=max)
371        elif _matches(o, (Real, Real, Real)):
372            step = o[2]
373            if step <= 0:
374                raise ValueError("step must be >= 0, not %r" % step)
375            min, max, value = _get_min_max_value(o[0], o[1], step=step)
376            if all(isinstance(_, Integral) for _ in o):
377                cls = IntSlider
378            else:
379                cls = FloatSlider
380            return cls(value=value, min=min, max=max, step=step)
381
382    @staticmethod
383    def widget_from_iterable(o):
384        """Make widgets from an iterable. This should not be done for
385        a string or tuple."""
386        # Dropdown expects a dict or list, so we convert an arbitrary
387        # iterable to either of those.
388        if isinstance(o, (list, dict)):
389            return Dropdown(options=o)
390        elif isinstance(o, Mapping):
391            return Dropdown(options=list(o.items()))
392        else:
393            return Dropdown(options=list(o))
394
395    # Return a factory for interactive functions
396    @classmethod
397    def factory(cls):
398        options = dict(manual=False, auto_display=True, manual_name="Run Interact")
399        return _InteractFactory(cls, options)
400
401
402class _InteractFactory(object):
403    """
404    Factory for instances of :class:`interactive`.
405
406    This class is needed to support options like::
407
408        >>> @interact.options(manual=True)
409        ... def greeting(text="World"):
410        ...     print("Hello {}".format(text))
411
412    Parameters
413    ----------
414    cls : class
415        The subclass of :class:`interactive` to construct.
416    options : dict
417        A dict of options used to construct the interactive
418        function. By default, this is returned by
419        ``cls.default_options()``.
420    kwargs : dict
421        A dict of **kwargs to use for widgets.
422    """
423    def __init__(self, cls, options, kwargs={}):
424        self.cls = cls
425        self.opts = options
426        self.kwargs = kwargs
427
428    def widget(self, f):
429        """
430        Return an interactive function widget for the given function.
431
432        The widget is only constructed, not displayed nor attached to
433        the function.
434
435        Returns
436        -------
437        An instance of ``self.cls`` (typically :class:`interactive`).
438
439        Parameters
440        ----------
441        f : function
442            The function to which the interactive widgets are tied.
443        """
444        return self.cls(f, self.opts, **self.kwargs)
445
446    def __call__(self, __interact_f=None, **kwargs):
447        """
448        Make the given function interactive by adding and displaying
449        the corresponding :class:`interactive` widget.
450
451        Expects the first argument to be a function. Parameters to this
452        function are widget abbreviations passed in as keyword arguments
453        (``**kwargs``). Can be used as a decorator (see examples).
454
455        Returns
456        -------
457        f : __interact_f with interactive widget attached to it.
458
459        Parameters
460        ----------
461        __interact_f : function
462            The function to which the interactive widgets are tied. The `**kwargs`
463            should match the function signature. Passed to :func:`interactive()`
464        **kwargs : various, optional
465            An interactive widget is created for each keyword argument that is a
466            valid widget abbreviation. Passed to :func:`interactive()`
467
468        Examples
469        --------
470        Render an interactive text field that shows the greeting with the passed in
471        text::
472
473            # 1. Using interact as a function
474            def greeting(text="World"):
475                print("Hello {}".format(text))
476            interact(greeting, text="IPython Widgets")
477
478            # 2. Using interact as a decorator
479            @interact
480            def greeting(text="World"):
481                print("Hello {}".format(text))
482
483            # 3. Using interact as a decorator with named parameters
484            @interact(text="IPython Widgets")
485            def greeting(text="World"):
486                print("Hello {}".format(text))
487
488        Render an interactive slider widget and prints square of number::
489
490            # 1. Using interact as a function
491            def square(num=1):
492                print("{} squared is {}".format(num, num*num))
493            interact(square, num=5)
494
495            # 2. Using interact as a decorator
496            @interact
497            def square(num=2):
498                print("{} squared is {}".format(num, num*num))
499
500            # 3. Using interact as a decorator with named parameters
501            @interact(num=5)
502            def square(num=2):
503                print("{} squared is {}".format(num, num*num))
504        """
505        # If kwargs are given, replace self by a new
506        # _InteractFactory with the updated kwargs
507        if kwargs:
508            kw = dict(self.kwargs)
509            kw.update(kwargs)
510            self = type(self)(self.cls, self.opts, kw)
511
512        f = __interact_f
513        if f is None:
514            # This branch handles the case 3
515            # @interact(a=30, b=40)
516            # def f(*args, **kwargs):
517            #     ...
518            #
519            # Simply return the new factory
520            return self
521
522        # positional arg support in: https://gist.github.com/8851331
523        # Handle the cases 1 and 2
524        # 1. interact(f, **kwargs)
525        # 2. @interact
526        #    def f(*args, **kwargs):
527        #        ...
528        w = self.widget(f)
529        try:
530            f.widget = w
531        except AttributeError:
532            # some things (instancemethods) can't have attributes attached,
533            # so wrap in a lambda
534            f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
535            f.widget = w
536        show_inline_matplotlib_plots()
537        display(w)
538        return f
539
540    def options(self, **kwds):
541        """
542        Change options for interactive functions.
543
544        Returns
545        -------
546        A new :class:`_InteractFactory` which will apply the
547        options when called.
548        """
549        opts = dict(self.opts)
550        for k in kwds:
551            try:
552                # Ensure that the key exists because we want to change
553                # existing options, not add new ones.
554                _ = opts[k]
555            except KeyError:
556                raise ValueError("invalid option {!r}".format(k))
557            opts[k] = kwds[k]
558        return type(self)(self.cls, opts, self.kwargs)
559
560
561interact = interactive.factory()
562interact_manual = interact.options(manual=True, manual_name="Run Interact")
563
564
565class fixed(HasTraits):
566    """A pseudo-widget whose value is fixed and never synced to the client."""
567    value = Any(help="Any Python object")
568    description = Unicode('', help="Any Python object")
569    def __init__(self, value, **kwargs):
570        super(fixed, self).__init__(value=value, **kwargs)
571    def get_interact_value(self):
572        """Return the value for this widget which should be passed to
573        interactive functions. Custom widgets can change this method
574        to process the raw value ``self.value``.
575        """
576        return self.value
577