##// END OF EJS Templates
update interact now that SelectionWidget.values is a dict
MinRK -
Show More
@@ -1,286 +1,285 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, DOMWidget)
25 ContainerWidget, DOMWidget)
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 from IPython.utils.traitlets import HasTraits, Any, Unicode
28 from IPython.utils.traitlets import HasTraits, Any, Unicode
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Classes and Functions
31 # Classes and Functions
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 def _matches(o, pattern):
35 def _matches(o, pattern):
36 """Match a pattern of types in a sequence."""
36 """Match a pattern of types in a sequence."""
37 if not len(o) == len(pattern):
37 if not len(o) == len(pattern):
38 return False
38 return False
39 comps = zip(o,pattern)
39 comps = zip(o,pattern)
40 return all(isinstance(obj,kind) for obj,kind in comps)
40 return all(isinstance(obj,kind) for obj,kind in comps)
41
41
42
42
43 def _get_min_max_value(min, max, value):
43 def _get_min_max_value(min, max, value):
44 """Return min, max, value given input values with possible None."""
44 """Return min, max, value given input values with possible None."""
45 if value is None:
45 if value is None:
46 if not max > min:
46 if not max > min:
47 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
47 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
48 value = min + abs(min-max)/2
48 value = min + abs(min-max)/2
49 value = type(min)(value)
49 value = type(min)(value)
50 elif min is None and max is None:
50 elif min is None and max is None:
51 if value == 0.0:
51 if value == 0.0:
52 min, max, value = 0.0, 1.0, 0.5
52 min, max, value = 0.0, 1.0, 0.5
53 elif value == 0:
53 elif value == 0:
54 min, max, value = 0, 1, 0
54 min, max, value = 0, 1, 0
55 elif isinstance(value, float):
55 elif isinstance(value, float):
56 min, max = (-value, 3.0*value) if value > 0 else (3.0*value, -value)
56 min, max = (-value, 3.0*value) if value > 0 else (3.0*value, -value)
57 elif isinstance(value, int):
57 elif isinstance(value, int):
58 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
58 min, max = (-value, 3*value) if value > 0 else (3*value, -value)
59 else:
59 else:
60 raise TypeError('expected a number, got: %r' % value)
60 raise TypeError('expected a number, got: %r' % value)
61 else:
61 else:
62 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
62 raise ValueError('unable to infer range, value from: ({0}, {1}, {2})'.format(min, max, value))
63 return min, max, value
63 return min, max, value
64
64
65 def _widget_abbrev_single_value(o):
65 def _widget_abbrev_single_value(o):
66 """Make widgets from single values, which can be used written as parameter defaults."""
66 """Make widgets from single values, which can be used written as parameter defaults."""
67 if isinstance(o, string_types):
67 if isinstance(o, string_types):
68 return TextWidget(value=unicode_type(o))
68 return TextWidget(value=unicode_type(o))
69 elif isinstance(o, dict):
69 elif isinstance(o, dict):
70 labels = [unicode_type(k) for k in o]
70 # get a single value in a Python 2+3 way:
71 values = o.values()
71 value = next(iter(o.values()))
72 w = DropdownWidget(value=values[0], values=values, labels=labels)
72 return DropdownWidget(value=value, values=o)
73 return w
74 elif isinstance(o, bool):
73 elif isinstance(o, bool):
75 return CheckboxWidget(value=o)
74 return CheckboxWidget(value=o)
76 elif isinstance(o, float):
75 elif isinstance(o, float):
77 min, max, value = _get_min_max_value(None, None, o)
76 min, max, value = _get_min_max_value(None, None, o)
78 return FloatSliderWidget(value=o, min=min, max=max)
77 return FloatSliderWidget(value=o, min=min, max=max)
79 elif isinstance(o, int):
78 elif isinstance(o, int):
80 min, max, value = _get_min_max_value(None, None, o)
79 min, max, value = _get_min_max_value(None, None, o)
81 return IntSliderWidget(value=o, min=min, max=max)
80 return IntSliderWidget(value=o, min=min, max=max)
82 else:
81 else:
83 return None
82 return None
84
83
85 def _widget_abbrev(o):
84 def _widget_abbrev(o):
86 """Make widgets from abbreviations: single values, lists or tuples."""
85 """Make widgets from abbreviations: single values, lists or tuples."""
87 if isinstance(o, (list, tuple)):
86 if isinstance(o, (list, tuple)):
88 if _matches(o, (int, int)):
87 if _matches(o, (int, int)):
89 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)
90 return IntSliderWidget(value=value, min=min, max=max)
89 return IntSliderWidget(value=value, min=min, max=max)
91 elif _matches(o, (int, int, int)):
90 elif _matches(o, (int, int, int)):
92 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)
93 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
92 return IntSliderWidget(value=value, min=min, max=max, step=o[2])
94 elif _matches(o, (float, float)):
93 elif _matches(o, (float, float)):
95 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)
96 return FloatSliderWidget(value=value, min=min, max=max)
95 return FloatSliderWidget(value=value, min=min, max=max)
97 elif _matches(o, (float, float, float)):
96 elif _matches(o, (float, float, float)):
98 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)
99 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
98 return FloatSliderWidget(value=value, min=min, max=max, step=o[2])
100 elif _matches(o, (float, float, int)):
99 elif _matches(o, (float, float, int)):
101 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)
102 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]))
103 elif all(isinstance(x, string_types) for x in o):
102 elif all(isinstance(x, string_types) for x in o):
104 return DropdownWidget(value=unicode_type(o[0]),
103 return DropdownWidget(value=unicode_type(o[0]),
105 values=[unicode_type(k) for k in o])
104 values=[unicode_type(k) for k in o])
106 else:
105 else:
107 return _widget_abbrev_single_value(o)
106 return _widget_abbrev_single_value(o)
108
107
109 def _widget_from_abbrev(abbrev):
108 def _widget_from_abbrev(abbrev):
110 """Build a Widget intstance given an abbreviation or Widget."""
109 """Build a Widget intstance given an abbreviation or Widget."""
111 if isinstance(abbrev, Widget) or isinstance(abbrev, const):
110 if isinstance(abbrev, Widget) or isinstance(abbrev, const):
112 return abbrev
111 return abbrev
113
112
114 widget = _widget_abbrev(abbrev)
113 widget = _widget_abbrev(abbrev)
115 if widget is None:
114 if widget is None:
116 raise ValueError("%r cannot be transformed to a Widget" % abbrev)
115 raise ValueError("%r cannot be transformed to a Widget" % abbrev)
117 return widget
116 return widget
118
117
119 def _yield_abbreviations_for_parameter(param, args, kwargs):
118 def _yield_abbreviations_for_parameter(param, args, kwargs):
120 """Get an abbreviation for a function parameter."""
119 """Get an abbreviation for a function parameter."""
121 # print(param, args, kwargs)
120 # print(param, args, kwargs)
122 name = param.name
121 name = param.name
123 kind = param.kind
122 kind = param.kind
124 ann = param.annotation
123 ann = param.annotation
125 default = param.default
124 default = param.default
126 empty = Parameter.empty
125 empty = Parameter.empty
127 if kind == Parameter.POSITIONAL_ONLY:
126 if kind == Parameter.POSITIONAL_ONLY:
128 if args:
127 if args:
129 yield name, args.pop(0), False
128 yield name, args.pop(0), False
130 elif ann is not empty:
129 elif ann is not empty:
131 yield name, ann, False
130 yield name, ann, False
132 else:
131 else:
133 yield None, None, None
132 yield None, None, None
134 elif kind == Parameter.POSITIONAL_OR_KEYWORD:
133 elif kind == Parameter.POSITIONAL_OR_KEYWORD:
135 if name in kwargs:
134 if name in kwargs:
136 yield name, kwargs.pop(name), True
135 yield name, kwargs.pop(name), True
137 elif args:
136 elif args:
138 yield name, args.pop(0), False
137 yield name, args.pop(0), False
139 elif ann is not empty:
138 elif ann is not empty:
140 if default is empty:
139 if default is empty:
141 yield name, ann, False
140 yield name, ann, False
142 else:
141 else:
143 yield name, ann, True
142 yield name, ann, True
144 elif default is not empty:
143 elif default is not empty:
145 yield name, default, True
144 yield name, default, True
146 else:
145 else:
147 yield None, None, None
146 yield None, None, None
148 elif kind == Parameter.VAR_POSITIONAL:
147 elif kind == Parameter.VAR_POSITIONAL:
149 # 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.
150 for item in args[::]:
149 for item in args[::]:
151 args.pop(0)
150 args.pop(0)
152 yield '', item, False
151 yield '', item, False
153 elif kind == Parameter.KEYWORD_ONLY:
152 elif kind == Parameter.KEYWORD_ONLY:
154 if name in kwargs:
153 if name in kwargs:
155 yield name, kwargs.pop(name), True
154 yield name, kwargs.pop(name), True
156 elif ann is not empty:
155 elif ann is not empty:
157 yield name, ann, True
156 yield name, ann, True
158 elif default is not empty:
157 elif default is not empty:
159 yield name, default, True
158 yield name, default, True
160 else:
159 else:
161 yield None, None, None
160 yield None, None, None
162 elif kind == Parameter.VAR_KEYWORD:
161 elif kind == Parameter.VAR_KEYWORD:
163 # 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.
164 for k, v in kwargs.copy().items():
163 for k, v in kwargs.copy().items():
165 kwargs.pop(k)
164 kwargs.pop(k)
166 yield k, v, True
165 yield k, v, True
167
166
168 def _find_abbreviations(f, args, kwargs):
167 def _find_abbreviations(f, args, kwargs):
169 """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."""
170 new_args = []
169 new_args = []
171 new_kwargs = []
170 new_kwargs = []
172 for param in signature(f).parameters.values():
171 for param in signature(f).parameters.values():
173 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):
174 if value is None:
173 if value is None:
175 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))
176 if kw:
175 if kw:
177 new_kwargs.append((name, value))
176 new_kwargs.append((name, value))
178 else:
177 else:
179 new_args.append((name, value))
178 new_args.append((name, value))
180 return new_args, new_kwargs
179 return new_args, new_kwargs
181
180
182 def _widgets_from_abbreviations(seq):
181 def _widgets_from_abbreviations(seq):
183 """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."""
184 result = []
183 result = []
185 for name, abbrev in seq:
184 for name, abbrev in seq:
186 widget = _widget_from_abbrev(abbrev)
185 widget = _widget_from_abbrev(abbrev)
187 widget.description = name
186 widget.description = name
188 result.append(widget)
187 result.append(widget)
189 return result
188 return result
190
189
191 def interactive(f, *args, **kwargs):
190 def interactive(f, *args, **kwargs):
192 """Build a group of widgets to interact with a function."""
191 """Build a group of widgets to interact with a function."""
193 co = kwargs.pop('clear_output', True)
192 co = kwargs.pop('clear_output', True)
194 args_widgets = []
193 args_widgets = []
195 kwargs_widgets = []
194 kwargs_widgets = []
196 container = ContainerWidget()
195 container = ContainerWidget()
197 container.result = None
196 container.result = None
198 container.args = []
197 container.args = []
199 container.kwargs = dict()
198 container.kwargs = dict()
200 # 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
201 args = list(args)
200 args = list(args)
202 kwargs = kwargs.copy()
201 kwargs = kwargs.copy()
203
202
204 new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
203 new_args, new_kwargs = _find_abbreviations(f, args, kwargs)
205 # 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
206 # 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
207 # and doubly-specified arguments.
206 # and doubly-specified arguments.
208 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})
209 # Now build the widgets from the abbreviations.
208 # Now build the widgets from the abbreviations.
210 args_widgets.extend(_widgets_from_abbreviations(new_args))
209 args_widgets.extend(_widgets_from_abbreviations(new_args))
211 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
210 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
212 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])))
213
212
214 # 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,
215 # so that traitlets notices the update. We skip any objects (such as const) that
214 # so that traitlets notices the update. We skip any objects (such as const) that
216 # are not DOMWidgets.
215 # are not DOMWidgets.
217 c = [w for w in args_widgets+kwargs_widgets if isinstance(w, DOMWidget)]
216 c = [w for w in args_widgets+kwargs_widgets if isinstance(w, DOMWidget)]
218 container.children = c
217 container.children = c
219
218
220 # Build the callback
219 # Build the callback
221 def call_f(name, old, new):
220 def call_f(name, old, new):
222 container.args = []
221 container.args = []
223 for widget in args_widgets:
222 for widget in args_widgets:
224 value = widget.value
223 value = widget.value
225 container.args.append(value)
224 container.args.append(value)
226 for widget in kwargs_widgets:
225 for widget in kwargs_widgets:
227 value = widget.value
226 value = widget.value
228 container.kwargs[widget.description] = value
227 container.kwargs[widget.description] = value
229 if co:
228 if co:
230 clear_output(wait=True)
229 clear_output(wait=True)
231 container.result = f(*container.args, **container.kwargs)
230 container.result = f(*container.args, **container.kwargs)
232
231
233 # Wire up the widgets
232 # Wire up the widgets
234 for widget in args_widgets:
233 for widget in args_widgets:
235 widget.on_trait_change(call_f, 'value')
234 widget.on_trait_change(call_f, 'value')
236 for widget in kwargs_widgets:
235 for widget in kwargs_widgets:
237 widget.on_trait_change(call_f, 'value')
236 widget.on_trait_change(call_f, 'value')
238
237
239 container.on_displayed(lambda _: call_f(None, None, None))
238 container.on_displayed(lambda _: call_f(None, None, None))
240
239
241 return container
240 return container
242
241
243 def interact(*args, **kwargs):
242 def interact(*args, **kwargs):
244 """Interact with a function using widgets."""
243 """Interact with a function using widgets."""
245 if args and callable(args[0]):
244 if args and callable(args[0]):
246 # This branch handles the cases:
245 # This branch handles the cases:
247 # 1. interact(f, *args, **kwargs)
246 # 1. interact(f, *args, **kwargs)
248 # 2. @interact
247 # 2. @interact
249 # def f(*args, **kwargs):
248 # def f(*args, **kwargs):
250 # ...
249 # ...
251 f = args[0]
250 f = args[0]
252 w = interactive(f, *args[1:], **kwargs)
251 w = interactive(f, *args[1:], **kwargs)
253 f.widget = w
252 f.widget = w
254 display(w)
253 display(w)
255 else:
254 else:
256 # This branch handles the case:
255 # This branch handles the case:
257 # @interact(10, 20, a=30, b=40)
256 # @interact(10, 20, a=30, b=40)
258 # def f(*args, **kwargs):
257 # def f(*args, **kwargs):
259 # ...
258 # ...
260 def dec(f):
259 def dec(f):
261 w = interactive(f, *args, **kwargs)
260 w = interactive(f, *args, **kwargs)
262 f.widget = w
261 f.widget = w
263 display(w)
262 display(w)
264 return f
263 return f
265 return dec
264 return dec
266
265
267 class const(HasTraits):
266 class const(HasTraits):
268 """A pseudo-widget whose value is constant and never client synced."""
267 """A pseudo-widget whose value is constant and never client synced."""
269 value = Any(help="Any Python object")
268 value = Any(help="Any Python object")
270 description = Unicode('', help="Any Python object")
269 description = Unicode('', help="Any Python object")
271 def __init__(self, value, **kwargs):
270 def __init__(self, value, **kwargs):
272 super(const, self).__init__(value=value, **kwargs)
271 super(const, self).__init__(value=value, **kwargs)
273
272
274 def annotate(**kwargs):
273 def annotate(**kwargs):
275 """Python 3 compatible function annotation for Python 2."""
274 """Python 3 compatible function annotation for Python 2."""
276 if not kwargs:
275 if not kwargs:
277 raise ValueError('annotations must be provided as keyword arguments')
276 raise ValueError('annotations must be provided as keyword arguments')
278 def dec(f):
277 def dec(f):
279 if hasattr(f, '__annotations__'):
278 if hasattr(f, '__annotations__'):
280 for k, v in kwargs.items():
279 for k, v in kwargs.items():
281 f.__annotations__[k] = v
280 f.__annotations__[k] = v
282 else:
281 else:
283 f.__annotations__ = kwargs
282 f.__annotations__ = kwargs
284 return f
283 return f
285 return dec
284 return dec
286
285
General Comments 0
You need to be logged in to leave comments. Login now