##// END OF EJS Templates
Fixed comments for optional kwargs so they are redundant.
Jonathan Frederic -
Show More
@@ -1,408 +1,408 b''
1 1 """Base Widget class. Allows user to create widgets in the backend that render
2 2 in the IPython notebook frontend.
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 copy import copy
16 16 from glob import glob
17 17 import uuid
18 18 import sys
19 19 import os
20 20 import inspect
21 21 import types
22 22
23 23 import IPython
24 24 from IPython.kernel.comm import Comm
25 25 from IPython.config import LoggingConfigurable
26 26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 27 from IPython.display import Javascript, display
28 28 from IPython.utils.py3compat import string_types
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Classes
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class Widget(LoggingConfigurable):
35 35
36 36 # Shared declarations (Class level)
37 37 widget_construction_callback = None
38 38
39 39 keys = ['view_name']
40 40
41 41 def on_widget_constructed(callback):
42 42 """Class method, registers a callback to be called when a widget is
43 43 constructed. The callback must have the following signature:
44 44 callback(widget)"""
45 45 Widget.widget_construction_callback = callback
46 46
47 47 def _call_widget_constructed(widget):
48 48 """Class method, called when a widget is constructed."""
49 49 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
50 50 Widget.widget_construction_callback(widget)
51 51
52 52
53 53
54 54 # Public declarations (Instance level)
55 55 target_name = Unicode('widget', help="""Name of the backbone model
56 56 registered in the frontend to create and sync this widget with.""")
57 57 view_name = Unicode(help="""Default view registered in the frontend
58 58 to use to represent the widget.""")
59 59
60 60 # Private/protected declarations
61 61 # todo: change this to a context manager
62 62 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
63 63 _comm = Instance('IPython.kernel.comm.Comm')
64 64
65 65 def __init__(self, **kwargs):
66 66 """Public constructor
67 67 """
68 68 self._display_callbacks = []
69 69 self._msg_callbacks = []
70 70 super(Widget, self).__init__(**kwargs)
71 71
72 72 self.on_trait_change(self._handle_property_changed, self.keys)
73 73 Widget._call_widget_constructed(self)
74 74
75 75 def __del__(self):
76 76 """Object disposal"""
77 77 self.close()
78 78
79 79
80 80 def close(self):
81 81 """Close method. Closes the widget which closes the underlying comm.
82 82 When the comm is closed, all of the widget views are automatically
83 83 removed from the frontend."""
84 84 self._close_communication()
85 85
86 86 @property
87 87 def comm(self):
88 88 if self._comm is None:
89 89 self._open_communication()
90 90 return self._comm
91 91
92 92 @property
93 93 def model_id(self):
94 94 return self.comm.comm_id
95 95
96 96 # Event handlers
97 97 def _handle_msg(self, msg):
98 98 """Called when a msg is recieved from the frontend"""
99 99 data = msg['content']['data']
100 100 method = data['method']
101 101
102 102 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
103 103 if method == 'backbone' and 'sync_data' in data:
104 104 sync_data = data['sync_data']
105 105 self._handle_recieve_state(sync_data) # handles all methods
106 106
107 107 # Handle a custom msg from the front-end
108 108 elif method == 'custom':
109 109 if 'custom_content' in data:
110 110 self._handle_custom_msg(data['custom_content'])
111 111
112 112 def _handle_custom_msg(self, content):
113 113 """Called when a custom msg is recieved."""
114 114 for handler in self._msg_callbacks:
115 115 if callable(handler):
116 116 argspec = inspect.getargspec(handler)
117 117 nargs = len(argspec[0])
118 118
119 119 # Bound methods have an additional 'self' argument
120 120 if isinstance(handler, types.MethodType):
121 121 nargs -= 1
122 122
123 123 # Call the callback
124 124 if nargs == 1:
125 125 handler(content)
126 126 elif nargs == 2:
127 127 handler(self, content)
128 128 else:
129 129 raise TypeError('Widget msg callback must ' \
130 130 'accept 1 or 2 arguments, not %d.' % nargs)
131 131
132 132
133 133 def _handle_recieve_state(self, sync_data):
134 134 """Called when a state is recieved from the frontend."""
135 135 for name in self.keys:
136 136 if name in sync_data:
137 137 try:
138 138 self._property_lock = (name, sync_data[name])
139 139 setattr(self, name, sync_data[name])
140 140 finally:
141 141 self._property_lock = (None, None)
142 142
143 143
144 144 def _handle_property_changed(self, name, old, new):
145 145 """Called when a property has been changed."""
146 146 # Make sure this isn't information that the front-end just sent us.
147 147 if self._property_lock[0] != name and self._property_lock[1] != new:
148 148 # Send new state to frontend
149 149 self.send_state(key=name)
150 150
151 151 def _handle_displayed(self, **kwargs):
152 152 """Called when a view has been displayed for this widget instance"""
153 153 for handler in self._display_callbacks:
154 154 if callable(handler):
155 155 argspec = inspect.getargspec(handler)
156 156 nargs = len(argspec[0])
157 157
158 158 # Bound methods have an additional 'self' argument
159 159 if isinstance(handler, types.MethodType):
160 160 nargs -= 1
161 161
162 162 # Call the callback
163 163 if nargs == 0:
164 164 handler()
165 165 elif nargs == 1:
166 166 handler(self)
167 167 else:
168 168 handler(self, **kwargs)
169 169
170 170 # Public methods
171 171 def send_state(self, key=None):
172 172 """Sends the widget state, or a piece of it, to the frontend.
173 173
174 174 Parameters
175 175 ----------
176 176 key : unicode (optional)
177 177 A single property's name to sync with the frontend.
178 178 """
179 179 self._send({"method": "update",
180 180 "state": self.get_state()})
181 181
182 182 def get_state(self, key=None):
183 183 """Gets the widget state, or a piece of it.
184 184
185 185 Parameters
186 186 ----------
187 187 key : unicode (optional)
188 188 A single property's name to get.
189 189 """
190 190 state = {}
191 191
192 192 # If a key is provided, just send the state of that key.
193 193 if key is None:
194 194 keys = self.keys[:]
195 195 else:
196 196 keys = [key]
197 197 for k in keys:
198 198 value = getattr(self, k)
199 199
200 200 # a more elegant solution to encoding Widgets would be
201 201 # to tap into the JSON encoder and teach it how to deal
202 202 # with Widget objects, or maybe just teach the JSON
203 203 # encoder to look for a _repr_json property before giving
204 204 # up encoding
205 205 if isinstance(value, Widget):
206 206 value = value.model_id
207 207 elif isinstance(value, list) and len(value)>0 and isinstance(value[0], Widget):
208 208 # assume all elements of the list are widgets
209 209 value = [i.model_id for i in value]
210 210 state[k] = value
211 211 return state
212 212
213 213
214 214 def send(self, content):
215 215 """Sends a custom msg to the widget model in the front-end.
216 216
217 217 Parameters
218 218 ----------
219 219 content : dict
220 220 Content of the message to send.
221 221 """
222 222 self._send({"method": "custom",
223 223 "custom_content": content})
224 224
225 225
226 226 def on_msg(self, callback, remove=False):
227 227 """Register a callback for when a custom msg is recieved from the front-end
228 228
229 229 Parameters
230 230 ----------
231 231 callback: method handler
232 232 Can have a signature of:
233 233 - callback(content)
234 234 - callback(sender, content)
235 235 remove: bool
236 236 True if the callback should be unregistered."""
237 237 if remove and callback in self._msg_callbacks:
238 238 self._msg_callbacks.remove(callback)
239 239 elif not remove and not callback in self._msg_callbacks:
240 240 self._msg_callbacks.append(callback)
241 241
242 242
243 243 def on_displayed(self, callback, remove=False):
244 244 """Register a callback to be called when the widget has been displayed
245 245
246 246 Parameters
247 247 ----------
248 248 callback: method handler
249 249 Can have a signature of:
250 250 - callback()
251 251 - callback(sender)
252 252 - callback(sender, **kwargs)
253 253 kwargs from display call passed through without modification.
254 254 remove: bool
255 255 True if the callback should be unregistered."""
256 256 if remove and callback in self._display_callbacks:
257 257 self._display_callbacks.remove(callback)
258 258 elif not remove and not callback in self._display_callbacks:
259 259 self._display_callbacks.append(callback)
260 260
261 261
262 262 # Support methods
263 263 def _repr_widget_(self, **kwargs):
264 264 """Function that is called when `IPython.display.display` is called on
265 265 the widget."""
266 266
267 267 # Show view. By sending a display message, the comm is opened and the
268 268 # initial state is sent.
269 269 self._send({"method": "display"})
270 270 self._handle_displayed(**kwargs)
271 271
272 272
273 273 def _open_communication(self):
274 274 """Opens a communication with the front-end."""
275 275 # Create a comm.
276 276 if self._comm is None:
277 277 self._comm = Comm(target_name=self.target_name)
278 278 self._comm.on_msg(self._handle_msg)
279 279 self._comm.on_close(self._close_communication)
280 280
281 281 # first update
282 282 self.send_state()
283 283
284 284
285 285 def _close_communication(self):
286 286 """Closes a communication with the front-end."""
287 287 if self._comm is not None:
288 288 try:
289 289 self._comm.close()
290 290 finally:
291 291 self._comm = None
292 292
293 293
294 294 def _send(self, msg):
295 295 """Sends a message to the model in the front-end"""
296 296 self.comm.send(msg)
297 297
298 298
299 299 class DOMWidget(Widget):
300 300 visible = Bool(True, help="Whether or not the widget is visible.")
301 301
302 302 # Private/protected declarations
303 303 _css = Dict() # Internal CSS property dict
304 304
305 305 keys = ['visible', '_css'] + Widget.keys
306 306
307 307 def get_css(self, key, selector=""):
308 308 """Get a CSS property of the widget. Note, this function does not
309 309 actually request the CSS from the front-end; Only properties that have
310 310 been set with set_css can be read.
311 311
312 312 Parameters
313 313 ----------
314 314 key: unicode
315 315 CSS key
316 316 selector: unicode (optional)
317 317 JQuery selector used when the CSS key/value was set.
318 318 """
319 319 if selector in self._css and key in self._css[selector]:
320 320 return self._css[selector][key]
321 321 else:
322 322 return None
323 323
324 324
325 325 def set_css(self, *args, **kwargs):
326 326 """Set one or more CSS properties of the widget (shared among all of the
327 327 views). This function has two signatures:
328 - set_css(css_dict, [selector=''])
329 - set_css(key, value, [selector=''])
328 - set_css(css_dict, selector='')
329 - set_css(key, value, selector='')
330 330
331 331 Parameters
332 332 ----------
333 333 css_dict : dict
334 334 CSS key/value pairs to apply
335 335 key: unicode
336 336 CSS key
337 337 value
338 338 CSS value
339 339 selector: unicode (optional)
340 340 JQuery selector to use to apply the CSS key/value.
341 341 """
342 342 selector = kwargs.get('selector', '')
343 343
344 # Signature 1: set_css(css_dict, [selector=''])
344 # Signature 1: set_css(css_dict, selector='')
345 345 if len(args) == 1:
346 346 if isinstance(args[0], dict):
347 347 for (key, value) in args[0].items():
348 348 self.set_css(key, value, selector=selector)
349 349 else:
350 350 raise Exception('css_dict must be a dict.')
351 351
352 # Signature 2: set_css(key, value, [selector=''])
352 # Signature 2: set_css(key, value, selector='')
353 353 elif len(args) == 2 or len(args) == 3:
354 354
355 355 # Selector can be a positional arg if it's the 3rd value
356 356 if len(args) == 3:
357 357 selector = args[2]
358 358 if selector not in self._css:
359 359 self._css[selector] = {}
360 360
361 361 # Only update the property if it has changed.
362 362 key = args[0]
363 363 value = args[1]
364 364 if not (key in self._css[selector] and value in self._css[selector][key]):
365 365 self._css[selector][key] = value
366 366 self.send_state('_css') # Send new state to client.
367 367 else:
368 368 raise Exception('set_css only accepts 1-3 arguments')
369 369
370 370
371 371 def add_class(self, class_names, selector=""):
372 372 """Add class[es] to a DOM element
373 373
374 374 Parameters
375 375 ----------
376 376 class_names: unicode or list
377 377 Class name(s) to add to the DOM element(s).
378 378 selector: unicode (optional)
379 379 JQuery selector to select the DOM element(s) that the class(es) will
380 380 be added to.
381 381 """
382 382 class_list = class_names
383 383 if isinstance(list, class_list):
384 384 class_list = ' '.join(class_list)
385 385
386 386 self.send({"msg_type": "add_class",
387 387 "class_list": class_list,
388 388 "selector": selector})
389 389
390 390
391 391 def remove_class(self, class_names, selector=""):
392 392 """Remove class[es] from a DOM element
393 393
394 394 Parameters
395 395 ----------
396 396 class_names: unicode or list
397 397 Class name(s) to remove from the DOM element(s).
398 398 selector: unicode (optional)
399 399 JQuery selector to select the DOM element(s) that the class(es) will
400 400 be removed from.
401 401 """
402 402 class_list = class_names
403 403 if isinstance(list, class_list):
404 404 class_list = ' '.join(class_list)
405 405
406 406 self.send({"msg_type": "remove_class",
407 407 "class_list": class_list,
408 408 "selector": selector})
General Comments 0
You need to be logged in to leave comments. Login now