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