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