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