interaction.py
274 lines
| 10.0 KiB
| text/x-python
|
PythonLexer
Brian E. Granger
|
r15140 | """Interact with functions using widgets.""" | ||
Brian E. Granger
|
r15132 | |||
Min RK
|
r19930 | # Copyright (c) IPython Development Team. | ||
Brian E. Granger
|
r15132 | # Distributed under the terms of the Modified BSD License. | ||
Brian E. Granger
|
r15140 | from __future__ import print_function | ||
Thomas Kluyver
|
r15137 | try: # Python >= 3.3 | ||
from inspect import signature, Parameter | ||||
except ImportError: | ||||
from IPython.utils.signatures import signature, Parameter | ||||
Brian E. Granger
|
r15140 | from inspect import getcallargs | ||
Thomas Kluyver
|
r15137 | |||
MinRK
|
r15193 | from IPython.core.getipython import get_ipython | ||
Jonathan Frederic
|
r17598 | from IPython.html.widgets import (Widget, Text, | ||
FloatSlider, IntSlider, Checkbox, Dropdown, | ||||
Gordon Ball
|
r17708 | Box, Button, DOMWidget) | ||
Brian E. Granger
|
r15132 | from IPython.display import display, clear_output | ||
from IPython.utils.py3compat import string_types, unicode_type | ||||
MinRK
|
r15398 | from IPython.utils.traitlets import HasTraits, Any, Unicode | ||
MinRK
|
r15348 | |||
empty = Parameter.empty | ||||
Brian E. Granger
|
r15132 | |||
def _matches(o, pattern): | ||||
Brian E. Granger
|
r15140 | """Match a pattern of types in a sequence.""" | ||
Brian E. Granger
|
r15132 | if not len(o) == len(pattern): | ||
return False | ||||
comps = zip(o,pattern) | ||||
return all(isinstance(obj,kind) for obj,kind in comps) | ||||
MinRK
|
r15148 | def _get_min_max_value(min, max, value=None, step=None): | ||
Brian E. Granger
|
r15135 | """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 | ||||
MinRK
|
r15150 | elif isinstance(value, (int, float)): | ||
Brian E. Granger
|
r15140 | min, max = (-value, 3*value) if value > 0 else (3*value, -value) | ||
Brian E. Granger
|
r15135 | else: | ||
Thomas Kluyver
|
r15137 | raise TypeError('expected a number, got: %r' % value) | ||
Brian E. Granger
|
r15135 | else: | ||
raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value)) | ||||
MinRK
|
r15150 | if step is not None: | ||
MinRK
|
r15148 | # ensure value is on a step | ||
r = (value - min) % step | ||||
value = value - r | ||||
Brian E. Granger
|
r15132 | return min, max, value | ||
Thomas Kluyver
|
r15137 | def _widget_abbrev_single_value(o): | ||
MinRK
|
r15150 | """Make widgets from single values, which can be used as parameter defaults.""" | ||
Brian E. Granger
|
r15132 | if isinstance(o, string_types): | ||
Jonathan Frederic
|
r17598 | return Text(value=unicode_type(o)) | ||
Brian E. Granger
|
r15132 | elif isinstance(o, dict): | ||
Jonathan Frederic
|
r17598 | return Dropdown(values=o) | ||
Brian E. Granger
|
r15132 | elif isinstance(o, bool): | ||
Jonathan Frederic
|
r17598 | return Checkbox(value=o) | ||
Brian E. Granger
|
r15132 | elif isinstance(o, float): | ||
Brian E. Granger
|
r15135 | min, max, value = _get_min_max_value(None, None, o) | ||
Jonathan Frederic
|
r17598 | return FloatSlider(value=o, min=min, max=max) | ||
Brian E. Granger
|
r15132 | elif isinstance(o, int): | ||
Brian E. Granger
|
r15135 | min, max, value = _get_min_max_value(None, None, o) | ||
Jonathan Frederic
|
r17598 | return IntSlider(value=o, min=min, max=max) | ||
Brian E. Granger
|
r15140 | else: | ||
return None | ||||
Thomas Kluyver
|
r15137 | |||
def _widget_abbrev(o): | ||||
"""Make widgets from abbreviations: single values, lists or tuples.""" | ||||
MinRK
|
r15150 | float_or_int = (float, int) | ||
Brian E. Granger
|
r15132 | if isinstance(o, (list, tuple)): | ||
MinRK
|
r15150 | if o and all(isinstance(x, string_types) for x in o): | ||
Jonathan Frederic
|
r17598 | return Dropdown(values=[unicode_type(k) for k in o]) | ||
MinRK
|
r15150 | elif _matches(o, (float_or_int, float_or_int)): | ||
MinRK
|
r15148 | min, max, value = _get_min_max_value(o[0], o[1]) | ||
MinRK
|
r15150 | if all(isinstance(_, int) for _ in o): | ||
Jonathan Frederic
|
r17598 | cls = IntSlider | ||
MinRK
|
r15150 | else: | ||
Jonathan Frederic
|
r17598 | cls = FloatSlider | ||
MinRK
|
r15150 | 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): | ||||
Jonathan Frederic
|
r17598 | cls = IntSlider | ||
MinRK
|
r15150 | else: | ||
Jonathan Frederic
|
r17598 | cls = FloatSlider | ||
MinRK
|
r15150 | return cls(value=value, min=min, max=max, step=step) | ||
Thomas Kluyver
|
r15137 | else: | ||
return _widget_abbrev_single_value(o) | ||||
MinRK
|
r15348 | def _widget_from_abbrev(abbrev, default=empty): | ||
"""Build a Widget instance given an abbreviation or Widget.""" | ||||
MinRK
|
r15147 | if isinstance(abbrev, Widget) or isinstance(abbrev, fixed): | ||
Brian E. Granger
|
r15140 | return abbrev | ||
Gordon Ball
|
r17058 | |||
Brian E. Granger
|
r15140 | widget = _widget_abbrev(abbrev) | ||
MinRK
|
r15398 | if default is not empty and isinstance(abbrev, (list, tuple, dict)): | ||
MinRK
|
r15348 | # if it's not a single-value abbreviation, | ||
# set the initial value from the default | ||||
try: | ||||
widget.value = default | ||||
MinRK
|
r15398 | except Exception: | ||
# ignore failure to set default | ||||
MinRK
|
r15348 | pass | ||
Thomas Kluyver
|
r15137 | if widget is None: | ||
MinRK
|
r15151 | raise ValueError("%r cannot be transformed to a Widget" % (abbrev,)) | ||
Thomas Kluyver
|
r15137 | return widget | ||
MinRK
|
r15145 | def _yield_abbreviations_for_parameter(param, kwargs): | ||
Brian E. Granger
|
r15140 | """Get an abbreviation for a function parameter.""" | ||
name = param.name | ||||
kind = param.kind | ||||
ann = param.annotation | ||||
default = param.default | ||||
MinRK
|
r15348 | not_found = (name, empty, empty) | ||
if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): | ||||
Brian E. Granger
|
r15140 | if name in kwargs: | ||
MinRK
|
r15348 | value = kwargs.pop(name) | ||
Brian E. Granger
|
r15140 | elif ann is not empty: | ||
MinRK
|
r15348 | value = ann | ||
Brian E. Granger
|
r15140 | elif default is not empty: | ||
MinRK
|
r15348 | value = default | ||
Brian E. Granger
|
r15140 | else: | ||
MinRK
|
r15145 | yield not_found | ||
MinRK
|
r15348 | yield (name, value, default) | ||
Brian E. Granger
|
r15140 | 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) | ||||
MinRK
|
r15348 | yield k, v, empty | ||
Brian E. Granger
|
r15132 | |||
MinRK
|
r15145 | def _find_abbreviations(f, kwargs): | ||
"""Find the abbreviations for a function and kwargs passed to interact.""" | ||||
Brian E. Granger
|
r15140 | new_kwargs = [] | ||
for param in signature(f).parameters.values(): | ||||
MinRK
|
r15348 | for name, value, default in _yield_abbreviations_for_parameter(param, kwargs): | ||
if value is empty: | ||||
Brian E. Granger
|
r15140 | raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name)) | ||
MinRK
|
r15348 | new_kwargs.append((name, value, default)) | ||
MinRK
|
r15145 | return new_kwargs | ||
Brian E. Granger
|
r15140 | |||
def _widgets_from_abbreviations(seq): | ||||
"""Given a sequence of (name, abbrev) tuples, return a sequence of Widgets.""" | ||||
result = [] | ||||
MinRK
|
r15348 | for name, abbrev, default in seq: | ||
widget = _widget_from_abbrev(abbrev, default) | ||||
Jessica B. Hamrick
|
r16657 | if not widget.description: | ||
widget.description = name | ||||
Brian E. Granger
|
r15140 | result.append(widget) | ||
return result | ||||
MinRK
|
r15145 | def interactive(__interact_f, **kwargs): | ||
Brian E. Granger
|
r15140 | """Build a group of widgets to interact with a function.""" | ||
MinRK
|
r15145 | f = __interact_f | ||
Brian E. Granger
|
r15132 | co = kwargs.pop('clear_output', True) | ||
Gordon Ball
|
r17923 | manual = kwargs.pop('__manual', False) | ||
Brian E. Granger
|
r15140 | kwargs_widgets = [] | ||
Jonathan Frederic
|
r17637 | container = Box() | ||
Brian E. Granger
|
r15132 | container.result = None | ||
Brian E. Granger
|
r15140 | container.args = [] | ||
Brian E. Granger
|
r15135 | container.kwargs = dict() | ||
Brian E. Granger
|
r15140 | kwargs = kwargs.copy() | ||
MinRK
|
r15145 | new_kwargs = _find_abbreviations(f, kwargs) | ||
Brian E. Granger
|
r15140 | # 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. | ||||
MinRK
|
r15348 | getcallargs(f, **{n:v for n,v,_ in new_kwargs}) | ||
Brian E. Granger
|
r15140 | # Now build the widgets from the abbreviations. | ||
kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs)) | ||||
Thomas Kluyver
|
r15137 | # This has to be done as an assignment, not using container.children.append, | ||
MinRK
|
r15147 | # so that traitlets notices the update. We skip any objects (such as fixed) that | ||
Brian E. Granger
|
r15142 | # are not DOMWidgets. | ||
MinRK
|
r15145 | c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)] | ||
Gordon Ball
|
r17058 | |||
# If we are only to run the function on demand, add a button to request this | ||||
Gordon Ball
|
r17923 | if manual: | ||
manual_button = Button(description="Run %s" % f.__name__) | ||||
c.append(manual_button) | ||||
Brian E. Granger
|
r15142 | container.children = c | ||
Brian E. Granger
|
r15134 | |||
Brian E. Granger
|
r15132 | # Build the callback | ||
Gordon Ball
|
r17058 | def call_f(name=None, old=None, new=None): | ||
MinRK
|
r15149 | container.kwargs = {} | ||
Brian E. Granger
|
r15140 | for widget in kwargs_widgets: | ||
Brian E. Granger
|
r15132 | value = widget.value | ||
Thomas Kluyver
|
r15137 | container.kwargs[widget.description] = value | ||
Brian E. Granger
|
r15132 | if co: | ||
clear_output(wait=True) | ||||
Gordon Ball
|
r17923 | if manual: | ||
manual_button.disabled = True | ||||
MinRK
|
r15193 | 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() | ||||
Gordon Ball
|
r17068 | finally: | ||
Gordon Ball
|
r17923 | if manual: | ||
manual_button.disabled = False | ||||
Brian E. Granger
|
r15132 | |||
# Wire up the widgets | ||||
Gordon Ball
|
r17923 | # If we are doing manual running, the callback is only triggered by the button | ||
Gordon Ball
|
r17058 | # Otherwise, it is triggered for every trait change received | ||
# On-demand running also suppresses running the fucntion with the initial parameters | ||||
Gordon Ball
|
r17923 | if manual: | ||
manual_button.on_click(call_f) | ||||
Gordon Ball
|
r17058 | else: | ||
for widget in kwargs_widgets: | ||||
widget.on_trait_change(call_f, 'value') | ||||
Brian E. Granger
|
r15132 | |||
Gordon Ball
|
r17058 | container.on_displayed(lambda _: call_f(None, None, None)) | ||
Brian E. Granger
|
r15132 | |||
return container | ||||
MinRK
|
r15145 | def interact(__interact_f=None, **kwargs): | ||
"""interact(f, **kwargs) | ||||
Gordon Ball
|
r17058 | |||
MinRK
|
r15145 | Interact with a function using widgets.""" | ||
# positional arg support in: https://gist.github.com/8851331 | ||||
if __interact_f is not None: | ||||
Brian E. Granger
|
r15141 | # This branch handles the cases: | ||
MinRK
|
r15145 | # 1. interact(f, **kwargs) | ||
Brian E. Granger
|
r15141 | # 2. @interact | ||
# def f(*args, **kwargs): | ||||
# ... | ||||
MinRK
|
r15145 | f = __interact_f | ||
w = interactive(f, **kwargs) | ||||
Min RK
|
r19930 | 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 | ||||
Brian E. Granger
|
r15141 | display(w) | ||
MinRK
|
r15152 | return f | ||
Brian E. Granger
|
r15141 | else: | ||
# This branch handles the case: | ||||
MinRK
|
r15145 | # @interact(a=30, b=40) | ||
Brian E. Granger
|
r15141 | # def f(*args, **kwargs): | ||
# ... | ||||
def dec(f): | ||||
Min RK
|
r19930 | return interact(f, **kwargs) | ||
Brian E. Granger
|
r15141 | return dec | ||
Brian E. Granger
|
r15140 | |||
Gordon Ball
|
r17923 | def interact_manual(__interact_f=None, **kwargs): | ||
"""interact_manual(f, **kwargs) | ||||
Gordon Ball
|
r17709 | |||
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. | ||||
""" | ||||
Gordon Ball
|
r17923 | return interact(__interact_f, __manual=True, **kwargs) | ||
Gordon Ball
|
r17709 | |||
MinRK
|
r15147 | class fixed(HasTraits): | ||
"""A pseudo-widget whose value is fixed and never synced to the client.""" | ||||
Brian E. Granger
|
r15142 | value = Any(help="Any Python object") | ||
description = Unicode('', help="Any Python object") | ||||
def __init__(self, value, **kwargs): | ||||
MinRK
|
r15147 | super(fixed, self).__init__(value=value, **kwargs) | ||