##// END OF EJS Templates
Merge pull request #5067 from minrk/widget-error...
Brian E. Granger -
r15213:2182b3fe merge
parent child Browse files
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)
@@ -1,423 +1,441 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16
16
17 from IPython.core.getipython import get_ipython
17 from IPython.kernel.comm import Comm
18 from IPython.kernel.comm import Comm
18 from IPython.config import LoggingConfigurable
19 from IPython.config import LoggingConfigurable
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
20 from IPython.utils.py3compat import string_types
21 from IPython.utils.py3compat import string_types
21
22
22 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
23 # Classes
24 # Classes
24 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
25 class CallbackDispatcher(LoggingConfigurable):
26 class CallbackDispatcher(LoggingConfigurable):
26 """A structure for registering and running callbacks"""
27 """A structure for registering and running callbacks"""
27 callbacks = List()
28 callbacks = List()
28
29
29 def __call__(self, *args, **kwargs):
30 def __call__(self, *args, **kwargs):
30 """Call all of the registered callbacks."""
31 """Call all of the registered callbacks."""
31 value = None
32 value = None
32 for callback in self.callbacks:
33 for callback in self.callbacks:
33 try:
34 try:
34 local_value = callback(*args, **kwargs)
35 local_value = callback(*args, **kwargs)
35 except Exception as e:
36 except Exception as e:
36 self.log.warn("Exception in callback %s: %s", callback, e)
37 ip = get_ipython()
38 if ip is None:
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 else:
41 ip.showtraceback()
37 else:
42 else:
38 value = local_value if local_value is not None else value
43 value = local_value if local_value is not None else value
39 return value
44 return value
40
45
41 def register_callback(self, callback, remove=False):
46 def register_callback(self, callback, remove=False):
42 """(Un)Register a callback
47 """(Un)Register a callback
43
48
44 Parameters
49 Parameters
45 ----------
50 ----------
46 callback: method handle
51 callback: method handle
47 Method to be registered or unregistered.
52 Method to be registered or unregistered.
48 remove=False: bool
53 remove=False: bool
49 Whether to unregister the callback."""
54 Whether to unregister the callback."""
50
55
51 # (Un)Register the callback.
56 # (Un)Register the callback.
52 if remove and callback in self.callbacks:
57 if remove and callback in self.callbacks:
53 self.callbacks.remove(callback)
58 self.callbacks.remove(callback)
54 elif not remove and callback not in self.callbacks:
59 elif not remove and callback not in self.callbacks:
55 self.callbacks.append(callback)
60 self.callbacks.append(callback)
56
61
62 def _show_traceback(method):
63 """decorator for showing tracebacks in IPython"""
64 def m(self, *args, **kwargs):
65 try:
66 return(method(self, *args, **kwargs))
67 except Exception as e:
68 ip = get_ipython()
69 if ip is None:
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 else:
72 ip.showtraceback()
73 return m
57
74
58 class Widget(LoggingConfigurable):
75 class Widget(LoggingConfigurable):
59 #-------------------------------------------------------------------------
76 #-------------------------------------------------------------------------
60 # Class attributes
77 # Class attributes
61 #-------------------------------------------------------------------------
78 #-------------------------------------------------------------------------
62 _widget_construction_callback = None
79 _widget_construction_callback = None
63 widgets = {}
80 widgets = {}
64
81
65 @staticmethod
82 @staticmethod
66 def on_widget_constructed(callback):
83 def on_widget_constructed(callback):
67 """Registers a callback to be called when a widget is constructed.
84 """Registers a callback to be called when a widget is constructed.
68
85
69 The callback must have the following signature:
86 The callback must have the following signature:
70 callback(widget)"""
87 callback(widget)"""
71 Widget._widget_construction_callback = callback
88 Widget._widget_construction_callback = callback
72
89
73 @staticmethod
90 @staticmethod
74 def _call_widget_constructed(widget):
91 def _call_widget_constructed(widget):
75 """Static method, called when a widget is constructed."""
92 """Static method, called when a widget is constructed."""
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
77 Widget._widget_construction_callback(widget)
94 Widget._widget_construction_callback(widget)
78
95
79 #-------------------------------------------------------------------------
96 #-------------------------------------------------------------------------
80 # Traits
97 # Traits
81 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
82 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
83 registered in the front-end to create and sync this widget with.""")
100 registered in the front-end to create and sync this widget with.""")
84 _view_name = Unicode(help="""Default view registered in the front-end
101 _view_name = Unicode(help="""Default view registered in the front-end
85 to use to represent the widget.""", sync=True)
102 to use to represent the widget.""", sync=True)
86 _comm = Instance('IPython.kernel.comm.Comm')
103 _comm = Instance('IPython.kernel.comm.Comm')
87
104
88 closed = Bool(False)
105 closed = Bool(False)
89
106
90 keys = List()
107 keys = List()
91 def _keys_default(self):
108 def _keys_default(self):
92 return [name for name in self.traits(sync=True)]
109 return [name for name in self.traits(sync=True)]
93
110
94 _property_lock = Tuple((None, None))
111 _property_lock = Tuple((None, None))
95
112
96 _display_callbacks = Instance(CallbackDispatcher, ())
113 _display_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
114 _msg_callbacks = Instance(CallbackDispatcher, ())
98
115
99 #-------------------------------------------------------------------------
116 #-------------------------------------------------------------------------
100 # (Con/de)structor
117 # (Con/de)structor
101 #-------------------------------------------------------------------------
118 #-------------------------------------------------------------------------
102 def __init__(self, **kwargs):
119 def __init__(self, **kwargs):
103 """Public constructor"""
120 """Public constructor"""
104 super(Widget, self).__init__(**kwargs)
121 super(Widget, self).__init__(**kwargs)
105
122
106 self.on_trait_change(self._handle_property_changed, self.keys)
123 self.on_trait_change(self._handle_property_changed, self.keys)
107 Widget._call_widget_constructed(self)
124 Widget._call_widget_constructed(self)
108
125
109 def __del__(self):
126 def __del__(self):
110 """Object disposal"""
127 """Object disposal"""
111 self.close()
128 self.close()
112
129
113 #-------------------------------------------------------------------------
130 #-------------------------------------------------------------------------
114 # Properties
131 # Properties
115 #-------------------------------------------------------------------------
132 #-------------------------------------------------------------------------
116
133
117 @property
134 @property
118 def comm(self):
135 def comm(self):
119 """Gets the Comm associated with this widget.
136 """Gets the Comm associated with this widget.
120
137
121 If a Comm doesn't exist yet, a Comm will be created automagically."""
138 If a Comm doesn't exist yet, a Comm will be created automagically."""
122 if self._comm is None:
139 if self._comm is None:
123 # Create a comm.
140 # Create a comm.
124 self._comm = Comm(target_name=self._model_name)
141 self._comm = Comm(target_name=self._model_name)
125 self._comm.on_msg(self._handle_msg)
142 self._comm.on_msg(self._handle_msg)
126 self._comm.on_close(self._close)
143 self._comm.on_close(self._close)
127 Widget.widgets[self.model_id] = self
144 Widget.widgets[self.model_id] = self
128
145
129 # first update
146 # first update
130 self.send_state()
147 self.send_state()
131 return self._comm
148 return self._comm
132
149
133 @property
150 @property
134 def model_id(self):
151 def model_id(self):
135 """Gets the model id of this widget.
152 """Gets the model id of this widget.
136
153
137 If a Comm doesn't exist yet, a Comm will be created automagically."""
154 If a Comm doesn't exist yet, a Comm will be created automagically."""
138 return self.comm.comm_id
155 return self.comm.comm_id
139
156
140 #-------------------------------------------------------------------------
157 #-------------------------------------------------------------------------
141 # Methods
158 # Methods
142 #-------------------------------------------------------------------------
159 #-------------------------------------------------------------------------
143 def _close(self):
160 def _close(self):
144 """Private close - cleanup objects, registry entries"""
161 """Private close - cleanup objects, registry entries"""
145 del Widget.widgets[self.model_id]
162 del Widget.widgets[self.model_id]
146 self._comm = None
163 self._comm = None
147 self.closed = True
164 self.closed = True
148
165
149 def close(self):
166 def close(self):
150 """Close method.
167 """Close method.
151
168
152 Closes the widget which closes the underlying comm.
169 Closes the widget which closes the underlying comm.
153 When the comm is closed, all of the widget views are automatically
170 When the comm is closed, all of the widget views are automatically
154 removed from the front-end."""
171 removed from the front-end."""
155 if not self.closed:
172 if not self.closed:
156 self._comm.close()
173 self._comm.close()
157 self._close()
174 self._close()
158
175
159 def send_state(self, key=None):
176 def send_state(self, key=None):
160 """Sends the widget state, or a piece of it, to the front-end.
177 """Sends the widget state, or a piece of it, to the front-end.
161
178
162 Parameters
179 Parameters
163 ----------
180 ----------
164 key : unicode (optional)
181 key : unicode (optional)
165 A single property's name to sync with the front-end.
182 A single property's name to sync with the front-end.
166 """
183 """
167 self._send({
184 self._send({
168 "method" : "update",
185 "method" : "update",
169 "state" : self.get_state()
186 "state" : self.get_state()
170 })
187 })
171
188
172 def get_state(self, key=None):
189 def get_state(self, key=None):
173 """Gets the widget state, or a piece of it.
190 """Gets the widget state, or a piece of it.
174
191
175 Parameters
192 Parameters
176 ----------
193 ----------
177 key : unicode (optional)
194 key : unicode (optional)
178 A single property's name to get.
195 A single property's name to get.
179 """
196 """
180 keys = self.keys if key is None else [key]
197 keys = self.keys if key is None else [key]
181 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
198 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
182
199
183 def send(self, content):
200 def send(self, content):
184 """Sends a custom msg to the widget model in the front-end.
201 """Sends a custom msg to the widget model in the front-end.
185
202
186 Parameters
203 Parameters
187 ----------
204 ----------
188 content : dict
205 content : dict
189 Content of the message to send.
206 Content of the message to send.
190 """
207 """
191 self._send({"method": "custom", "content": content})
208 self._send({"method": "custom", "content": content})
192
209
193 def on_msg(self, callback, remove=False):
210 def on_msg(self, callback, remove=False):
194 """(Un)Register a custom msg receive callback.
211 """(Un)Register a custom msg receive callback.
195
212
196 Parameters
213 Parameters
197 ----------
214 ----------
198 callback: callable
215 callback: callable
199 callback will be passed two arguments when a message arrives::
216 callback will be passed two arguments when a message arrives::
200
217
201 callback(widget, content)
218 callback(widget, content)
202
219
203 remove: bool
220 remove: bool
204 True if the callback should be unregistered."""
221 True if the callback should be unregistered."""
205 self._msg_callbacks.register_callback(callback, remove=remove)
222 self._msg_callbacks.register_callback(callback, remove=remove)
206
223
207 def on_displayed(self, callback, remove=False):
224 def on_displayed(self, callback, remove=False):
208 """(Un)Register a widget displayed callback.
225 """(Un)Register a widget displayed callback.
209
226
210 Parameters
227 Parameters
211 ----------
228 ----------
212 callback: method handler
229 callback: method handler
213 Must have a signature of::
230 Must have a signature of::
214
231
215 callback(widget, **kwargs)
232 callback(widget, **kwargs)
216
233
217 kwargs from display are passed through without modification.
234 kwargs from display are passed through without modification.
218 remove: bool
235 remove: bool
219 True if the callback should be unregistered."""
236 True if the callback should be unregistered."""
220 self._display_callbacks.register_callback(callback, remove=remove)
237 self._display_callbacks.register_callback(callback, remove=remove)
221
238
222 #-------------------------------------------------------------------------
239 #-------------------------------------------------------------------------
223 # Support methods
240 # Support methods
224 #-------------------------------------------------------------------------
241 #-------------------------------------------------------------------------
225 @contextmanager
242 @contextmanager
226 def _lock_property(self, key, value):
243 def _lock_property(self, key, value):
227 """Lock a property-value pair.
244 """Lock a property-value pair.
228
245
229 NOTE: This, in addition to the single lock for all state changes, is
246 NOTE: This, in addition to the single lock for all state changes, is
230 flawed. In the future we may want to look into buffering state changes
247 flawed. In the future we may want to look into buffering state changes
231 back to the front-end."""
248 back to the front-end."""
232 self._property_lock = (key, value)
249 self._property_lock = (key, value)
233 try:
250 try:
234 yield
251 yield
235 finally:
252 finally:
236 self._property_lock = (None, None)
253 self._property_lock = (None, None)
237
254
238 def _should_send_property(self, key, value):
255 def _should_send_property(self, key, value):
239 """Check the property lock (property_lock)"""
256 """Check the property lock (property_lock)"""
240 return key != self._property_lock[0] or \
257 return key != self._property_lock[0] or \
241 value != self._property_lock[1]
258 value != self._property_lock[1]
242
259
243 # Event handlers
260 # Event handlers
261 @_show_traceback
244 def _handle_msg(self, msg):
262 def _handle_msg(self, msg):
245 """Called when a msg is received from the front-end"""
263 """Called when a msg is received from the front-end"""
246 data = msg['content']['data']
264 data = msg['content']['data']
247 method = data['method']
265 method = data['method']
248 if not method in ['backbone', 'custom']:
266 if not method in ['backbone', 'custom']:
249 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
267 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
250
268
251 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
269 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
252 if method == 'backbone' and 'sync_data' in data:
270 if method == 'backbone' and 'sync_data' in data:
253 sync_data = data['sync_data']
271 sync_data = data['sync_data']
254 self._handle_receive_state(sync_data) # handles all methods
272 self._handle_receive_state(sync_data) # handles all methods
255
273
256 # Handle a custom msg from the front-end
274 # Handle a custom msg from the front-end
257 elif method == 'custom':
275 elif method == 'custom':
258 if 'content' in data:
276 if 'content' in data:
259 self._handle_custom_msg(data['content'])
277 self._handle_custom_msg(data['content'])
260
278
261 def _handle_receive_state(self, sync_data):
279 def _handle_receive_state(self, sync_data):
262 """Called when a state is received from the front-end."""
280 """Called when a state is received from the front-end."""
263 for name in self.keys:
281 for name in self.keys:
264 if name in sync_data:
282 if name in sync_data:
265 value = self._unpack_widgets(sync_data[name])
283 value = self._unpack_widgets(sync_data[name])
266 with self._lock_property(name, value):
284 with self._lock_property(name, value):
267 setattr(self, name, value)
285 setattr(self, name, value)
268
286
269 def _handle_custom_msg(self, content):
287 def _handle_custom_msg(self, content):
270 """Called when a custom msg is received."""
288 """Called when a custom msg is received."""
271 self._msg_callbacks(self, content)
289 self._msg_callbacks(self, content)
272
290
273 def _handle_property_changed(self, name, old, new):
291 def _handle_property_changed(self, name, old, new):
274 """Called when a property has been changed."""
292 """Called when a property has been changed."""
275 # Make sure this isn't information that the front-end just sent us.
293 # Make sure this isn't information that the front-end just sent us.
276 if self._should_send_property(name, new):
294 if self._should_send_property(name, new):
277 # Send new state to front-end
295 # Send new state to front-end
278 self.send_state(key=name)
296 self.send_state(key=name)
279
297
280 def _handle_displayed(self, **kwargs):
298 def _handle_displayed(self, **kwargs):
281 """Called when a view has been displayed for this widget instance"""
299 """Called when a view has been displayed for this widget instance"""
282 self._display_callbacks(self, **kwargs)
300 self._display_callbacks(self, **kwargs)
283
301
284 def _pack_widgets(self, x):
302 def _pack_widgets(self, x):
285 """Recursively converts all widget instances to model id strings.
303 """Recursively converts all widget instances to model id strings.
286
304
287 Children widgets will be stored and transmitted to the front-end by
305 Children widgets will be stored and transmitted to the front-end by
288 their model ids. Return value must be JSON-able."""
306 their model ids. Return value must be JSON-able."""
289 if isinstance(x, dict):
307 if isinstance(x, dict):
290 return {k: self._pack_widgets(v) for k, v in x.items()}
308 return {k: self._pack_widgets(v) for k, v in x.items()}
291 elif isinstance(x, list):
309 elif isinstance(x, list):
292 return [self._pack_widgets(v) for v in x]
310 return [self._pack_widgets(v) for v in x]
293 elif isinstance(x, Widget):
311 elif isinstance(x, Widget):
294 return x.model_id
312 return x.model_id
295 else:
313 else:
296 return x # Value must be JSON-able
314 return x # Value must be JSON-able
297
315
298 def _unpack_widgets(self, x):
316 def _unpack_widgets(self, x):
299 """Recursively converts all model id strings to widget instances.
317 """Recursively converts all model id strings to widget instances.
300
318
301 Children widgets will be stored and transmitted to the front-end by
319 Children widgets will be stored and transmitted to the front-end by
302 their model ids."""
320 their model ids."""
303 if isinstance(x, dict):
321 if isinstance(x, dict):
304 return {k: self._unpack_widgets(v) for k, v in x.items()}
322 return {k: self._unpack_widgets(v) for k, v in x.items()}
305 elif isinstance(x, list):
323 elif isinstance(x, list):
306 return [self._unpack_widgets(v) for v in x]
324 return [self._unpack_widgets(v) for v in x]
307 elif isinstance(x, string_types):
325 elif isinstance(x, string_types):
308 return x if x not in Widget.widgets else Widget.widgets[x]
326 return x if x not in Widget.widgets else Widget.widgets[x]
309 else:
327 else:
310 return x
328 return x
311
329
312 def _ipython_display_(self, **kwargs):
330 def _ipython_display_(self, **kwargs):
313 """Called when `IPython.display.display` is called on the widget."""
331 """Called when `IPython.display.display` is called on the widget."""
314 # Show view. By sending a display message, the comm is opened and the
332 # Show view. By sending a display message, the comm is opened and the
315 # initial state is sent.
333 # initial state is sent.
316 self._send({"method": "display"})
334 self._send({"method": "display"})
317 self._handle_displayed(**kwargs)
335 self._handle_displayed(**kwargs)
318
336
319 def _send(self, msg):
337 def _send(self, msg):
320 """Sends a message to the model in the front-end."""
338 """Sends a message to the model in the front-end."""
321 self.comm.send(msg)
339 self.comm.send(msg)
322
340
323
341
324 class DOMWidget(Widget):
342 class DOMWidget(Widget):
325 visible = Bool(True, help="Whether the widget is visible.", sync=True)
343 visible = Bool(True, help="Whether the widget is visible.", sync=True)
326 _css = Dict(sync=True) # Internal CSS property dict
344 _css = Dict(sync=True) # Internal CSS property dict
327
345
328 def get_css(self, key, selector=""):
346 def get_css(self, key, selector=""):
329 """Get a CSS property of the widget.
347 """Get a CSS property of the widget.
330
348
331 Note: This function does not actually request the CSS from the
349 Note: This function does not actually request the CSS from the
332 front-end; Only properties that have been set with set_css can be read.
350 front-end; Only properties that have been set with set_css can be read.
333
351
334 Parameters
352 Parameters
335 ----------
353 ----------
336 key: unicode
354 key: unicode
337 CSS key
355 CSS key
338 selector: unicode (optional)
356 selector: unicode (optional)
339 JQuery selector used when the CSS key/value was set.
357 JQuery selector used when the CSS key/value was set.
340 """
358 """
341 if selector in self._css and key in self._css[selector]:
359 if selector in self._css and key in self._css[selector]:
342 return self._css[selector][key]
360 return self._css[selector][key]
343 else:
361 else:
344 return None
362 return None
345
363
346 def set_css(self, dict_or_key, value=None, selector=''):
364 def set_css(self, dict_or_key, value=None, selector=''):
347 """Set one or more CSS properties of the widget.
365 """Set one or more CSS properties of the widget.
348
366
349 This function has two signatures:
367 This function has two signatures:
350 - set_css(css_dict, selector='')
368 - set_css(css_dict, selector='')
351 - set_css(key, value, selector='')
369 - set_css(key, value, selector='')
352
370
353 Parameters
371 Parameters
354 ----------
372 ----------
355 css_dict : dict
373 css_dict : dict
356 CSS key/value pairs to apply
374 CSS key/value pairs to apply
357 key: unicode
375 key: unicode
358 CSS key
376 CSS key
359 value:
377 value:
360 CSS value
378 CSS value
361 selector: unicode (optional, kwarg only)
379 selector: unicode (optional, kwarg only)
362 JQuery selector to use to apply the CSS key/value. If no selector
380 JQuery selector to use to apply the CSS key/value. If no selector
363 is provided, an empty selector is used. An empty selector makes the
381 is provided, an empty selector is used. An empty selector makes the
364 front-end try to apply the css to a default element. The default
382 front-end try to apply the css to a default element. The default
365 element is an attribute unique to each view, which is a DOM element
383 element is an attribute unique to each view, which is a DOM element
366 of the view that should be styled with common CSS (see
384 of the view that should be styled with common CSS (see
367 `$el_to_style` in the Javascript code).
385 `$el_to_style` in the Javascript code).
368 """
386 """
369 if not selector in self._css:
387 if not selector in self._css:
370 self._css[selector] = {}
388 self._css[selector] = {}
371 my_css = self._css[selector]
389 my_css = self._css[selector]
372
390
373 if value is None:
391 if value is None:
374 css_dict = dict_or_key
392 css_dict = dict_or_key
375 else:
393 else:
376 css_dict = {dict_or_key: value}
394 css_dict = {dict_or_key: value}
377
395
378 for (key, value) in css_dict.items():
396 for (key, value) in css_dict.items():
379 if not (key in my_css and value == my_css[key]):
397 if not (key in my_css and value == my_css[key]):
380 my_css[key] = value
398 my_css[key] = value
381 self.send_state('_css')
399 self.send_state('_css')
382
400
383 def add_class(self, class_names, selector=""):
401 def add_class(self, class_names, selector=""):
384 """Add class[es] to a DOM element.
402 """Add class[es] to a DOM element.
385
403
386 Parameters
404 Parameters
387 ----------
405 ----------
388 class_names: unicode or list
406 class_names: unicode or list
389 Class name(s) to add to the DOM element(s).
407 Class name(s) to add to the DOM element(s).
390 selector: unicode (optional)
408 selector: unicode (optional)
391 JQuery selector to select the DOM element(s) that the class(es) will
409 JQuery selector to select the DOM element(s) that the class(es) will
392 be added to.
410 be added to.
393 """
411 """
394 class_list = class_names
412 class_list = class_names
395 if isinstance(class_list, list):
413 if isinstance(class_list, list):
396 class_list = ' '.join(class_list)
414 class_list = ' '.join(class_list)
397
415
398 self.send({
416 self.send({
399 "msg_type" : "add_class",
417 "msg_type" : "add_class",
400 "class_list" : class_list,
418 "class_list" : class_list,
401 "selector" : selector
419 "selector" : selector
402 })
420 })
403
421
404 def remove_class(self, class_names, selector=""):
422 def remove_class(self, class_names, selector=""):
405 """Remove class[es] from a DOM element.
423 """Remove class[es] from a DOM element.
406
424
407 Parameters
425 Parameters
408 ----------
426 ----------
409 class_names: unicode or list
427 class_names: unicode or list
410 Class name(s) to remove from the DOM element(s).
428 Class name(s) to remove from the DOM element(s).
411 selector: unicode (optional)
429 selector: unicode (optional)
412 JQuery selector to select the DOM element(s) that the class(es) will
430 JQuery selector to select the DOM element(s) that the class(es) will
413 be removed from.
431 be removed from.
414 """
432 """
415 class_list = class_names
433 class_list = class_names
416 if isinstance(class_list, list):
434 if isinstance(class_list, list):
417 class_list = ' '.join(class_list)
435 class_list = ' '.join(class_list)
418
436
419 self.send({
437 self.send({
420 "msg_type" : "remove_class",
438 "msg_type" : "remove_class",
421 "class_list" : class_list,
439 "class_list" : class_list,
422 "selector" : selector,
440 "selector" : selector,
423 })
441 })
General Comments 0
You need to be logged in to leave comments. Login now