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