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