mirror of
https://github.com/jupyter/notebook.git
synced 2025-01-30 12:11:32 +08:00
191 lines
6.9 KiB
Python
191 lines
6.9 KiB
Python
"""Interact with functions using widgets.
|
|
"""
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Copyright (c) 2013, the IPython Development Team.
|
|
#
|
|
# Distributed under the terms of the Modified BSD License.
|
|
#
|
|
# The full license is in the file COPYING.txt, distributed with this software.
|
|
#-----------------------------------------------------------------------------
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# 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)
|
|
from IPython.display import display, clear_output
|
|
from IPython.utils.py3compat import string_types, unicode_type
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Classes and Functions
|
|
#-----------------------------------------------------------------------------
|
|
|
|
|
|
def _matches(o, pattern):
|
|
if not len(o) == len(pattern):
|
|
return False
|
|
comps = zip(o,pattern)
|
|
return all(isinstance(obj,kind) for obj,kind in comps)
|
|
|
|
|
|
def _get_min_max_value(min, max, value):
|
|
"""Return min, max, value given input values with possible None."""
|
|
if value is None:
|
|
if not max > min:
|
|
raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
|
|
value = min + abs(min-max)/2
|
|
value = type(min)(value)
|
|
elif min is None and max is None:
|
|
if value == 0.0:
|
|
min, max, value = 0.0, 1.0, 0.5
|
|
elif value == 0:
|
|
min, max, value = 0, 1, 0
|
|
elif isinstance(value, float):
|
|
min, max = -value, 3.0*value
|
|
elif isinstance(value, int):
|
|
min, max = -value, 3*value
|
|
else:
|
|
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_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):
|
|
labels = [unicode_type(k) for k in 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):
|
|
min, max, value = _get_min_max_value(None, None, o)
|
|
return FloatSliderWidget(value=o, min=min, max=max)
|
|
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)
|
|
return IntSliderWidget(value=value, min=min, max=max)
|
|
elif _matches(o, (int, int, int)):
|
|
min, max, value = _get_min_max_value(o[0], o[1], None)
|
|
return IntSliderWidget(value=value, min=min, max=max, step=o[2])
|
|
elif _matches(o, (float, float)):
|
|
min, max, value = _get_min_max_value(o[0], o[1], None)
|
|
return FloatSliderWidget(value=value, min=min, max=max)
|
|
elif _matches(o, (float, float, float)):
|
|
min, max, value = _get_min_max_value(o[0], o[1], None)
|
|
return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
|
|
elif _matches(o, (float, float, int)):
|
|
min, max, value = _get_min_max_value(o[0], o[1], None)
|
|
return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
|
|
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
|
|
|
|
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):
|
|
"""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
|
|
widgets = []
|
|
container = ContainerWidget()
|
|
container.result = None
|
|
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)
|
|
|
|
# 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 widget in 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)
|
|
|
|
# Wire up the widgets
|
|
for widget in widgets:
|
|
widget.on_trait_change(call_f, 'value')
|
|
|
|
container.on_displayed(lambda _: call_f(None, None, None))
|
|
|
|
return container
|
|
|
|
def interact(f, **kwargs):
|
|
"""Interact with a function using widgets."""
|
|
w = interactive(f, **kwargs)
|
|
f.widget = w
|
|
display(w)
|