Utter interact insanity.

This involves a bunch of really complicated logic to handle the
different ways that function parameters can be processed in Python.
Most importantly, this includes support for *args in interact.
This commit is contained in:
Brian E. Granger 2014-02-01 16:06:40 -08:00 committed by MinRK
parent 55cddce784
commit dd9a6efaed
4 changed files with 154 additions and 64 deletions

View File

@ -9,4 +9,4 @@ from .widget_int import IntTextWidget, BoundedIntTextWidget, IntSliderWidget, In
from .widget_selection import RadioButtonsWidget, ToggleButtonsWidget, DropdownWidget, SelectWidget from .widget_selection import RadioButtonsWidget, ToggleButtonsWidget, DropdownWidget, SelectWidget
from .widget_selectioncontainer import TabWidget, AccordionWidget from .widget_selectioncontainer import TabWidget, AccordionWidget
from .widget_string import HTMLWidget, LatexWidget, TextWidget, TextareaWidget from .widget_string import HTMLWidget, LatexWidget, TextWidget, TextareaWidget
from .interaction import interact, interactive from .interaction import interact, interactive, annotate

View File

@ -1,5 +1,4 @@
"""Interact with functions using widgets. """Interact with functions using widgets."""
"""
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Copyright (c) 2013, the IPython Development Team. # Copyright (c) 2013, the IPython Development Team.
@ -13,10 +12,13 @@
# Imports # Imports
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
from __future__ import print_function
try: # Python >= 3.3 try: # Python >= 3.3
from inspect import signature, Parameter from inspect import signature, Parameter
except ImportError: except ImportError:
from IPython.utils.signatures import signature, Parameter from IPython.utils.signatures import signature, Parameter
from inspect import getcallargs
from IPython.html.widgets import (Widget, TextWidget, from IPython.html.widgets import (Widget, TextWidget,
FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget, FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
@ -30,6 +32,7 @@ from IPython.utils.py3compat import string_types, unicode_type
def _matches(o, pattern): def _matches(o, pattern):
"""Match a pattern of types in a sequence."""
if not len(o) == len(pattern): if not len(o) == len(pattern):
return False return False
comps = zip(o,pattern) comps = zip(o,pattern)
@ -49,9 +52,9 @@ def _get_min_max_value(min, max, value):
elif value == 0: elif value == 0:
min, max, value = 0, 1, 0 min, max, value = 0, 1, 0
elif isinstance(value, float): elif isinstance(value, float):
min, max = -value, 3.0*value min, max = (-value, 3.0*value) if value > 0 else (3.0*value, -value)
elif isinstance(value, int): elif isinstance(value, int):
min, max = -value, 3*value min, max = (-value, 3*value) if value > 0 else (3*value, -value)
else: else:
raise TypeError('expected a number, got: %r' % value) raise TypeError('expected a number, got: %r' % value)
else: else:
@ -67,8 +70,6 @@ def _widget_abbrev_single_value(o):
values = o.values() values = o.values()
w = DropdownWidget(value=values[0], values=values, labels=labels) w = DropdownWidget(value=values[0], values=values, labels=labels)
return w return w
# Special case float and int == 0.0
# get_range(value):
elif isinstance(o, bool): elif isinstance(o, bool):
return CheckboxWidget(value=o) return CheckboxWidget(value=o)
elif isinstance(o, float): elif isinstance(o, float):
@ -77,6 +78,8 @@ def _widget_abbrev_single_value(o):
elif isinstance(o, int): elif isinstance(o, int):
min, max, value = _get_min_max_value(None, None, o) min, max, value = _get_min_max_value(None, None, o)
return IntSliderWidget(value=o, min=min, max=max) return IntSliderWidget(value=o, min=min, max=max)
else:
return None
def _widget_abbrev(o): def _widget_abbrev(o):
"""Make widgets from abbreviations: single values, lists or tuples.""" """Make widgets from abbreviations: single values, lists or tuples."""
@ -99,92 +102,157 @@ def _widget_abbrev(o):
elif all(isinstance(x, string_types) for x in o): elif all(isinstance(x, string_types) for x in o):
return DropdownWidget(value=unicode_type(o[0]), return DropdownWidget(value=unicode_type(o[0]),
values=[unicode_type(k) for k in o]) values=[unicode_type(k) for k in o])
else: else:
return _widget_abbrev_single_value(o) return _widget_abbrev_single_value(o)
def _widget_or_abbrev(value): def _widget_from_abbrev(abbrev):
if isinstance(value, Widget): """Build a Widget intstance given an abbreviation or Widget."""
return value if isinstance(abbrev, Widget):
return abbrev
widget = _widget_abbrev(value) widget = _widget_abbrev(abbrev)
if widget is None: if widget is None:
raise ValueError("%r cannot be transformed to a Widget" % value) raise ValueError("%r cannot be transformed to a Widget" % abbrev)
return widget return widget
def _widget_for_param(param, kwargs): def _yield_abbreviations_for_parameter(param, args, kwargs):
"""Get a widget for a parameter. """Get an abbreviation for a function parameter."""
# print(param, args, kwargs)
We look for, in this order: name = param.name
- keyword arguments passed to interact[ive]() that match the parameter name. kind = param.kind
- function annotations ann = param.annotation
- default values default = param.default
empty = Parameter.empty
Returns an instance of Widget, or None if nothing suitable is found. if kind == Parameter.POSITIONAL_ONLY:
if args:
Raises ValueError if the kwargs or annotation value cannot be made into yield name, args.pop(0), False
a widget. elif ann is not empty:
""" yield name, ann, False
if param.name in kwargs: else:
return _widget_or_abbrev(kwargs.pop(param.name)) yield None, None, None
elif kind == Parameter.POSITIONAL_OR_KEYWORD:
if param.annotation is not Parameter.empty: if name in kwargs:
return _widget_or_abbrev(param.annotation) yield name, kwargs.pop(name), True
elif args:
if param.default is not Parameter.empty: yield name, args.pop(0), False
# Returns None if it's not suitable elif ann is not empty:
return _widget_abbrev_single_value(param.default) if default is empty:
yield name, ann, False
return None else:
yield name, ann, True
elif default is not empty:
yield name, default, True
else:
yield None, None, None
elif kind == Parameter.VAR_POSITIONAL:
# In this case name=args or something and we don't actually know the names.
for item in args[::]:
args.pop(0)
yield '', item, False
elif kind == Parameter.KEYWORD_ONLY:
if name in kwargs:
yield name, kwargs.pop(name), True
elif ann is not empty:
yield name, ann, True
elif default is not empty:
yield name, default, True
else:
yield None, None, None
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, True
def interactive(f, **kwargs): def _find_abbreviations(f, args, kwargs):
"""Build a group of widgets for setting the inputs to a function.""" """Find the abbreviations for a function and args/kwargs passed to interact."""
new_args = []
new_kwargs = []
for param in signature(f).parameters.values():
for name, value, kw in _yield_abbreviations_for_parameter(param, args, kwargs):
if value is None:
raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
if kw:
new_kwargs.append((name, value))
else:
new_args.append((name, value))
return new_args, new_kwargs
def _widgets_from_abbreviations(seq):
"""Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
result = []
for name, abbrev in seq:
widget = _widget_from_abbrev(abbrev)
widget.description = name
result.append(widget)
return result
def interactive(f, *args, **kwargs):
"""Build a group of widgets to interact with a function."""
co = kwargs.pop('clear_output', True) co = kwargs.pop('clear_output', True)
# First convert all args to Widget instances args_widgets = []
widgets = [] kwargs_widgets = []
container = ContainerWidget() container = ContainerWidget()
container.result = None container.result = None
container.args = []
container.kwargs = dict() container.kwargs = dict()
# We need this to be a list as we iteratively pop elements off it
# Extract parameters from the function signature args = list(args)
for param in signature(f).parameters.values(): kwargs = kwargs.copy()
param_widget = _widget_for_param(param, kwargs)
if param_widget is not None: new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
param_widget.description = param.name # Before we proceed, let's make sure that the user has passed a set of args+kwargs
widgets.append(param_widget) # that will lead to a valid call of the function. This protects against unspecified
# and doubly-specified arguments.
# Extra parameters from keyword args - we assume f takes **kwargs getcallargs(f, *[v for n,v in new_args], **{n:v for n,v in new_kwargs})
for name, value in sorted(kwargs.items(), key = lambda x: x[0]): # Now build the widgets from the abbreviations.
widget = _widget_or_abbrev(value) args_widgets.extend(_widgets_from_abbreviations(new_args))
widget.description = name kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
widgets.append(widget) kwargs_widgets.extend(_widgets_from_abbreviations(sorted(kwargs.items(), key = lambda x: x[0])))
# This has to be done as an assignment, not using container.children.append, # This has to be done as an assignment, not using container.children.append,
# so that traitlets notices the update. # so that traitlets notices the update.
container.children = widgets container.children = args_widgets + kwargs_widgets
# Build the callback # Build the callback
def call_f(name, old, new): def call_f(name, old, new):
actual_kwargs = {} container.args = []
for widget in widgets: for widget in args_widgets:
value = widget.value
container.args.append(value)
for widget in kwargs_widgets:
value = widget.value value = widget.value
container.kwargs[widget.description] = value container.kwargs[widget.description] = value
actual_kwargs[widget.description] = value
if co: if co:
clear_output(wait=True) clear_output(wait=True)
container.result = f(**actual_kwargs) container.result = f(*container.args, **container.kwargs)
# Wire up the widgets # Wire up the widgets
for widget in widgets: for widget in args_widgets:
widget.on_trait_change(call_f, 'value')
for widget in kwargs_widgets:
widget.on_trait_change(call_f, 'value') widget.on_trait_change(call_f, 'value')
container.on_displayed(lambda _: call_f(None, None, None)) container.on_displayed(lambda _: call_f(None, None, None))
return container return container
def interact(f, **kwargs): def interact(f, *args, **kwargs):
"""Interact with a function using widgets.""" """Interact with a function using widgets."""
w = interactive(f, **kwargs) w = interactive(f, *args, **kwargs)
f.widget = w f.widget = w
display(w) display(w)
def annotate(**kwargs):
"""Python 3 compatible function annotation for Python 2."""
if not kwargs:
raise ValueError('annotations must be provided as keyword arguments')
def dec(f):
if hasattr(f, '__annotations__'):
for k, v in kwargs.items():
f.__annotations__[k] = v
else:
f.__annotations__ = kwargs
return f
return dec

View File

View File

@ -0,0 +1,22 @@
"""Test interact and interactive."""
#-----------------------------------------------------------------------------
# Copyright (C) 2014 The IPython Development Team
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
import nose.tools as nt
from IPython.html.widgets import interact, interactive
from IPython.html.widgets import interaction
#-----------------------------------------------------------------------------
# Test functions
#-----------------------------------------------------------------------------