"""Interact with functions using widgets.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import print_function try: # Python >= 3.3 from inspect import signature, Parameter except ImportError: from IPython.utils.signatures import signature, Parameter from inspect import getcallargs from IPython.core.getipython import get_ipython from IPython.html.widgets import (Widget, Text, FloatSlider, IntSlider, Checkbox, Dropdown, Box, Button, DOMWidget, Output) from IPython.display import display, clear_output from IPython.utils.py3compat import string_types, unicode_type from IPython.utils.traitlets import HasTraits, Any, Unicode empty = Parameter.empty 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.""" if value is None: if not max > min: raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max)) value = min + abs(min-max)/2 value = type(min)(value) elif min is None and max is None: if value == 0.0: min, max, value = 0.0, 1.0, 0.5 elif value == 0: min, max, value = 0, 1, 0 elif isinstance(value, (int, float)): min, max = (-value, 3*value) if value > 0 else (3*value, -value) else: raise TypeError('expected a number, got: %r' % value) else: raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value)) if step is not None: # ensure value is on a step r = (value - min) % step value = value - r return min, max, value def _widget_abbrev_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, dict): return Dropdown(options=o) elif isinstance(o, bool): return Checkbox(value=o) elif isinstance(o, float): min, max, value = _get_min_max_value(None, None, o) return FloatSlider(value=o, min=min, max=max) elif isinstance(o, int): min, max, value = _get_min_max_value(None, None, o) return IntSlider(value=o, min=min, max=max) else: return None def _widget_abbrev(o): """Make widgets from abbreviations: single values, lists or tuples.""" float_or_int = (float, int) if isinstance(o, (list, tuple)): if o and all(isinstance(x, string_types) for x in o): return Dropdown(options=[unicode_type(k) for k in o]) elif _matches(o, (float_or_int, float_or_int)): min, max, value = _get_min_max_value(o[0], o[1]) if all(isinstance(_, int) for _ in o): cls = IntSlider else: cls = FloatSlider return cls(value=value, min=min, max=max) elif _matches(o, (float_or_int, float_or_int, float_or_int)): 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(_, int) for _ in o): cls = IntSlider else: cls = FloatSlider return cls(value=value, min=min, max=max, step=step) else: return _widget_abbrev_single_value(o) def _widget_from_abbrev(abbrev, default=empty): """Build a Widget instance given an abbreviation or Widget.""" if isinstance(abbrev, Widget) or isinstance(abbrev, fixed): return abbrev widget = _widget_abbrev(abbrev) if default is not empty and isinstance(abbrev, (list, tuple, dict)): # if it's not a single-value abbreviation, # set the initial value from the default try: widget.value = default except Exception: # ignore failure to set default pass if widget is None: raise ValueError("%r cannot be transformed to a Widget" % (abbrev,)) return widget 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: 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 def _find_abbreviations(f, kwargs): """Find the abbreviations for a function and kwargs passed to interact.""" new_kwargs = [] for param in signature(f).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 def _widgets_from_abbreviations(seq): """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets.""" result = [] for name, abbrev, default in seq: widget = _widget_from_abbrev(abbrev, default) if not widget.description: widget.description = name widget._kwarg = name result.append(widget) return result def interactive(__interact_f, **kwargs): """ Builds a group of interactive widgets tied to a function and places the group into a Box container. Returns ------- container : a Box instance containing multiple widgets Parameters ---------- __interact_f : function The function to which the interactive widgets are tied. The **kwargs should match the function signature. **kwargs : various, optional An interactive widget is created for each keyword argument that is a valid widget abbreviation. """ f = __interact_f co = kwargs.pop('clear_output', True) manual = kwargs.pop('__manual', False) kwargs_widgets = [] container = Box() container.result = None container.args = [] container.kwargs = dict() kwargs = kwargs.copy() new_kwargs = _find_abbreviations(f, 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. getcallargs(f, **{n:v for n,v,_ in new_kwargs}) # Now build the widgets from the abbreviations. kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs)) # This has to be done as an assignment, not using container.children.append, # so that traitlets notices the update. We skip any objects (such as fixed) that # are not DOMWidgets. c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)] # If we are only to run the function on demand, add a button to request this if manual: manual_button = Button(description="Run %s" % f.__name__) c.append(manual_button) # Use an output widget to capture the output of interact. output = Output() c.append(output) container.children = c # Build the callback def call_f(name=None, old=None, new=None): with output: container.kwargs = {} for widget in kwargs_widgets: value = widget.value container.kwargs[widget._kwarg] = value if co: clear_output(wait=True) if manual: manual_button.disabled = True try: container.result = f(**container.kwargs) except Exception as e: ip = get_ipython() if ip is None: container.log.warn("Exception in interact callback: %s", e, exc_info=True) else: ip.showtraceback() finally: if manual: manual_button.disabled = False # 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 manual: manual_button.on_click(call_f) else: for widget in kwargs_widgets: widget.on_trait_change(call_f, 'value') container.on_displayed(lambda _: call_f(None, None, None)) return container def interact(__interact_f=None, **kwargs): """ Displays interactive widgets which are tied to a function. 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 -------- Renders an interactive text field that shows the greeting with the passed in text. 1. Invocation of interact as a function def greeting(text="World"): print "Hello {}".format(text) interact(greeting, text="IPython Widgets") 2. Invocation of interact as a decorator @interact def greeting(text="World"): print "Hello {}".format(text) 3. Invocation of interact as a decorator with named parameters @interact(text="IPython Widgets") def greeting(text="World"): print "Hello {}".format(text) Renders an interactive slider widget and prints square of number. 1. Invocation of interact as a function def square(num=1): print "{} squared is {}".format(num, num*num) interact(square, num=5) 2. Invocation of interact as a decorator @interact def square(num=2): print "{} squared is {}".format(num, num*num) 3. Invocation of interact as a decorator with named parameters @interact(num=5) def square(num=2): print "{} squared is {}".format(num, num*num) """ # positional arg support in: https://gist.github.com/8851331 if __interact_f is not None: # This branch handles the cases 1 and 2 # 1. interact(f, **kwargs) # 2. @interact # def f(*args, **kwargs): # ... f = __interact_f w = interactive(f, **kwargs) 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 display(w) return f else: # This branch handles the case 3 # @interact(a=30, b=40) # def f(*args, **kwargs): # ... def dec(f): return interact(f, **kwargs) return dec def interact_manual(__interact_f=None, **kwargs): """interact_manual(f, **kwargs) As `interact()`, generates widgets for each argument, but rather than running the function after each widget change, adds a "Run" button and waits for it to be clicked. Useful if the function is long-running and has several parameters to change. """ return interact(__interact_f, __manual=True, **kwargs) 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)