123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576 |
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- """Interact with functions using widgets."""
- from __future__ import print_function
- from __future__ import division
- try: # Python >= 3.3
- from inspect import signature, Parameter
- except ImportError:
- from IPython.utils.signatures import signature, Parameter
- from inspect import getcallargs
- try:
- from inspect import getfullargspec as check_argspec
- except ImportError:
- from inspect import getargspec as check_argspec # py2
- import sys
- from IPython.core.getipython import get_ipython
- from . import (ValueWidget, Text,
- FloatSlider, IntSlider, Checkbox, Dropdown,
- VBox, Button, DOMWidget, Output)
- from IPython.display import display, clear_output
- from ipython_genutils.py3compat import string_types, unicode_type
- from traitlets import HasTraits, Any, Unicode, observe
- from numbers import Real, Integral
- from warnings import warn
- try:
- from collections.abc import Iterable, Mapping
- except ImportError:
- from collections import Iterable, Mapping # py2
- empty = Parameter.empty
- def show_inline_matplotlib_plots():
- """Show matplotlib plots immediately if using the inline backend.
- With ipywidgets 6.0, matplotlib plots don't work well with interact when
- using the inline backend that comes with ipykernel. Basically, the inline
- backend only shows the plot after the entire cell executes, which does not
- play well with drawing plots inside of an interact function. See
- https://github.com/jupyter-widgets/ipywidgets/issues/1181/ and
- https://github.com/ipython/ipython/issues/10376 for more details. This
- function displays any matplotlib plots if the backend is the inline backend.
- """
- if 'matplotlib' not in sys.modules:
- # matplotlib hasn't been imported, nothing to do.
- return
- try:
- import matplotlib as mpl
- from ipykernel.pylab.backend_inline import flush_figures
- except ImportError:
- return
- if mpl.get_backend() == 'module://ipykernel.pylab.backend_inline':
- flush_figures()
- def interactive_output(f, controls):
- """Connect widget controls to a function.
- This function does not generate a user interface for the widgets (unlike `interact`).
- This enables customisation of the widget user interface layout.
- The user interface layout must be defined and displayed manually.
- """
- out = Output()
- def observer(change):
- kwargs = {k:v.value for k,v in controls.items()}
- show_inline_matplotlib_plots()
- with out:
- clear_output(wait=True)
- f(**kwargs)
- show_inline_matplotlib_plots()
- for k,w in controls.items():
- w.observe(observer, 'value')
- show_inline_matplotlib_plots()
- observer(None)
- return out
- def _matches(o, pattern):
- """Match a pattern of types in a sequence."""
- if not len(o) == len(pattern):
- return False
- comps = zip(o,pattern)
- return all(isinstance(obj,kind) for obj,kind in comps)
- def _get_min_max_value(min, max, value=None, step=None):
- """Return min, max, value given input values with possible None."""
- # Either min and max need to be given, or value needs to be given
- if value is None:
- if min is None or max is None:
- raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
- diff = max - min
- value = min + (diff / 2)
- # Ensure that value has the same type as diff
- if not isinstance(value, type(diff)):
- value = min + (diff // 2)
- else: # value is not None
- if not isinstance(value, Real):
- raise TypeError('expected a real number, got: %r' % value)
- # Infer min/max from value
- if value == 0:
- # This gives (0, 1) of the correct type
- vrange = (value, value + 1)
- elif value > 0:
- vrange = (-value, 3*value)
- else:
- vrange = (3*value, -value)
- if min is None:
- min = vrange[0]
- if max is None:
- max = vrange[1]
- if step is not None:
- # ensure value is on a step
- tick = int((value - min) / step)
- value = min + tick * step
- if not min <= value <= max:
- raise ValueError('value must be between min and max (min={0}, value={1}, max={2})'.format(min, value, max))
- return min, max, value
- def _yield_abbreviations_for_parameter(param, kwargs):
- """Get an abbreviation for a function parameter."""
- name = param.name
- kind = param.kind
- ann = param.annotation
- default = param.default
- not_found = (name, empty, empty)
- if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY):
- if name in kwargs:
- value = kwargs.pop(name)
- elif ann is not empty:
- warn("Using function annotations to implicitly specify interactive controls is deprecated. Use an explicit keyword argument for the parameter instead.", DeprecationWarning)
- value = ann
- elif default is not empty:
- value = default
- else:
- yield not_found
- yield (name, value, default)
- elif kind == Parameter.VAR_KEYWORD:
- # In this case name=kwargs and we yield the items in kwargs with their keys.
- for k, v in kwargs.copy().items():
- kwargs.pop(k)
- yield k, v, empty
- class interactive(VBox):
- """
- A VBox container containing a group of interactive widgets tied to a
- function.
- Parameters
- ----------
- __interact_f : function
- The function to which the interactive widgets are tied. The `**kwargs`
- should match the function signature.
- __options : dict
- A dict of options. Currently, the only supported keys are
- ``"manual"`` and ``"manual_name"``.
- **kwargs : various, optional
- An interactive widget is created for each keyword argument that is a
- valid widget abbreviation.
- Note that the first two parameters intentionally start with a double
- underscore to avoid being mixed up with keyword arguments passed by
- ``**kwargs``.
- """
- def __init__(self, __interact_f, __options={}, **kwargs):
- VBox.__init__(self, _dom_classes=['widget-interact'])
- self.result = None
- self.args = []
- self.kwargs = {}
- self.f = f = __interact_f
- self.clear_output = kwargs.pop('clear_output', True)
- self.manual = __options.get("manual", False)
- self.manual_name = __options.get("manual_name", "Run Interact")
- self.auto_display = __options.get("auto_display", False)
- new_kwargs = self.find_abbreviations(kwargs)
- # Before we proceed, let's make sure that the user has passed a set of args+kwargs
- # that will lead to a valid call of the function. This protects against unspecified
- # and doubly-specified arguments.
- try:
- check_argspec(f)
- except TypeError:
- # if we can't inspect, we can't validate
- pass
- else:
- getcallargs(f, **{n:v for n,v,_ in new_kwargs})
- # Now build the widgets from the abbreviations.
- self.kwargs_widgets = self.widgets_from_abbreviations(new_kwargs)
- # This has to be done as an assignment, not using self.children.append,
- # so that traitlets notices the update. We skip any objects (such as fixed) that
- # are not DOMWidgets.
- c = [w for w in self.kwargs_widgets if isinstance(w, DOMWidget)]
- # If we are only to run the function on demand, add a button to request this.
- if self.manual:
- self.manual_button = Button(description=self.manual_name)
- c.append(self.manual_button)
- self.out = Output()
- c.append(self.out)
- self.children = c
- # Wire up the widgets
- # If we are doing manual running, the callback is only triggered by the button
- # Otherwise, it is triggered for every trait change received
- # On-demand running also suppresses running the function with the initial parameters
- if self.manual:
- self.manual_button.on_click(self.update)
- # Also register input handlers on text areas, so the user can hit return to
- # invoke execution.
- for w in self.kwargs_widgets:
- if isinstance(w, Text):
- w.on_submit(self.update)
- else:
- for widget in self.kwargs_widgets:
- widget.observe(self.update, names='value')
- self.on_displayed(self.update)
- # Callback function
- def update(self, *args):
- """
- Call the interact function and update the output widget with
- the result of the function call.
- Parameters
- ----------
- *args : ignored
- Required for this method to be used as traitlets callback.
- """
- self.kwargs = {}
- if self.manual:
- self.manual_button.disabled = True
- try:
- show_inline_matplotlib_plots()
- with self.out:
- if self.clear_output:
- clear_output(wait=True)
- for widget in self.kwargs_widgets:
- value = widget.get_interact_value()
- self.kwargs[widget._kwarg] = value
- self.result = self.f(**self.kwargs)
- show_inline_matplotlib_plots()
- if self.auto_display and self.result is not None:
- display(self.result)
- except Exception as e:
- ip = get_ipython()
- if ip is None:
- self.log.warn("Exception in interact callback: %s", e, exc_info=True)
- else:
- ip.showtraceback()
- finally:
- if self.manual:
- self.manual_button.disabled = False
- # Find abbreviations
- def signature(self):
- return signature(self.f)
- def find_abbreviations(self, kwargs):
- """Find the abbreviations for the given function and kwargs.
- Return (name, abbrev, default) tuples.
- """
- new_kwargs = []
- try:
- sig = self.signature()
- except (ValueError, TypeError):
- # can't inspect, no info from function; only use kwargs
- return [ (key, value, value) for key, value in kwargs.items() ]
- for param in sig.parameters.values():
- for name, value, default in _yield_abbreviations_for_parameter(param, kwargs):
- if value is empty:
- raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
- new_kwargs.append((name, value, default))
- return new_kwargs
- # Abbreviations to widgets
- def widgets_from_abbreviations(self, seq):
- """Given a sequence of (name, abbrev, default) tuples, return a sequence of Widgets."""
- result = []
- for name, abbrev, default in seq:
- widget = self.widget_from_abbrev(abbrev, default)
- if not (isinstance(widget, ValueWidget) or isinstance(widget, fixed)):
- if widget is None:
- raise ValueError("{!r} cannot be transformed to a widget".format(abbrev))
- else:
- raise TypeError("{!r} is not a ValueWidget".format(widget))
- if not widget.description:
- widget.description = name
- widget._kwarg = name
- result.append(widget)
- return result
- @classmethod
- def widget_from_abbrev(cls, abbrev, default=empty):
- """Build a ValueWidget instance given an abbreviation or Widget."""
- if isinstance(abbrev, ValueWidget) or isinstance(abbrev, fixed):
- return abbrev
- if isinstance(abbrev, tuple):
- widget = cls.widget_from_tuple(abbrev)
- if default is not empty:
- try:
- widget.value = default
- except Exception:
- # ignore failure to set default
- pass
- return widget
- # Try single value
- widget = cls.widget_from_single_value(abbrev)
- if widget is not None:
- return widget
- # Something iterable (list, dict, generator, ...). Note that str and
- # tuple should be handled before, that is why we check this case last.
- if isinstance(abbrev, Iterable):
- widget = cls.widget_from_iterable(abbrev)
- if default is not empty:
- try:
- widget.value = default
- except Exception:
- # ignore failure to set default
- pass
- return widget
- # No idea...
- return None
- @staticmethod
- def widget_from_single_value(o):
- """Make widgets from single values, which can be used as parameter defaults."""
- if isinstance(o, string_types):
- return Text(value=unicode_type(o))
- elif isinstance(o, bool):
- return Checkbox(value=o)
- elif isinstance(o, Integral):
- min, max, value = _get_min_max_value(None, None, o)
- return IntSlider(value=o, min=min, max=max)
- elif isinstance(o, Real):
- min, max, value = _get_min_max_value(None, None, o)
- return FloatSlider(value=o, min=min, max=max)
- else:
- return None
- @staticmethod
- def widget_from_tuple(o):
- """Make widgets from a tuple abbreviation."""
- if _matches(o, (Real, Real)):
- min, max, value = _get_min_max_value(o[0], o[1])
- if all(isinstance(_, Integral) for _ in o):
- cls = IntSlider
- else:
- cls = FloatSlider
- return cls(value=value, min=min, max=max)
- elif _matches(o, (Real, Real, Real)):
- step = o[2]
- if step <= 0:
- raise ValueError("step must be >= 0, not %r" % step)
- min, max, value = _get_min_max_value(o[0], o[1], step=step)
- if all(isinstance(_, Integral) for _ in o):
- cls = IntSlider
- else:
- cls = FloatSlider
- return cls(value=value, min=min, max=max, step=step)
- @staticmethod
- def widget_from_iterable(o):
- """Make widgets from an iterable. This should not be done for
- a string or tuple."""
- # Dropdown expects a dict or list, so we convert an arbitrary
- # iterable to either of those.
- if isinstance(o, (list, dict)):
- return Dropdown(options=o)
- elif isinstance(o, Mapping):
- return Dropdown(options=list(o.items()))
- else:
- return Dropdown(options=list(o))
- # Return a factory for interactive functions
- @classmethod
- def factory(cls):
- options = dict(manual=False, auto_display=True, manual_name="Run Interact")
- return _InteractFactory(cls, options)
- class _InteractFactory(object):
- """
- Factory for instances of :class:`interactive`.
- This class is needed to support options like::
- >>> @interact.options(manual=True)
- ... def greeting(text="World"):
- ... print("Hello {}".format(text))
- Parameters
- ----------
- cls : class
- The subclass of :class:`interactive` to construct.
- options : dict
- A dict of options used to construct the interactive
- function. By default, this is returned by
- ``cls.default_options()``.
- kwargs : dict
- A dict of **kwargs to use for widgets.
- """
- def __init__(self, cls, options, kwargs={}):
- self.cls = cls
- self.opts = options
- self.kwargs = kwargs
- def widget(self, f):
- """
- Return an interactive function widget for the given function.
- The widget is only constructed, not displayed nor attached to
- the function.
- Returns
- -------
- An instance of ``self.cls`` (typically :class:`interactive`).
- Parameters
- ----------
- f : function
- The function to which the interactive widgets are tied.
- """
- return self.cls(f, self.opts, **self.kwargs)
- def __call__(self, __interact_f=None, **kwargs):
- """
- Make the given function interactive by adding and displaying
- the corresponding :class:`interactive` widget.
- Expects the first argument to be a function. Parameters to this
- function are widget abbreviations passed in as keyword arguments
- (``**kwargs``). Can be used as a decorator (see examples).
- Returns
- -------
- f : __interact_f with interactive widget attached to it.
- Parameters
- ----------
- __interact_f : function
- The function to which the interactive widgets are tied. The `**kwargs`
- should match the function signature. Passed to :func:`interactive()`
- **kwargs : various, optional
- An interactive widget is created for each keyword argument that is a
- valid widget abbreviation. Passed to :func:`interactive()`
- Examples
- --------
- Render an interactive text field that shows the greeting with the passed in
- text::
- # 1. Using interact as a function
- def greeting(text="World"):
- print("Hello {}".format(text))
- interact(greeting, text="IPython Widgets")
- # 2. Using interact as a decorator
- @interact
- def greeting(text="World"):
- print("Hello {}".format(text))
- # 3. Using interact as a decorator with named parameters
- @interact(text="IPython Widgets")
- def greeting(text="World"):
- print("Hello {}".format(text))
- Render an interactive slider widget and prints square of number::
- # 1. Using interact as a function
- def square(num=1):
- print("{} squared is {}".format(num, num*num))
- interact(square, num=5)
- # 2. Using interact as a decorator
- @interact
- def square(num=2):
- print("{} squared is {}".format(num, num*num))
- # 3. Using interact as a decorator with named parameters
- @interact(num=5)
- def square(num=2):
- print("{} squared is {}".format(num, num*num))
- """
- # If kwargs are given, replace self by a new
- # _InteractFactory with the updated kwargs
- if kwargs:
- kw = dict(self.kwargs)
- kw.update(kwargs)
- self = type(self)(self.cls, self.opts, kw)
- f = __interact_f
- if f is None:
- # This branch handles the case 3
- # @interact(a=30, b=40)
- # def f(*args, **kwargs):
- # ...
- #
- # Simply return the new factory
- return self
- # positional arg support in: https://gist.github.com/8851331
- # Handle the cases 1 and 2
- # 1. interact(f, **kwargs)
- # 2. @interact
- # def f(*args, **kwargs):
- # ...
- w = self.widget(f)
- try:
- f.widget = w
- except AttributeError:
- # some things (instancemethods) can't have attributes attached,
- # so wrap in a lambda
- f = lambda *args, **kwargs: __interact_f(*args, **kwargs)
- f.widget = w
- show_inline_matplotlib_plots()
- display(w)
- return f
- def options(self, **kwds):
- """
- Change options for interactive functions.
- Returns
- -------
- A new :class:`_InteractFactory` which will apply the
- options when called.
- """
- opts = dict(self.opts)
- for k in kwds:
- try:
- # Ensure that the key exists because we want to change
- # existing options, not add new ones.
- _ = opts[k]
- except KeyError:
- raise ValueError("invalid option {!r}".format(k))
- opts[k] = kwds[k]
- return type(self)(self.cls, opts, self.kwargs)
- interact = interactive.factory()
- interact_manual = interact.options(manual=True, manual_name="Run Interact")
- class fixed(HasTraits):
- """A pseudo-widget whose value is fixed and never synced to the client."""
- value = Any(help="Any Python object")
- description = Unicode('', help="Any Python object")
- def __init__(self, value, **kwargs):
- super(fixed, self).__init__(value=value, **kwargs)
- def get_interact_value(self):
- """Return the value for this widget which should be passed to
- interactive functions. Custom widgets can change this method
- to process the raw value ``self.value``.
- """
- return self.value
|