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