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