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