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.
This commit is contained in:
Thomas Kluyver 2014-01-31 16:04:58 -08:00 committed by MinRK
parent 5853bb8ff7
commit a96c228554
2 changed files with 177 additions and 104 deletions

View File

@ -13,6 +13,11 @@
# 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)
@ -48,13 +53,13 @@ def _get_min_max_value(min, max, value):
elif isinstance(value, int):
min, max = -value, 3*value
else:
raise TypeError('expected a number, got: %r' % number)
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(o):
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):
@ -72,6 +77,9 @@ def _widget_abbrev(o):
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)
@ -92,9 +100,45 @@ def _widget_abbrev(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):
"""Interact with a function using widgets."""
"""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
@ -102,31 +146,37 @@ def interactive(f, **kwargs):
container = ContainerWidget()
container.result = None
container.kwargs = dict()
for key, value in kwargs.items():
if isinstance(value, Widget):
widget = value
else:
widget = _widget_abbrev(value)
if widget is None:
raise ValueError("Object cannot be transformed to a Widget")
widget.description = key
widgets.append((key,widget))
widgets.sort(key=lambda e: e[1].__class__.__name__)
container.children = [e[1] for e in widgets]
# 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 key, widget in widgets:
for widget in widgets:
value = widget.value
container.kwargs[key] = value
actual_kwargs[key] = 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 key, widget in widgets:
for widget in widgets:
widget.on_trait_change(call_f, 'value')
container.on_displayed(lambda _: call_f(None, None, None))
@ -134,6 +184,7 @@ def interactive(f, **kwargs):
return container
def interact(f, **kwargs):
"""Interact with a function using widgets."""
w = interactive(f, **kwargs)
f.widget = w
display(w)

File diff suppressed because one or more lines are too long