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)
name = param.name
kind = param.kind
ann = param.annotation
default = param.default
empty = Parameter.empty
if kind == Parameter.POSITIONAL_ONLY:
if args:
yield name, args.pop(0), False
elif ann is not empty:
yield name, ann, False
else:
yield None, None, None
elif kind == Parameter.POSITIONAL_OR_KEYWORD:
if name in kwargs:
yield name, kwargs.pop(name), True
elif args:
yield name, args.pop(0), False
elif ann is not empty:
if default is empty:
yield name, ann, False
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
We look for, in this order: def _find_abbreviations(f, args, kwargs):
- keyword arguments passed to interact[ive]() that match the parameter name. """Find the abbreviations for a function and args/kwargs passed to interact."""
- function annotations new_args = []
- default values 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
Returns an instance of Widget, or None if nothing suitable is found. def _widgets_from_abbreviations(seq):
"""Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
Raises ValueError if the kwargs or annotation value cannot be made into result = []
a widget. for name, abbrev in seq:
""" widget = _widget_from_abbrev(abbrev)
if param.name in kwargs: widget.description = name
return _widget_or_abbrev(kwargs.pop(param.name)) result.append(widget)
return result
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."""
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
args = list(args)
kwargs = kwargs.copy()
# Extract parameters from the function signature new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
for param in signature(f).parameters.values(): # Before we proceed, let's make sure that the user has passed a set of args+kwargs
param_widget = _widget_for_param(param, kwargs) # that will lead to a valid call of the function. This protects against unspecified
if param_widget is not None: # and doubly-specified arguments.
param_widget.description = param.name getcallargs(f, *[v for n,v in new_args], **{n:v for n,v in new_kwargs})
widgets.append(param_widget) # Now build the widgets from the abbreviations.
args_widgets.extend(_widgets_from_abbreviations(new_args))
# Extra parameters from keyword args - we assume f takes **kwargs kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
for name, value in sorted(kwargs.items(), key = lambda x: x[0]): kwargs_widgets.extend(_widgets_from_abbreviations(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, # 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
#-----------------------------------------------------------------------------