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