mirror of
https://github.com/jupyter/notebook.git
synced 2024-12-21 04:10:17 +08:00
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:
parent
55cddce784
commit
dd9a6efaed
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
0
IPython/html/widgets/tests/__init__.py
Normal file
0
IPython/html/widgets/tests/__init__.py
Normal file
22
IPython/html/widgets/tests/test_interaction.py
Normal file
22
IPython/html/widgets/tests/test_interaction.py
Normal 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
|
||||
#-----------------------------------------------------------------------------
|
||||
|
Loading…
Reference in New Issue
Block a user