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