##// END OF EJS Templates
Get widgets from function annotations and default arguments....
Get widgets from function annotations and default arguments. Also, preserve the order of function parameters from the signature where possible. This uses a backport of the Python 3.3 signature machinery that @minrk found and improved.

File last commit:

r15137:7b115517
r15137:7b115517
Show More
interact.py
190 lines | 6.9 KiB | text/x-python | PythonLexer
"""Interact with functions using widgets.
"""
#-----------------------------------------------------------------------------
# Copyright (c) 2013, the IPython Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
try: # Python >= 3.3
from inspect import signature, Parameter
except ImportError:
from IPython.utils.signatures import signature, Parameter
from IPython.html.widgets import (Widget, TextWidget,
FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
ContainerWidget)
from IPython.display import display, clear_output
from IPython.utils.py3compat import string_types, unicode_type
#-----------------------------------------------------------------------------
# Classes and Functions
#-----------------------------------------------------------------------------
def _matches(o, pattern):
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):
"""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, float):
min, max = -value, 3.0*value
elif isinstance(value, int):
min, max = -value, 3*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))
return min, max, value
def _widget_abbrev_single_value(o):
"""Make widgets from single values, which can be used written as parameter defaults."""
if isinstance(o, string_types):
return TextWidget(value=unicode_type(o))
elif isinstance(o, dict):
labels = [unicode_type(k) for k in o]
values = o.values()
w = DropdownWidget(value=values[0], values=values, labels=labels)
return w
# Special case float and int == 0.0
# get_range(value):
elif isinstance(o, bool):
return CheckboxWidget(value=o)
elif isinstance(o, float):
min, max, value = _get_min_max_value(None, None, o)
return FloatSliderWidget(value=o, min=min, max=max)
elif isinstance(o, int):
min, max, value = _get_min_max_value(None, None, o)
return IntSliderWidget(value=o, min=min, max=max)
def _widget_abbrev(o):
"""Make widgets from abbreviations: single values, lists or tuples."""
if isinstance(o, (list, tuple)):
if _matches(o, (int, int)):
min, max, value = _get_min_max_value(o[0], o[1], None)
return IntSliderWidget(value=value, min=min, max=max)
elif _matches(o, (int, int, int)):
min, max, value = _get_min_max_value(o[0], o[1], None)
return IntSliderWidget(value=value, min=min, max=max, step=o[2])
elif _matches(o, (float, float)):
min, max, value = _get_min_max_value(o[0], o[1], None)
return FloatSliderWidget(value=value, min=min, max=max)
elif _matches(o, (float, float, float)):
min, max, value = _get_min_max_value(o[0], o[1], None)
return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
elif _matches(o, (float, float, int)):
min, max, value = _get_min_max_value(o[0], o[1], None)
return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
elif all(isinstance(x, string_types) for x in o):
return DropdownWidget(value=unicode_type(o[0]),
values=[unicode_type(k) for k in o])
else:
return _widget_abbrev_single_value(o)
def _widget_or_abbrev(value):
if isinstance(value, Widget):
return value
widget = _widget_abbrev(value)
if widget is None:
raise ValueError("%r cannot be transformed to a Widget" % value)
return widget
def _widget_for_param(param, kwargs):
"""Get a widget for a parameter.
We look for, in this order:
- keyword arguments passed to interact[ive]() that match the parameter name.
- function annotations
- default values
Returns an instance of Widget, or None if nothing suitable is found.
Raises ValueError if the kwargs or annotation value cannot be made into
a widget.
"""
if param.name in kwargs:
return _widget_or_abbrev(kwargs.pop(param.name))
if param.annotation is not Parameter.empty:
return _widget_or_abbrev(param.annotation)
if param.default is not Parameter.empty:
# Returns None if it's not suitable
return _widget_abbrev_single_value(param.default)
return None
def interactive(f, **kwargs):
"""Build a group of widgets for setting the inputs to a function."""
co = kwargs.pop('clear_output', True)
# First convert all args to Widget instances
widgets = []
container = ContainerWidget()
container.result = None
container.kwargs = dict()
# Extract parameters from the function signature
for param in signature(f).parameters.values():
param_widget = _widget_for_param(param, kwargs)
if param_widget is not None:
param_widget.description = param.name
widgets.append(param_widget)
# Extra parameters from keyword args - we assume f takes **kwargs
for name, value in sorted(kwargs.items(), key = lambda x: x[0]):
widget = _widget_or_abbrev(value)
widget.description = name
widgets.append(widget)
# This has to be done as an assignment, not using container.children.append,
# so that traitlets notices the update.
container.children = widgets
# Build the callback
def call_f(name, old, new):
actual_kwargs = {}
for widget in widgets:
value = widget.value
container.kwargs[widget.description] = value
actual_kwargs[widget.description] = value
if co:
clear_output(wait=True)
container.result = f(**actual_kwargs)
# Wire up the widgets
for widget in widgets:
widget.on_trait_change(call_f, 'value')
container.on_displayed(lambda _: call_f(None, None, None))
return container
def interact(f, **kwargs):
"""Interact with a function using widgets."""
w = interactive(f, **kwargs)
f.widget = w
display(w)