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