##// END OF EJS Templates
Adding decorator forms of interact. Yeah!
Brian E. Granger -
Show More
@@ -1,258 +1,276 b''
1 """Interact with functions using widgets."""
1 """Interact with functions using widgets."""
2
2
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Copyright (c) 2013, the IPython Development Team.
4 # Copyright (c) 2013, the IPython Development Team.
5 #
5 #
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7 #
7 #
8 # The full license is in the file COPYING.txt, distributed with this software.
8 # The full license is in the file COPYING.txt, distributed with this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 from __future__ import print_function
15 from __future__ import print_function
16
16
17 try: # Python >= 3.3
17 try: # Python >= 3.3
18 from inspect import signature, Parameter
18 from inspect import signature, Parameter
19 except ImportError:
19 except ImportError:
20 from IPython.utils.signatures import signature, Parameter
20 from IPython.utils.signatures import signature, Parameter
21 from inspect import getcallargs
21 from inspect import getcallargs
22
22
23 from IPython.html.widgets import (Widget, TextWidget,
23 from IPython.html.widgets import (Widget, TextWidget,
24 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
24 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
25 ContainerWidget)
25 ContainerWidget)
26 from IPython.display import display, clear_output
26 from IPython.display import display, clear_output
27 from IPython.utils.py3compat import string_types, unicode_type
27 from IPython.utils.py3compat import string_types, unicode_type
28
28
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 # Classes and Functions
30 # Classes and Functions
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32
32
33
33
34 def _matches(o, pattern):
34 def _matches(o, pattern):
35 """Match a pattern of types in a sequence."""
35 """Match a pattern of types in a sequence."""
36 if not len(o) == len(pattern):
36 if not len(o) == len(pattern):
37 return False
37 return False
38 comps = zip(o,pattern)
38 comps = zip(o,pattern)
39 return all(isinstance(obj,kind) for obj,kind in comps)
39 return all(isinstance(obj,kind) for obj,kind in comps)
40
40
41
41
42 def _get_min_max_value(min, max, value):
42 def _get_min_max_value(min, max, value):
43 """Return min, max, value given input values with possible None."""
43 """Return min, max, value given input values with possible None."""
44 if value is None:
44 if value is None:
45 if not max > min:
45 if not max > min:
46 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
46 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
47 value = min + abs(min-max)/2
47 value = min + abs(min-max)/2
48 value = type(min)(value)
48 value = type(min)(value)
49 elif min is None and max is None:
49 elif min is None and max is None:
50 if value == 0.0:
50 if value == 0.0:
51 min, max, value = 0.0, 1.0, 0.5
51 min, max, value = 0.0, 1.0, 0.5
52 elif value == 0:
52 elif value == 0:
53 min, max, value = 0, 1, 0
53 min, max, value = 0, 1, 0
54 elif isinstance(value, float):
54 elif isinstance(value, float):
55 min, max = (-value, 3.0*value) if value > 0 else (3.0*value, -value)
55 min, max = (-value, 3.0*value) if value > 0 else (3.0*value, -value)
56 elif isinstance(value, int):
56 elif isinstance(value, int):
57 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
57 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
58 else:
58 else:
59 raise TypeError('expected a number, got: %r' % value)
59 raise TypeError('expected a number, got: %r' % value)
60 else:
60 else:
61 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
61 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
62 return min, max, value
62 return min, max, value
63
63
64 def _widget_abbrev_single_value(o):
64 def _widget_abbrev_single_value(o):
65 """Make widgets from single values, which can be used written as parameter defaults."""
65 """Make widgets from single values, which can be used written as parameter defaults."""
66 if isinstance(o, string_types):
66 if isinstance(o, string_types):
67 return TextWidget(value=unicode_type(o))
67 return TextWidget(value=unicode_type(o))
68 elif isinstance(o, dict):
68 elif isinstance(o, dict):
69 labels = [unicode_type(k) for k in o]
69 labels = [unicode_type(k) for k in o]
70 values = o.values()
70 values = o.values()
71 w = DropdownWidget(value=values[0], values=values, labels=labels)
71 w = DropdownWidget(value=values[0], values=values, labels=labels)
72 return w
72 return w
73 elif isinstance(o, bool):
73 elif isinstance(o, bool):
74 return CheckboxWidget(value=o)
74 return CheckboxWidget(value=o)
75 elif isinstance(o, float):
75 elif isinstance(o, float):
76 min, max, value = _get_min_max_value(None, None, o)
76 min, max, value = _get_min_max_value(None, None, o)
77 return FloatSliderWidget(value=o, min=min, max=max)
77 return FloatSliderWidget(value=o, min=min, max=max)
78 elif isinstance(o, int):
78 elif isinstance(o, int):
79 min, max, value = _get_min_max_value(None, None, o)
79 min, max, value = _get_min_max_value(None, None, o)
80 return IntSliderWidget(value=o, min=min, max=max)
80 return IntSliderWidget(value=o, min=min, max=max)
81 else:
81 else:
82 return None
82 return None
83
83
84 def _widget_abbrev(o):
84 def _widget_abbrev(o):
85 """Make widgets from abbreviations: single values, lists or tuples."""
85 """Make widgets from abbreviations: single values, lists or tuples."""
86 if isinstance(o, (list, tuple)):
86 if isinstance(o, (list, tuple)):
87 if _matches(o, (int, int)):
87 if _matches(o, (int, int)):
88 min, max, value = _get_min_max_value(o[0], o[1], None)
88 min, max, value = _get_min_max_value(o[0], o[1], None)
89 return IntSliderWidget(value=value, min=min, max=max)
89 return IntSliderWidget(value=value, min=min, max=max)
90 elif _matches(o, (int, int, int)):
90 elif _matches(o, (int, int, int)):
91 min, max, value = _get_min_max_value(o[0], o[1], None)
91 min, max, value = _get_min_max_value(o[0], o[1], None)
92 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
92 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
93 elif _matches(o, (float, float)):
93 elif _matches(o, (float, float)):
94 min, max, value = _get_min_max_value(o[0], o[1], None)
94 min, max, value = _get_min_max_value(o[0], o[1], None)
95 return FloatSliderWidget(value=value, min=min, max=max)
95 return FloatSliderWidget(value=value, min=min, max=max)
96 elif _matches(o, (float, float, float)):
96 elif _matches(o, (float, float, float)):
97 min, max, value = _get_min_max_value(o[0], o[1], None)
97 min, max, value = _get_min_max_value(o[0], o[1], None)
98 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
98 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
99 elif _matches(o, (float, float, int)):
99 elif _matches(o, (float, float, int)):
100 min, max, value = _get_min_max_value(o[0], o[1], None)
100 min, max, value = _get_min_max_value(o[0], o[1], None)
101 return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
101 return FloatSliderWidget(value=value, min=min, max=max, step=float(o[2]))
102 elif all(isinstance(x, string_types) for x in o):
102 elif all(isinstance(x, string_types) for x in o):
103 return DropdownWidget(value=unicode_type(o[0]),
103 return DropdownWidget(value=unicode_type(o[0]),
104 values=[unicode_type(k) for k in o])
104 values=[unicode_type(k) for k in o])
105 else:
105 else:
106 return _widget_abbrev_single_value(o)
106 return _widget_abbrev_single_value(o)
107
107
108 def _widget_from_abbrev(abbrev):
108 def _widget_from_abbrev(abbrev):
109 """Build a Widget intstance given an abbreviation or Widget."""
109 """Build a Widget intstance given an abbreviation or Widget."""
110 if isinstance(abbrev, Widget):
110 if isinstance(abbrev, Widget):
111 return abbrev
111 return abbrev
112
112
113 widget = _widget_abbrev(abbrev)
113 widget = _widget_abbrev(abbrev)
114 if widget is None:
114 if widget is None:
115 raise ValueError("%r cannot be transformed to a Widget" % abbrev)
115 raise ValueError("%r cannot be transformed to a Widget" % abbrev)
116 return widget
116 return widget
117
117
118 def _yield_abbreviations_for_parameter(param, args, kwargs):
118 def _yield_abbreviations_for_parameter(param, args, kwargs):
119 """Get an abbreviation for a function parameter."""
119 """Get an abbreviation for a function parameter."""
120 # print(param, args, kwargs)
120 # print(param, args, kwargs)
121 name = param.name
121 name = param.name
122 kind = param.kind
122 kind = param.kind
123 ann = param.annotation
123 ann = param.annotation
124 default = param.default
124 default = param.default
125 empty = Parameter.empty
125 empty = Parameter.empty
126 if kind == Parameter.POSITIONAL_ONLY:
126 if kind == Parameter.POSITIONAL_ONLY:
127 if args:
127 if args:
128 yield name, args.pop(0), False
128 yield name, args.pop(0), False
129 elif ann is not empty:
129 elif ann is not empty:
130 yield name, ann, False
130 yield name, ann, False
131 else:
131 else:
132 yield None, None, None
132 yield None, None, None
133 elif kind == Parameter.POSITIONAL_OR_KEYWORD:
133 elif kind == Parameter.POSITIONAL_OR_KEYWORD:
134 if name in kwargs:
134 if name in kwargs:
135 yield name, kwargs.pop(name), True
135 yield name, kwargs.pop(name), True
136 elif args:
136 elif args:
137 yield name, args.pop(0), False
137 yield name, args.pop(0), False
138 elif ann is not empty:
138 elif ann is not empty:
139 if default is empty:
139 if default is empty:
140 yield name, ann, False
140 yield name, ann, False
141 else:
141 else:
142 yield name, ann, True
142 yield name, ann, True
143 elif default is not empty:
143 elif default is not empty:
144 yield name, default, True
144 yield name, default, True
145 else:
145 else:
146 yield None, None, None
146 yield None, None, None
147 elif kind == Parameter.VAR_POSITIONAL:
147 elif kind == Parameter.VAR_POSITIONAL:
148 # In this case name=args or something and we don't actually know the names.
148 # In this case name=args or something and we don't actually know the names.
149 for item in args[::]:
149 for item in args[::]:
150 args.pop(0)
150 args.pop(0)
151 yield '', item, False
151 yield '', item, False
152 elif kind == Parameter.KEYWORD_ONLY:
152 elif kind == Parameter.KEYWORD_ONLY:
153 if name in kwargs:
153 if name in kwargs:
154 yield name, kwargs.pop(name), True
154 yield name, kwargs.pop(name), True
155 elif ann is not empty:
155 elif ann is not empty:
156 yield name, ann, True
156 yield name, ann, True
157 elif default is not empty:
157 elif default is not empty:
158 yield name, default, True
158 yield name, default, True
159 else:
159 else:
160 yield None, None, None
160 yield None, None, None
161 elif kind == Parameter.VAR_KEYWORD:
161 elif kind == Parameter.VAR_KEYWORD:
162 # In this case name=kwargs and we yield the items in kwargs with their keys.
162 # In this case name=kwargs and we yield the items in kwargs with their keys.
163 for k, v in kwargs.copy().items():
163 for k, v in kwargs.copy().items():
164 kwargs.pop(k)
164 kwargs.pop(k)
165 yield k, v, True
165 yield k, v, True
166
166
167 def _find_abbreviations(f, args, kwargs):
167 def _find_abbreviations(f, args, kwargs):
168 """Find the abbreviations for a function and args/kwargs passed to interact."""
168 """Find the abbreviations for a function and args/kwargs passed to interact."""
169 new_args = []
169 new_args = []
170 new_kwargs = []
170 new_kwargs = []
171 for param in signature(f).parameters.values():
171 for param in signature(f).parameters.values():
172 for name, value, kw in _yield_abbreviations_for_parameter(param, args, kwargs):
172 for name, value, kw in _yield_abbreviations_for_parameter(param, args, kwargs):
173 if value is None:
173 if value is None:
174 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
174 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
175 if kw:
175 if kw:
176 new_kwargs.append((name, value))
176 new_kwargs.append((name, value))
177 else:
177 else:
178 new_args.append((name, value))
178 new_args.append((name, value))
179 return new_args, new_kwargs
179 return new_args, new_kwargs
180
180
181 def _widgets_from_abbreviations(seq):
181 def _widgets_from_abbreviations(seq):
182 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
182 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
183 result = []
183 result = []
184 for name, abbrev in seq:
184 for name, abbrev in seq:
185 widget = _widget_from_abbrev(abbrev)
185 widget = _widget_from_abbrev(abbrev)
186 widget.description = name
186 widget.description = name
187 result.append(widget)
187 result.append(widget)
188 return result
188 return result
189
189
190 def interactive(f, *args, **kwargs):
190 def interactive(f, *args, **kwargs):
191 """Build a group of widgets to interact with a function."""
191 """Build a group of widgets to interact with a function."""
192 co = kwargs.pop('clear_output', True)
192 co = kwargs.pop('clear_output', True)
193 args_widgets = []
193 args_widgets = []
194 kwargs_widgets = []
194 kwargs_widgets = []
195 container = ContainerWidget()
195 container = ContainerWidget()
196 container.result = None
196 container.result = None
197 container.args = []
197 container.args = []
198 container.kwargs = dict()
198 container.kwargs = dict()
199 # We need this to be a list as we iteratively pop elements off it
199 # We need this to be a list as we iteratively pop elements off it
200 args = list(args)
200 args = list(args)
201 kwargs = kwargs.copy()
201 kwargs = kwargs.copy()
202
202
203 new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
203 new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
204 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
204 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
205 # that will lead to a valid call of the function. This protects against unspecified
205 # that will lead to a valid call of the function. This protects against unspecified
206 # and doubly-specified arguments.
206 # and doubly-specified arguments.
207 getcallargs(f, *[v for n,v in new_args], **{n:v for n,v in new_kwargs})
207 getcallargs(f, *[v for n,v in new_args], **{n:v for n,v in new_kwargs})
208 # Now build the widgets from the abbreviations.
208 # Now build the widgets from the abbreviations.
209 args_widgets.extend(_widgets_from_abbreviations(new_args))
209 args_widgets.extend(_widgets_from_abbreviations(new_args))
210 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
210 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
211 kwargs_widgets.extend(_widgets_from_abbreviations(sorted(kwargs.items(), key = lambda x: x[0])))
211 kwargs_widgets.extend(_widgets_from_abbreviations(sorted(kwargs.items(), key = lambda x: x[0])))
212
212
213 # This has to be done as an assignment, not using container.children.append,
213 # This has to be done as an assignment, not using container.children.append,
214 # so that traitlets notices the update.
214 # so that traitlets notices the update.
215 container.children = args_widgets + kwargs_widgets
215 container.children = args_widgets + kwargs_widgets
216
216
217 # Build the callback
217 # Build the callback
218 def call_f(name, old, new):
218 def call_f(name, old, new):
219 container.args = []
219 container.args = []
220 for widget in args_widgets:
220 for widget in args_widgets:
221 value = widget.value
221 value = widget.value
222 container.args.append(value)
222 container.args.append(value)
223 for widget in kwargs_widgets:
223 for widget in kwargs_widgets:
224 value = widget.value
224 value = widget.value
225 container.kwargs[widget.description] = value
225 container.kwargs[widget.description] = value
226 if co:
226 if co:
227 clear_output(wait=True)
227 clear_output(wait=True)
228 container.result = f(*container.args, **container.kwargs)
228 container.result = f(*container.args, **container.kwargs)
229
229
230 # Wire up the widgets
230 # Wire up the widgets
231 for widget in args_widgets:
231 for widget in args_widgets:
232 widget.on_trait_change(call_f, 'value')
232 widget.on_trait_change(call_f, 'value')
233 for widget in kwargs_widgets:
233 for widget in kwargs_widgets:
234 widget.on_trait_change(call_f, 'value')
234 widget.on_trait_change(call_f, 'value')
235
235
236 container.on_displayed(lambda _: call_f(None, None, None))
236 container.on_displayed(lambda _: call_f(None, None, None))
237
237
238 return container
238 return container
239
239
240 def interact(f, *args, **kwargs):
240 def interact(*args, **kwargs):
241 """Interact with a function using widgets."""
241 """Interact with a function using widgets."""
242 w = interactive(f, *args, **kwargs)
242 if args and callable(args[0]):
243 f.widget = w
243 # This branch handles the cases:
244 display(w)
244 # 1. interact(f, *args, **kwargs)
245 # 2. @interact
246 # def f(*args, **kwargs):
247 # ...
248 f = args[0]
249 w = interactive(f, *args[1:], **kwargs)
250 f.widget = w
251 display(w)
252 else:
253 # This branch handles the case:
254 # @interact(10, 20, a=30, b=40)
255 # def f(*args, **kwargs):
256 # ...
257 def dec(f):
258 w = interactive(f, *args, **kwargs)
259 f.widget = w
260 display(w)
261 return f
262 return dec
245
263
246 def annotate(**kwargs):
264 def annotate(**kwargs):
247 """Python 3 compatible function annotation for Python 2."""
265 """Python 3 compatible function annotation for Python 2."""
248 if not kwargs:
266 if not kwargs:
249 raise ValueError('annotations must be provided as keyword arguments')
267 raise ValueError('annotations must be provided as keyword arguments')
250 def dec(f):
268 def dec(f):
251 if hasattr(f, '__annotations__'):
269 if hasattr(f, '__annotations__'):
252 for k, v in kwargs.items():
270 for k, v in kwargs.items():
253 f.__annotations__[k] = v
271 f.__annotations__[k] = v
254 else:
272 else:
255 f.__annotations__ = kwargs
273 f.__annotations__ = kwargs
256 return f
274 return f
257 return dec
275 return dec
258
276
General Comments 0
You need to be logged in to leave comments. Login now