##// END OF EJS Templates
Change serialize/deserialize to to_json/from_json
Jason Grout -
Show More
@@ -1,453 +1,453 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 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
127 127 def __del__(self):
128 128 """Object disposal"""
129 129 self.close()
130 130
131 131 #-------------------------------------------------------------------------
132 132 # Properties
133 133 #-------------------------------------------------------------------------
134 134
135 135 @property
136 136 def comm(self):
137 137 """Gets the Comm associated with this widget.
138 138
139 139 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 140 if self._comm is None:
141 141 # Create a comm.
142 142 self._comm = Comm(target_name=self._model_name)
143 143 self._comm.on_msg(self._handle_msg)
144 144 self._comm.on_close(self._close)
145 145 Widget.widgets[self.model_id] = self
146 146
147 147 # first update
148 148 self.send_state()
149 149 return self._comm
150 150
151 151 @property
152 152 def model_id(self):
153 153 """Gets the model id of this widget.
154 154
155 155 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 156 return self.comm.comm_id
157 157
158 158 #-------------------------------------------------------------------------
159 159 # Methods
160 160 #-------------------------------------------------------------------------
161 161 def _close(self):
162 162 """Private close - cleanup objects, registry entries"""
163 163 del Widget.widgets[self.model_id]
164 164 self._comm = None
165 165
166 166 def close(self):
167 167 """Close method.
168 168
169 169 Closes the widget which closes the underlying comm.
170 170 When the comm is closed, all of the widget views are automatically
171 171 removed from the front-end."""
172 172 if self._comm is not None:
173 173 self._comm.close()
174 174 self._close()
175 175
176 176 def send_state(self, key=None):
177 177 """Sends the widget state, or a piece of it, to the front-end.
178 178
179 179 Parameters
180 180 ----------
181 181 key : unicode (optional)
182 182 A single property's name to sync with the front-end.
183 183 """
184 184 self._send({
185 185 "method" : "update",
186 186 "state" : self.get_state()
187 187 })
188 188
189 189 def get_state(self, key=None):
190 190 """Gets the widget state, or a piece of it.
191 191
192 192 Parameters
193 193 ----------
194 194 key : unicode (optional)
195 195 A single property's name to get.
196 196 """
197 197 keys = self.keys if key is None else [key]
198 198 state = {}
199 199 for k in keys:
200 f = self.trait_metadata(k, 'serialize')
200 f = self.trait_metadata(k, 'to_json')
201 201 if f is None:
202 f = self._serialize_trait
202 f = self._trait_to_json
203 203 value = getattr(self, k)
204 204 state[k] = f(value)
205 205 return state
206 206
207 207 def send(self, content):
208 208 """Sends a custom msg to the widget model in the front-end.
209 209
210 210 Parameters
211 211 ----------
212 212 content : dict
213 213 Content of the message to send.
214 214 """
215 215 self._send({"method": "custom", "content": content})
216 216
217 217 def on_msg(self, callback, remove=False):
218 218 """(Un)Register a custom msg receive callback.
219 219
220 220 Parameters
221 221 ----------
222 222 callback: callable
223 223 callback will be passed two arguments when a message arrives::
224 224
225 225 callback(widget, content)
226 226
227 227 remove: bool
228 228 True if the callback should be unregistered."""
229 229 self._msg_callbacks.register_callback(callback, remove=remove)
230 230
231 231 def on_displayed(self, callback, remove=False):
232 232 """(Un)Register a widget displayed callback.
233 233
234 234 Parameters
235 235 ----------
236 236 callback: method handler
237 237 Must have a signature of::
238 238
239 239 callback(widget, **kwargs)
240 240
241 241 kwargs from display are passed through without modification.
242 242 remove: bool
243 243 True if the callback should be unregistered."""
244 244 self._display_callbacks.register_callback(callback, remove=remove)
245 245
246 246 #-------------------------------------------------------------------------
247 247 # Support methods
248 248 #-------------------------------------------------------------------------
249 249 @contextmanager
250 250 def _lock_property(self, key, value):
251 251 """Lock a property-value pair.
252 252
253 253 NOTE: This, in addition to the single lock for all state changes, is
254 254 flawed. In the future we may want to look into buffering state changes
255 255 back to the front-end."""
256 256 self._property_lock = (key, value)
257 257 try:
258 258 yield
259 259 finally:
260 260 self._property_lock = (None, None)
261 261
262 262 def _should_send_property(self, key, value):
263 263 """Check the property lock (property_lock)"""
264 264 return key != self._property_lock[0] or \
265 265 value != self._property_lock[1]
266 266
267 267 # Event handlers
268 268 @_show_traceback
269 269 def _handle_msg(self, msg):
270 270 """Called when a msg is received from the front-end"""
271 271 data = msg['content']['data']
272 272 method = data['method']
273 273 if not method in ['backbone', 'custom']:
274 274 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
275 275
276 276 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
277 277 if method == 'backbone' and 'sync_data' in data:
278 278 sync_data = data['sync_data']
279 279 self._handle_receive_state(sync_data) # handles all methods
280 280
281 281 # Handle a custom msg from the front-end
282 282 elif method == 'custom':
283 283 if 'content' in data:
284 284 self._handle_custom_msg(data['content'])
285 285
286 286 def _handle_receive_state(self, sync_data):
287 287 """Called when a state is received from the front-end."""
288 288 for name in self.keys:
289 289 if name in sync_data:
290 f = self.trait_metadata(name, 'deserialize')
290 f = self.trait_metadata(name, 'from_json')
291 291 if f is None:
292 f = self._deserialize_trait
292 f = self._trait_from_json
293 293 value = f(sync_data[name])
294 294 with self._lock_property(name, value):
295 295 setattr(self, name, value)
296 296
297 297 def _handle_custom_msg(self, content):
298 298 """Called when a custom msg is received."""
299 299 self._msg_callbacks(self, content)
300 300
301 301 def _handle_property_changed(self, name, old, new):
302 302 """Called when a property has been changed."""
303 303 # Make sure this isn't information that the front-end just sent us.
304 304 if self._should_send_property(name, new):
305 305 # Send new state to front-end
306 306 self.send_state(key=name)
307 307
308 308 def _handle_displayed(self, **kwargs):
309 309 """Called when a view has been displayed for this widget instance"""
310 310 self._display_callbacks(self, **kwargs)
311 311
312 def _serialize_trait(self, x):
313 """Serialize a trait value to json
312 def _trait_to_json(self, x):
313 """Convert a trait value to json
314 314
315 315 Traverse lists/tuples and dicts and serialize their values as well.
316 316 Replace any widgets with their model_id
317 317 """
318 318 if isinstance(x, dict):
319 return {k: self._serialize_trait(v) for k, v in x.items()}
319 return {k: self._trait_to_json(v) for k, v in x.items()}
320 320 elif isinstance(x, (list, tuple)):
321 return [self._serialize_trait(v) for v in x]
321 return [self._trait_to_json(v) for v in x]
322 322 elif isinstance(x, Widget):
323 323 return "IPY_MODEL_" + x.model_id
324 324 else:
325 325 return x # Value must be JSON-able
326 326
327 def _deserialize_trait(self, x):
327 def _trait_from_json(self, x):
328 328 """Convert json values to objects
329 329
330 We explicitly support converting valid string widget UUIDs to Widget references.
330 Replace any strings representing valid model id values to Widget references.
331 331 """
332 332 if isinstance(x, dict):
333 return {k: self._deserialize_trait(v) for k, v in x.items()}
333 return {k: self._trait_from_json(v) for k, v in x.items()}
334 334 elif isinstance(x, (list, tuple)):
335 return [self._deserialize_trait(v) for v in x]
335 return [self._trait_from_json(v) for v in x]
336 336 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
337 337 # we want to support having child widgets at any level in a hierarchy
338 338 # trusting that a widget UUID will not appear out in the wild
339 339 return Widget.widgets[x]
340 340 else:
341 341 return x
342 342
343 343 def _ipython_display_(self, **kwargs):
344 344 """Called when `IPython.display.display` is called on the widget."""
345 345 # Show view. By sending a display message, the comm is opened and the
346 346 # initial state is sent.
347 347 self._send({"method": "display"})
348 348 self._handle_displayed(**kwargs)
349 349
350 350 def _send(self, msg):
351 351 """Sends a message to the model in the front-end."""
352 352 self.comm.send(msg)
353 353
354 354
355 355 class DOMWidget(Widget):
356 356 visible = Bool(True, help="Whether the widget is visible.", sync=True)
357 357 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
358 358
359 359 def get_css(self, key, selector=""):
360 360 """Get a CSS property of the widget.
361 361
362 362 Note: This function does not actually request the CSS from the
363 363 front-end; Only properties that have been set with set_css can be read.
364 364
365 365 Parameters
366 366 ----------
367 367 key: unicode
368 368 CSS key
369 369 selector: unicode (optional)
370 370 JQuery selector used when the CSS key/value was set.
371 371 """
372 372 if selector in self._css and key in self._css[selector]:
373 373 return self._css[selector][key]
374 374 else:
375 375 return None
376 376
377 377 def set_css(self, dict_or_key, value=None, selector=''):
378 378 """Set one or more CSS properties of the widget.
379 379
380 380 This function has two signatures:
381 381 - set_css(css_dict, selector='')
382 382 - set_css(key, value, selector='')
383 383
384 384 Parameters
385 385 ----------
386 386 css_dict : dict
387 387 CSS key/value pairs to apply
388 388 key: unicode
389 389 CSS key
390 390 value:
391 391 CSS value
392 392 selector: unicode (optional, kwarg only)
393 393 JQuery selector to use to apply the CSS key/value. If no selector
394 394 is provided, an empty selector is used. An empty selector makes the
395 395 front-end try to apply the css to a default element. The default
396 396 element is an attribute unique to each view, which is a DOM element
397 397 of the view that should be styled with common CSS (see
398 398 `$el_to_style` in the Javascript code).
399 399 """
400 400 if value is None:
401 401 css_dict = dict_or_key
402 402 else:
403 403 css_dict = {dict_or_key: value}
404 404
405 405 for (key, value) in css_dict.items():
406 406 # First remove the selector/key pair from the css list if it exists.
407 407 # Then add the selector/key pair and new value to the bottom of the
408 408 # list.
409 409 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
410 410 self._css += [(selector, key, value)]
411 411 self.send_state('_css')
412 412
413 413 def add_class(self, class_names, selector=""):
414 414 """Add class[es] to a DOM element.
415 415
416 416 Parameters
417 417 ----------
418 418 class_names: unicode or list
419 419 Class name(s) to add to the DOM element(s).
420 420 selector: unicode (optional)
421 421 JQuery selector to select the DOM element(s) that the class(es) will
422 422 be added to.
423 423 """
424 424 class_list = class_names
425 425 if isinstance(class_list, (list, tuple)):
426 426 class_list = ' '.join(class_list)
427 427
428 428 self.send({
429 429 "msg_type" : "add_class",
430 430 "class_list" : class_list,
431 431 "selector" : selector
432 432 })
433 433
434 434 def remove_class(self, class_names, selector=""):
435 435 """Remove class[es] from a DOM element.
436 436
437 437 Parameters
438 438 ----------
439 439 class_names: unicode or list
440 440 Class name(s) to remove from the DOM element(s).
441 441 selector: unicode (optional)
442 442 JQuery selector to select the DOM element(s) that the class(es) will
443 443 be removed from.
444 444 """
445 445 class_list = class_names
446 446 if isinstance(class_list, (list, tuple)):
447 447 class_list = ' '.join(class_list)
448 448
449 449 self.send({
450 450 "msg_type" : "remove_class",
451 451 "class_list" : class_list,
452 452 "selector" : selector,
453 453 })
General Comments 0
You need to be logged in to leave comments. Login now