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