diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py
index b62a61aed..1425d77b9 100644
--- a/IPython/html/widgets/__init__.py
+++ b/IPython/html/widgets/__init__.py
@@ -9,4 +9,4 @@ from .widget_int import IntTextWidget, BoundedIntTextWidget, IntSliderWidget, In
from .widget_selection import RadioButtonsWidget, ToggleButtonsWidget, DropdownWidget, SelectWidget
from .widget_selectioncontainer import TabWidget, AccordionWidget
from .widget_string import HTMLWidget, LatexWidget, TextWidget, TextareaWidget
-from .interaction import interact, interactive
+from .interaction import interact, interactive, annotate
diff --git a/IPython/html/widgets/interaction.py b/IPython/html/widgets/interaction.py
index 8d60d111f..2faa9af96 100644
--- a/IPython/html/widgets/interaction.py
+++ b/IPython/html/widgets/interaction.py
@@ -1,5 +1,4 @@
-"""Interact with functions using widgets.
-"""
+"""Interact with functions using widgets."""
#-----------------------------------------------------------------------------
# Copyright (c) 2013, the IPython Development Team.
@@ -13,10 +12,13 @@
# Imports
#-----------------------------------------------------------------------------
+from __future__ import print_function
+
try: # Python >= 3.3
from inspect import signature, Parameter
except ImportError:
from IPython.utils.signatures import signature, Parameter
+from inspect import getcallargs
from IPython.html.widgets import (Widget, TextWidget,
FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
@@ -30,6 +32,7 @@ from IPython.utils.py3compat import string_types, unicode_type
def _matches(o, pattern):
+ """Match a pattern of types in a sequence."""
if not len(o) == len(pattern):
return False
comps = zip(o,pattern)
@@ -49,9 +52,9 @@ def _get_min_max_value(min, max, value):
elif value == 0:
min, max, value = 0, 1, 0
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):
- min, max = -value, 3*value
+ min, max = (-value, 3*value) if value > 0 else (3*value, -value)
else:
raise TypeError('expected a number, got: %r' % value)
else:
@@ -67,8 +70,6 @@ def _widget_abbrev_single_value(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):
@@ -77,6 +78,8 @@ def _widget_abbrev_single_value(o):
elif isinstance(o, int):
min, max, value = _get_min_max_value(None, None, o)
return IntSliderWidget(value=o, min=min, max=max)
+ else:
+ return None
def _widget_abbrev(o):
"""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):
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
+def _widget_from_abbrev(abbrev):
+ """Build a Widget intstance given an abbreviation or Widget."""
+ if isinstance(abbrev, Widget):
+ return abbrev
- widget = _widget_abbrev(value)
+ widget = _widget_abbrev(abbrev)
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
-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 _yield_abbreviations_for_parameter(param, args, kwargs):
+ """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
-def interactive(f, **kwargs):
- """Build a group of widgets for setting the inputs to a function."""
-
+def _find_abbreviations(f, args, kwargs):
+ """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)
- # First convert all args to Widget instances
- widgets = []
+ args_widgets = []
+ kwargs_widgets = []
container = ContainerWidget()
container.result = None
+ container.args = []
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)
-
+ # We need this to be a list as we iteratively pop elements off it
+ args = list(args)
+ kwargs = kwargs.copy()
+
+ new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
+ # 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.
+ getcallargs(f, *[v for n,v in new_args], **{n:v for n,v in new_kwargs})
+ # Now build the widgets from the abbreviations.
+ args_widgets.extend(_widgets_from_abbreviations(new_args))
+ kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
+ 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,
# so that traitlets notices the update.
- container.children = widgets
+ container.children = args_widgets + kwargs_widgets
# Build the callback
def call_f(name, old, new):
- actual_kwargs = {}
- for widget in widgets:
+ container.args = []
+ for widget in args_widgets:
+ value = widget.value
+ container.args.append(value)
+ for widget in kwargs_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)
+ container.result = f(*container.args, **container.kwargs)
# 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')
container.on_displayed(lambda _: call_f(None, None, None))
return container
-def interact(f, **kwargs):
+def interact(f, *args, **kwargs):
"""Interact with a function using widgets."""
- w = interactive(f, **kwargs)
+ w = interactive(f, *args, **kwargs)
f.widget = 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
+
diff --git a/IPython/html/widgets/tests/__init__.py b/IPython/html/widgets/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/IPython/html/widgets/tests/test_interaction.py b/IPython/html/widgets/tests/test_interaction.py
new file mode 100644
index 000000000..d2aab8a7b
--- /dev/null
+++ b/IPython/html/widgets/tests/test_interaction.py
@@ -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
+#-----------------------------------------------------------------------------
+