##// END OF EJS Templates
catch errors at a lower level in interact...
MinRK -
Show More
@@ -1,251 +1,259 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.core.getipython import get_ipython
23 from IPython.html.widgets import (Widget, TextWidget,
24 from IPython.html.widgets import (Widget, TextWidget,
24 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
25 FloatSliderWidget, IntSliderWidget, CheckboxWidget, DropdownWidget,
25 ContainerWidget, DOMWidget)
26 ContainerWidget, DOMWidget)
26 from IPython.display import display, clear_output
27 from IPython.display import display, clear_output
27 from IPython.utils.py3compat import string_types, unicode_type
28 from IPython.utils.py3compat import string_types, unicode_type
28 from IPython.utils.traitlets import HasTraits, Any, Unicode
29 from IPython.utils.traitlets import HasTraits, Any, Unicode
29
30
30 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
31 # Classes and Functions
32 # Classes and Functions
32 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
33
34
34
35
35 def _matches(o, pattern):
36 def _matches(o, pattern):
36 """Match a pattern of types in a sequence."""
37 """Match a pattern of types in a sequence."""
37 if not len(o) == len(pattern):
38 if not len(o) == len(pattern):
38 return False
39 return False
39 comps = zip(o,pattern)
40 comps = zip(o,pattern)
40 return all(isinstance(obj,kind) for obj,kind in comps)
41 return all(isinstance(obj,kind) for obj,kind in comps)
41
42
42
43
43 def _get_min_max_value(min, max, value=None, step=None):
44 def _get_min_max_value(min, max, value=None, step=None):
44 """Return min, max, value given input values with possible None."""
45 """Return min, max, value given input values with possible None."""
45 if value is None:
46 if value is None:
46 if not max > min:
47 if not max > min:
47 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
48 raise ValueError('max must be greater than min: (min={0}, max={1})'.format(min, max))
48 value = min + abs(min-max)/2
49 value = min + abs(min-max)/2
49 value = type(min)(value)
50 value = type(min)(value)
50 elif min is None and max is None:
51 elif min is None and max is None:
51 if value == 0.0:
52 if value == 0.0:
52 min, max, value = 0.0, 1.0, 0.5
53 min, max, value = 0.0, 1.0, 0.5
53 elif value == 0:
54 elif value == 0:
54 min, max, value = 0, 1, 0
55 min, max, value = 0, 1, 0
55 elif isinstance(value, (int, float)):
56 elif isinstance(value, (int, float)):
56 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)
57 else:
58 else:
58 raise TypeError('expected a number, got: %r' % value)
59 raise TypeError('expected a number, got: %r' % value)
59 else:
60 else:
60 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))
61 if step is not None:
62 if step is not None:
62 # ensure value is on a step
63 # ensure value is on a step
63 r = (value - min) % step
64 r = (value - min) % step
64 value = value - r
65 value = value - r
65 return min, max, value
66 return min, max, value
66
67
67 def _widget_abbrev_single_value(o):
68 def _widget_abbrev_single_value(o):
68 """Make widgets from single values, which can be used as parameter defaults."""
69 """Make widgets from single values, which can be used as parameter defaults."""
69 if isinstance(o, string_types):
70 if isinstance(o, string_types):
70 return TextWidget(value=unicode_type(o))
71 return TextWidget(value=unicode_type(o))
71 elif isinstance(o, dict):
72 elif isinstance(o, dict):
72 return DropdownWidget(values=o)
73 return DropdownWidget(values=o)
73 elif isinstance(o, bool):
74 elif isinstance(o, bool):
74 return CheckboxWidget(value=o)
75 return CheckboxWidget(value=o)
75 elif isinstance(o, float):
76 elif isinstance(o, float):
76 min, max, value = _get_min_max_value(None, None, o)
77 min, max, value = _get_min_max_value(None, None, o)
77 return FloatSliderWidget(value=o, min=min, max=max)
78 return FloatSliderWidget(value=o, min=min, max=max)
78 elif isinstance(o, int):
79 elif isinstance(o, int):
79 min, max, value = _get_min_max_value(None, None, o)
80 min, max, value = _get_min_max_value(None, None, o)
80 return IntSliderWidget(value=o, min=min, max=max)
81 return IntSliderWidget(value=o, min=min, max=max)
81 else:
82 else:
82 return None
83 return None
83
84
84 def _widget_abbrev(o):
85 def _widget_abbrev(o):
85 """Make widgets from abbreviations: single values, lists or tuples."""
86 """Make widgets from abbreviations: single values, lists or tuples."""
86 float_or_int = (float, int)
87 float_or_int = (float, int)
87 if isinstance(o, (list, tuple)):
88 if isinstance(o, (list, tuple)):
88 if o and all(isinstance(x, string_types) for x in o):
89 if o and all(isinstance(x, string_types) for x in o):
89 return DropdownWidget(values=[unicode_type(k) for k in o])
90 return DropdownWidget(values=[unicode_type(k) for k in o])
90 elif _matches(o, (float_or_int, float_or_int)):
91 elif _matches(o, (float_or_int, float_or_int)):
91 min, max, value = _get_min_max_value(o[0], o[1])
92 min, max, value = _get_min_max_value(o[0], o[1])
92 if all(isinstance(_, int) for _ in o):
93 if all(isinstance(_, int) for _ in o):
93 cls = IntSliderWidget
94 cls = IntSliderWidget
94 else:
95 else:
95 cls = FloatSliderWidget
96 cls = FloatSliderWidget
96 return cls(value=value, min=min, max=max)
97 return cls(value=value, min=min, max=max)
97 elif _matches(o, (float_or_int, float_or_int, float_or_int)):
98 elif _matches(o, (float_or_int, float_or_int, float_or_int)):
98 step = o[2]
99 step = o[2]
99 if step <= 0:
100 if step <= 0:
100 raise ValueError("step must be >= 0, not %r" % step)
101 raise ValueError("step must be >= 0, not %r" % step)
101 min, max, value = _get_min_max_value(o[0], o[1], step=step)
102 min, max, value = _get_min_max_value(o[0], o[1], step=step)
102 if all(isinstance(_, int) for _ in o):
103 if all(isinstance(_, int) for _ in o):
103 cls = IntSliderWidget
104 cls = IntSliderWidget
104 else:
105 else:
105 cls = FloatSliderWidget
106 cls = FloatSliderWidget
106 return cls(value=value, min=min, max=max, step=step)
107 return cls(value=value, min=min, max=max, step=step)
107 else:
108 else:
108 return _widget_abbrev_single_value(o)
109 return _widget_abbrev_single_value(o)
109
110
110 def _widget_from_abbrev(abbrev):
111 def _widget_from_abbrev(abbrev):
111 """Build a Widget intstance given an abbreviation or Widget."""
112 """Build a Widget intstance given an abbreviation or Widget."""
112 if isinstance(abbrev, Widget) or isinstance(abbrev, fixed):
113 if isinstance(abbrev, Widget) or isinstance(abbrev, fixed):
113 return abbrev
114 return abbrev
114
115
115 widget = _widget_abbrev(abbrev)
116 widget = _widget_abbrev(abbrev)
116 if widget is None:
117 if widget is None:
117 raise ValueError("%r cannot be transformed to a Widget" % (abbrev,))
118 raise ValueError("%r cannot be transformed to a Widget" % (abbrev,))
118 return widget
119 return widget
119
120
120 def _yield_abbreviations_for_parameter(param, kwargs):
121 def _yield_abbreviations_for_parameter(param, kwargs):
121 """Get an abbreviation for a function parameter."""
122 """Get an abbreviation for a function parameter."""
122 name = param.name
123 name = param.name
123 kind = param.kind
124 kind = param.kind
124 ann = param.annotation
125 ann = param.annotation
125 default = param.default
126 default = param.default
126 empty = Parameter.empty
127 empty = Parameter.empty
127 not_found = (None, None)
128 not_found = (None, None)
128 if kind == Parameter.POSITIONAL_OR_KEYWORD:
129 if kind == Parameter.POSITIONAL_OR_KEYWORD:
129 if name in kwargs:
130 if name in kwargs:
130 yield name, kwargs.pop(name)
131 yield name, kwargs.pop(name)
131 elif ann is not empty:
132 elif ann is not empty:
132 if default is empty:
133 if default is empty:
133 yield name, ann
134 yield name, ann
134 else:
135 else:
135 yield name, ann
136 yield name, ann
136 elif default is not empty:
137 elif default is not empty:
137 yield name, default
138 yield name, default
138 else:
139 else:
139 yield not_found
140 yield not_found
140 elif kind == Parameter.KEYWORD_ONLY:
141 elif kind == Parameter.KEYWORD_ONLY:
141 if name in kwargs:
142 if name in kwargs:
142 yield name, kwargs.pop(name)
143 yield name, kwargs.pop(name)
143 elif ann is not empty:
144 elif ann is not empty:
144 yield name, ann
145 yield name, ann
145 elif default is not empty:
146 elif default is not empty:
146 yield name, default
147 yield name, default
147 else:
148 else:
148 yield not_found
149 yield not_found
149 elif kind == Parameter.VAR_KEYWORD:
150 elif kind == Parameter.VAR_KEYWORD:
150 # In this case name=kwargs and we yield the items in kwargs with their keys.
151 # In this case name=kwargs and we yield the items in kwargs with their keys.
151 for k, v in kwargs.copy().items():
152 for k, v in kwargs.copy().items():
152 kwargs.pop(k)
153 kwargs.pop(k)
153 yield k, v
154 yield k, v
154
155
155 def _find_abbreviations(f, kwargs):
156 def _find_abbreviations(f, kwargs):
156 """Find the abbreviations for a function and kwargs passed to interact."""
157 """Find the abbreviations for a function and kwargs passed to interact."""
157 new_kwargs = []
158 new_kwargs = []
158 for param in signature(f).parameters.values():
159 for param in signature(f).parameters.values():
159 for name, value in _yield_abbreviations_for_parameter(param, kwargs):
160 for name, value in _yield_abbreviations_for_parameter(param, kwargs):
160 if value is None:
161 if value is None:
161 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
162 raise ValueError('cannot find widget or abbreviation for argument: {!r}'.format(name))
162 new_kwargs.append((name, value))
163 new_kwargs.append((name, value))
163 return new_kwargs
164 return new_kwargs
164
165
165 def _widgets_from_abbreviations(seq):
166 def _widgets_from_abbreviations(seq):
166 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
167 """Given a sequence of (name, abbrev) tuples, return a sequence of Widgets."""
167 result = []
168 result = []
168 for name, abbrev in seq:
169 for name, abbrev in seq:
169 widget = _widget_from_abbrev(abbrev)
170 widget = _widget_from_abbrev(abbrev)
170 widget.description = name
171 widget.description = name
171 result.append(widget)
172 result.append(widget)
172 return result
173 return result
173
174
174 def interactive(__interact_f, **kwargs):
175 def interactive(__interact_f, **kwargs):
175 """Build a group of widgets to interact with a function."""
176 """Build a group of widgets to interact with a function."""
176 f = __interact_f
177 f = __interact_f
177 co = kwargs.pop('clear_output', True)
178 co = kwargs.pop('clear_output', True)
178 kwargs_widgets = []
179 kwargs_widgets = []
179 container = ContainerWidget()
180 container = ContainerWidget()
180 container.result = None
181 container.result = None
181 container.args = []
182 container.args = []
182 container.kwargs = dict()
183 container.kwargs = dict()
183 kwargs = kwargs.copy()
184 kwargs = kwargs.copy()
184
185
185 new_kwargs = _find_abbreviations(f, kwargs)
186 new_kwargs = _find_abbreviations(f, kwargs)
186 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
187 # Before we proceed, let's make sure that the user has passed a set of args+kwargs
187 # that will lead to a valid call of the function. This protects against unspecified
188 # that will lead to a valid call of the function. This protects against unspecified
188 # and doubly-specified arguments.
189 # and doubly-specified arguments.
189 getcallargs(f, **{n:v for n,v in new_kwargs})
190 getcallargs(f, **{n:v for n,v in new_kwargs})
190 # Now build the widgets from the abbreviations.
191 # Now build the widgets from the abbreviations.
191 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
192 kwargs_widgets.extend(_widgets_from_abbreviations(new_kwargs))
192 kwargs_widgets.extend(_widgets_from_abbreviations(sorted(kwargs.items(), key = lambda x: x[0])))
193 kwargs_widgets.extend(_widgets_from_abbreviations(sorted(kwargs.items(), key = lambda x: x[0])))
193
194
194 # This has to be done as an assignment, not using container.children.append,
195 # This has to be done as an assignment, not using container.children.append,
195 # so that traitlets notices the update. We skip any objects (such as fixed) that
196 # so that traitlets notices the update. We skip any objects (such as fixed) that
196 # are not DOMWidgets.
197 # are not DOMWidgets.
197 c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)]
198 c = [w for w in kwargs_widgets if isinstance(w, DOMWidget)]
198 container.children = c
199 container.children = c
199
200
200 # Build the callback
201 # Build the callback
201 def call_f(name, old, new):
202 def call_f(name, old, new):
202 container.kwargs = {}
203 container.kwargs = {}
203 for widget in kwargs_widgets:
204 for widget in kwargs_widgets:
204 value = widget.value
205 value = widget.value
205 container.kwargs[widget.description] = value
206 container.kwargs[widget.description] = value
206 if co:
207 if co:
207 clear_output(wait=True)
208 clear_output(wait=True)
208 container.result = f(**container.kwargs)
209 try:
210 container.result = f(**container.kwargs)
211 except Exception as e:
212 ip = get_ipython()
213 if ip is None:
214 container.log.warn("Exception in interact callback: %s", e, exc_info=True)
215 else:
216 ip.showtraceback()
209
217
210 # Wire up the widgets
218 # Wire up the widgets
211 for widget in kwargs_widgets:
219 for widget in kwargs_widgets:
212 widget.on_trait_change(call_f, 'value')
220 widget.on_trait_change(call_f, 'value')
213
221
214 container.on_displayed(lambda _: call_f(None, None, None))
222 container.on_displayed(lambda _: call_f(None, None, None))
215
223
216 return container
224 return container
217
225
218 def interact(__interact_f=None, **kwargs):
226 def interact(__interact_f=None, **kwargs):
219 """interact(f, **kwargs)
227 """interact(f, **kwargs)
220
228
221 Interact with a function using widgets."""
229 Interact with a function using widgets."""
222 # positional arg support in: https://gist.github.com/8851331
230 # positional arg support in: https://gist.github.com/8851331
223 if __interact_f is not None:
231 if __interact_f is not None:
224 # This branch handles the cases:
232 # This branch handles the cases:
225 # 1. interact(f, **kwargs)
233 # 1. interact(f, **kwargs)
226 # 2. @interact
234 # 2. @interact
227 # def f(*args, **kwargs):
235 # def f(*args, **kwargs):
228 # ...
236 # ...
229 f = __interact_f
237 f = __interact_f
230 w = interactive(f, **kwargs)
238 w = interactive(f, **kwargs)
231 f.widget = w
239 f.widget = w
232 display(w)
240 display(w)
233 return f
241 return f
234 else:
242 else:
235 # This branch handles the case:
243 # This branch handles the case:
236 # @interact(a=30, b=40)
244 # @interact(a=30, b=40)
237 # def f(*args, **kwargs):
245 # def f(*args, **kwargs):
238 # ...
246 # ...
239 def dec(f):
247 def dec(f):
240 w = interactive(f, **kwargs)
248 w = interactive(f, **kwargs)
241 f.widget = w
249 f.widget = w
242 display(w)
250 display(w)
243 return f
251 return f
244 return dec
252 return dec
245
253
246 class fixed(HasTraits):
254 class fixed(HasTraits):
247 """A pseudo-widget whose value is fixed and never synced to the client."""
255 """A pseudo-widget whose value is fixed and never synced to the client."""
248 value = Any(help="Any Python object")
256 value = Any(help="Any Python object")
249 description = Unicode('', help="Any Python object")
257 description = Unicode('', help="Any Python object")
250 def __init__(self, value, **kwargs):
258 def __init__(self, value, **kwargs):
251 super(fixed, self).__init__(value=value, **kwargs)
259 super(fixed, self).__init__(value=value, **kwargs)
General Comments 0
You need to be logged in to leave comments. Login now