##// END OF EJS Templates
Correct documentation
Jason Grout -
Show More
@@ -1,543 +1,539
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.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25 from .trait_types import Color
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30 class CallbackDispatcher(LoggingConfigurable):
31 31 """A structure for registering and running callbacks"""
32 32 callbacks = List()
33 33
34 34 def __call__(self, *args, **kwargs):
35 35 """Call all of the registered callbacks."""
36 36 value = None
37 37 for callback in self.callbacks:
38 38 try:
39 39 local_value = callback(*args, **kwargs)
40 40 except Exception as e:
41 41 ip = get_ipython()
42 42 if ip is None:
43 43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
44 44 else:
45 45 ip.showtraceback()
46 46 else:
47 47 value = local_value if local_value is not None else value
48 48 return value
49 49
50 50 def register_callback(self, callback, remove=False):
51 51 """(Un)Register a callback
52 52
53 53 Parameters
54 54 ----------
55 55 callback: method handle
56 56 Method to be registered or unregistered.
57 57 remove=False: bool
58 58 Whether to unregister the callback."""
59 59
60 60 # (Un)Register the callback.
61 61 if remove and callback in self.callbacks:
62 62 self.callbacks.remove(callback)
63 63 elif not remove and callback not in self.callbacks:
64 64 self.callbacks.append(callback)
65 65
66 66 def _show_traceback(method):
67 67 """decorator for showing tracebacks in IPython"""
68 68 def m(self, *args, **kwargs):
69 69 try:
70 70 return(method(self, *args, **kwargs))
71 71 except Exception as e:
72 72 ip = get_ipython()
73 73 if ip is None:
74 74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
75 75 else:
76 76 ip.showtraceback()
77 77 return m
78 78
79 79
80 80 def register(key=None):
81 81 """Returns a decorator registering a widget class in the widget registry.
82 82 If no key is provided, the class name is used as a key. A key is
83 83 provided for each core IPython widget so that the frontend can use
84 84 this key regardless of the language of the kernel"""
85 85 def wrap(widget):
86 86 l = key if key is not None else widget.__module__ + widget.__name__
87 87 Widget.widget_types[l] = widget
88 88 return widget
89 89 return wrap
90 90
91 91
92 92 def _widget_to_json(x):
93 93 if isinstance(x, dict):
94 94 return {k: _widget_to_json(v) for k, v in x.items()}
95 95 elif isinstance(x, (list, tuple)):
96 96 return [_widget_to_json(v) for v in x]
97 97 elif isinstance(x, Widget):
98 98 return "IPY_MODEL_" + x.model_id
99 99 else:
100 100 return x
101 101
102 102 def _json_to_widget(x):
103 103 if isinstance(x, dict):
104 104 return {k: _json_to_widget(v) for k, v in x.items()}
105 105 elif isinstance(x, (list, tuple)):
106 106 return [_json_to_widget(v) for v in x]
107 107 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
108 108 return Widget.widgets[x[10:]]
109 109 else:
110 110 return x
111 111
112 112 widget_serialization = {
113 113 'from_json': _json_to_widget,
114 114 'to_json': lambda x: (_widget_to_json(x), {'serialization': ('widget_serialization', 'widgets/js/types')})
115 115 }
116 116
117 117 def _to_binary_list(x):
118 118 import numpy
119 119 return memoryview(numpy.array(x, dtype=float)), {'serialization': ('list_of_numbers', 'widgets/js/types')}
120 120
121 121 def _from_binary_list(x):
122 122 import numpy
123 123 a = numpy.frombuffer(x.tobytes(), dtype=float)
124 124 return list(a)
125 125
126 126 list_of_numbers = {
127 127 'from_json': _from_binary_list,
128 128 'to_json': _to_binary_list
129 129 }
130 130
131 131
132 132
133 133 class Widget(LoggingConfigurable):
134 134 #-------------------------------------------------------------------------
135 135 # Class attributes
136 136 #-------------------------------------------------------------------------
137 137 _widget_construction_callback = None
138 138 widgets = {}
139 139 widget_types = {}
140 140
141 141 @staticmethod
142 142 def on_widget_constructed(callback):
143 143 """Registers a callback to be called when a widget is constructed.
144 144
145 145 The callback must have the following signature:
146 146 callback(widget)"""
147 147 Widget._widget_construction_callback = callback
148 148
149 149 @staticmethod
150 150 def _call_widget_constructed(widget):
151 151 """Static method, called when a widget is constructed."""
152 152 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
153 153 Widget._widget_construction_callback(widget)
154 154
155 155 @staticmethod
156 156 def handle_comm_opened(comm, msg):
157 157 """Static method, called when a widget is constructed."""
158 158 widget_class = import_item(msg['content']['data']['widget_class'])
159 159 widget = widget_class(comm=comm)
160 160
161 161
162 162 #-------------------------------------------------------------------------
163 163 # Traits
164 164 #-------------------------------------------------------------------------
165 165 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
166 166 in which to find _model_name. If empty, look in the global registry.""")
167 167 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
168 168 registered in the front-end to create and sync this widget with.""")
169 169 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
170 170 If empty, look in the global registry.""", sync=True)
171 171 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
172 172 to use to represent the widget.""", sync=True)
173 173 comm = Instance('IPython.kernel.comm.Comm')
174 174
175 175 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
176 176 front-end can send before receiving an idle msg from the back-end.""")
177 177
178 178 version = Int(0, sync=True, help="""Widget's version""")
179 179 keys = List()
180 180 def _keys_default(self):
181 181 return [name for name in self.traits(sync=True)]
182 182
183 183 _property_lock = Tuple((None, None))
184 184 _send_state_lock = Int(0)
185 185 _states_to_send = Set()
186 186 _display_callbacks = Instance(CallbackDispatcher, ())
187 187 _msg_callbacks = Instance(CallbackDispatcher, ())
188 188
189 189 #-------------------------------------------------------------------------
190 190 # (Con/de)structor
191 191 #-------------------------------------------------------------------------
192 192 def __init__(self, **kwargs):
193 193 """Public constructor"""
194 194 self._model_id = kwargs.pop('model_id', None)
195 195 super(Widget, self).__init__(**kwargs)
196 196
197 197 Widget._call_widget_constructed(self)
198 198 self.open()
199 199
200 200 def __del__(self):
201 201 """Object disposal"""
202 202 self.close()
203 203
204 204 #-------------------------------------------------------------------------
205 205 # Properties
206 206 #-------------------------------------------------------------------------
207 207
208 208 def open(self):
209 209 """Open a comm to the frontend if one isn't already open."""
210 210 if self.comm is None:
211 211 args = dict(target_name='ipython.widget',
212 212 data={'model_name': self._model_name,
213 213 'model_module': self._model_module})
214 214 if self._model_id is not None:
215 215 args['comm_id'] = self._model_id
216 216 self.comm = Comm(**args)
217 217
218 218 def _comm_changed(self, name, new):
219 219 """Called when the comm is changed."""
220 220 if new is None:
221 221 return
222 222 self._model_id = self.model_id
223 223
224 224 self.comm.on_msg(self._handle_msg)
225 225 Widget.widgets[self.model_id] = self
226 226
227 227 # first update
228 228 self.send_state()
229 229
230 230 @property
231 231 def model_id(self):
232 232 """Gets the model id of this widget.
233 233
234 234 If a Comm doesn't exist yet, a Comm will be created automagically."""
235 235 return self.comm.comm_id
236 236
237 237 #-------------------------------------------------------------------------
238 238 # Methods
239 239 #-------------------------------------------------------------------------
240 240
241 241 def close(self):
242 242 """Close method.
243 243
244 244 Closes the underlying comm.
245 245 When the comm is closed, all of the widget views are automatically
246 246 removed from the front-end."""
247 247 if self.comm is not None:
248 248 Widget.widgets.pop(self.model_id, None)
249 249 self.comm.close()
250 250 self.comm = None
251 251
252 252 def send_state(self, key=None):
253 253 """Sends the widget state, or a piece of it, to the front-end.
254 254
255 255 Parameters
256 256 ----------
257 257 key : unicode, or iterable (optional)
258 258 A single property's name or iterable of property names to sync with the front-end.
259 259 """
260 260 state, buffer_keys, buffers, metadata = self.get_state(key=key)
261 261 msg = {"method": "update", "state": state}
262 262 if buffer_keys:
263 263 msg['buffers'] = buffer_keys
264 264 if metadata:
265 265 msg['metadata'] = metadata
266 266 self._send(msg, buffers=buffers)
267 267
268 268 def get_state(self, key=None):
269 269 """Gets the widget state, or a piece of it.
270 270
271 271 Parameters
272 272 ----------
273 273 key : unicode or iterable (optional)
274 274 A single property's name or iterable of property names to get.
275 275
276 276 Returns
277 277 -------
278 278 state : dict of states
279 279 buffer_keys : list of strings
280 280 the values that are stored in buffers
281 281 buffers : list of binary memoryviews
282 282 values to transmit in binary
283 283 metadata : dict
284 284 metadata for each field: {key: metadata}
285 285 """
286 286 if key is None:
287 287 keys = self.keys
288 288 elif isinstance(key, string_types):
289 289 keys = [key]
290 290 elif isinstance(key, collections.Iterable):
291 291 keys = key
292 292 else:
293 293 raise ValueError("key must be a string, an iterable of keys, or None")
294 294 state = {}
295 295 buffers = []
296 296 buffer_keys = []
297 297 metadata = {}
298 298 for k in keys:
299 299 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
300 300 value = getattr(self, k)
301 301 serialized, md = f(value)
302 302 if isinstance(serialized, memoryview):
303 303 buffers.append(serialized)
304 304 buffer_keys.append(k)
305 305 else:
306 306 state[k] = serialized
307 307 if md is not None:
308 308 metadata[k] = md
309 309 return state, buffer_keys, buffers, metadata
310 310
311 311 def set_state(self, sync_data):
312 312 """Called when a state is received from the front-end."""
313 313 for name in self.keys:
314 314 if name in sync_data:
315 315 json_value = sync_data[name]
316 316 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
317 317 with self._lock_property(name, json_value):
318 318 setattr(self, name, from_json(json_value))
319 319
320 320 def send(self, content, buffers=None):
321 321 """Sends a custom msg to the widget model in the front-end.
322 322
323 323 Parameters
324 324 ----------
325 325 content : dict
326 326 Content of the message to send.
327 327 buffers : list of binary buffers
328 328 Binary buffers to send with message
329 329 """
330 330 self._send({"method": "custom", "content": content}, buffers=buffers)
331 331
332 332 def on_msg(self, callback, remove=False):
333 333 """(Un)Register a custom msg receive callback.
334 334
335 335 Parameters
336 336 ----------
337 337 callback: callable
338 338 callback will be passed three arguments when a message arrives::
339 339
340 340 callback(widget, content, buffers)
341 341
342 342 remove: bool
343 343 True if the callback should be unregistered."""
344 344 self._msg_callbacks.register_callback(callback, remove=remove)
345 345
346 346 def on_displayed(self, callback, remove=False):
347 347 """(Un)Register a widget displayed callback.
348 348
349 349 Parameters
350 350 ----------
351 351 callback: method handler
352 352 Must have a signature of::
353 353
354 354 callback(widget, **kwargs)
355 355
356 356 kwargs from display are passed through without modification.
357 357 remove: bool
358 358 True if the callback should be unregistered."""
359 359 self._display_callbacks.register_callback(callback, remove=remove)
360 360
361 361 #-------------------------------------------------------------------------
362 362 # Support methods
363 363 #-------------------------------------------------------------------------
364 364 @contextmanager
365 365 def _lock_property(self, key, value):
366 366 """Lock a property-value pair.
367 367
368 368 The value should be the JSON state of the property.
369 369
370 370 NOTE: This, in addition to the single lock for all state changes, is
371 371 flawed. In the future we may want to look into buffering state changes
372 372 back to the front-end."""
373 373 self._property_lock = (key, value)
374 374 try:
375 375 yield
376 376 finally:
377 377 self._property_lock = (None, None)
378 378
379 379 @contextmanager
380 380 def hold_sync(self):
381 381 """Hold syncing any state until the context manager is released"""
382 382 # We increment a value so that this can be nested. Syncing will happen when
383 383 # all levels have been released.
384 384 self._send_state_lock += 1
385 385 try:
386 386 yield
387 387 finally:
388 388 self._send_state_lock -=1
389 389 if self._send_state_lock == 0:
390 390 self.send_state(self._states_to_send)
391 391 self._states_to_send.clear()
392 392
393 393 def _should_send_property(self, key, value):
394 394 """Check the property lock (property_lock)"""
395 395 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
396 396 if (key == self._property_lock[0]
397 397 and to_json(value) == self._property_lock[1]):
398 398 return False
399 399 elif self._send_state_lock > 0:
400 400 self._states_to_send.add(key)
401 401 return False
402 402 else:
403 403 return True
404 404
405 405 # Event handlers
406 406 @_show_traceback
407 407 def _handle_msg(self, msg):
408 408 """Called when a msg is received from the front-end"""
409 409 data = msg['content']['data']
410 410 method = data['method']
411 411
412 412 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
413 413 if method == 'backbone':
414 414 if 'sync_data' in data:
415 415 # get binary buffers too
416 416 sync_data = data['sync_data']
417 417 for i,k in enumerate(data.get('buffer_keys', [])):
418 418 sync_data[k] = msg['buffers'][i]
419 419 self.set_state(sync_data) # handles all methods
420 420
421 421 # Handle a state request.
422 422 elif method == 'request_state':
423 423 self.send_state()
424 424
425 425 # Handle a custom msg from the front-end.
426 426 elif method == 'custom':
427 427 if 'content' in data:
428 428 self._handle_custom_msg(data['content'], msg['buffers'])
429 429
430 430 # Catch remainder.
431 431 else:
432 432 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
433 433
434 434 def _handle_custom_msg(self, content, buffers):
435 435 """Called when a custom msg is received."""
436 436 self._msg_callbacks(self, content, buffers)
437 437
438 438 def _notify_trait(self, name, old_value, new_value):
439 439 """Called when a property has been changed."""
440 440 # Trigger default traitlet callback machinery. This allows any user
441 441 # registered validation to be processed prior to allowing the widget
442 442 # machinery to handle the state.
443 443 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
444 444
445 445 # Send the state after the user registered callbacks for trait changes
446 446 # have all fired (allows for user to validate values).
447 447 if self.comm is not None and name in self.keys:
448 448 # Make sure this isn't information that the front-end just sent us.
449 449 if self._should_send_property(name, new_value):
450 450 # Send new state to front-end
451 451 self.send_state(key=name)
452 452
453 453 def _handle_displayed(self, **kwargs):
454 454 """Called when a view has been displayed for this widget instance"""
455 455 self._display_callbacks(self, **kwargs)
456 456
457 457 def _trait_to_json(self, x):
458 """Convert a trait value to json
458 """Convert a trait value to json.
459 459
460 Traverse lists/tuples and dicts and serialize their values as well.
461 Replace any widgets with their model_id
460 Metadata (the second return value) is not sent
462 461 """
463 462 return x, None
464 463
465 464 def _trait_from_json(self, x):
466 """Convert json values to objects
467
468 Replace any strings representing valid model id values to Widget references.
469 """
465 """Convert json values to objects."""
470 466 return x
471 467
472 468 def _ipython_display_(self, **kwargs):
473 469 """Called when `IPython.display.display` is called on the widget."""
474 470 # Show view.
475 471 if self._view_name is not None:
476 472 self._send({"method": "display"})
477 473 self._handle_displayed(**kwargs)
478 474
479 475 def _send(self, msg, buffers=None):
480 476 """Sends a message to the model in the front-end."""
481 477 self.comm.send(data=msg, buffers=buffers)
482 478
483 479
484 480 class DOMWidget(Widget):
485 481 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
486 482 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
487 483 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
488 484
489 485 width = CUnicode(sync=True)
490 486 height = CUnicode(sync=True)
491 487 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
492 488 padding = CUnicode(sync=True)
493 489 margin = CUnicode(sync=True)
494 490
495 491 color = Color(None, allow_none=True, sync=True)
496 492 background_color = Color(None, allow_none=True, sync=True)
497 493 border_color = Color(None, allow_none=True, sync=True)
498 494
499 495 border_width = CUnicode(sync=True)
500 496 border_radius = CUnicode(sync=True)
501 497 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
502 498 'none',
503 499 'hidden',
504 500 'dotted',
505 501 'dashed',
506 502 'solid',
507 503 'double',
508 504 'groove',
509 505 'ridge',
510 506 'inset',
511 507 'outset',
512 508 'initial',
513 509 'inherit', ''],
514 510 default_value='', sync=True)
515 511
516 512 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
517 513 'normal',
518 514 'italic',
519 515 'oblique',
520 516 'initial',
521 517 'inherit', ''],
522 518 default_value='', sync=True)
523 519 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
524 520 'normal',
525 521 'bold',
526 522 'bolder',
527 523 'lighter',
528 524 'initial',
529 525 'inherit', ''] + list(map(str, range(100,1000,100))),
530 526 default_value='', sync=True)
531 527 font_size = CUnicode(sync=True)
532 528 font_family = Unicode(sync=True)
533 529
534 530 def __init__(self, *pargs, **kwargs):
535 531 super(DOMWidget, self).__init__(*pargs, **kwargs)
536 532
537 533 def _validate_border(name, old, new):
538 534 if new is not None and new != '':
539 535 if name != 'border_width' and not self.border_width:
540 536 self.border_width = 1
541 537 if name != 'border_style' and self.border_style == '':
542 538 self.border_style = 'solid'
543 539 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now