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