##// END OF EJS Templates
Dev meeting widget review day 1
Jonathan Frederic -
Show More
@@ -114,9 +114,10 b' def display(*objs, **kwargs):'
114 format = InteractiveShell.instance().display_formatter.format
114 format = InteractiveShell.instance().display_formatter.format
115
115
116 for obj in objs:
116 for obj in objs:
117 if hasattr(obj, '_repr_widget_'):
117
118 obj._repr_widget_(**kwargs)
118 # If _ipython_display_ is defined, use that to display this object. If
119 else:
119 # it returns NotImplemented, use the _repr_ logic (default).
120 if not hasattr(obj, '_ipython_display_') or isinstance(obj._ipython_display_(**kwargs), NotImplemented):
120 if raw:
121 if raw:
121 publish_display_data('display', obj, metadata)
122 publish_display_data('display', obj, metadata)
122 else:
123 else:
@@ -1,5 +1,5 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 back-end that render
2 in the IPython notebook frontend.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
@@ -24,25 +24,11 b' from IPython.utils.py3compat import string_types'
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Classes
25 # Classes
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 @contextmanager
28 def PropertyLock(instance, key, value):
29 instance._property_lock = (key, value)
30 try:
31 yield
32 finally:
33 del instance._property_lock
34
35 def should_send_property(instance, key, value):
36 return not hasattr(instance, '_property_lock') or \
37 key != instance._property_lock[0] or \
38 value != instance._property_lock[1]
39
40
41 class Widget(LoggingConfigurable):
27 class Widget(LoggingConfigurable):
42
28
43 # Shared declarations (Class level)
29 # Shared declarations (Class level)
44 widget_construction_callback = None
30 widget_construction_callback = None
45 widgets = []
31 widgets = {}
46
32
47 keys = ['view_name'] # TODO: Sync = True
33 keys = ['view_name'] # TODO: Sync = True
48
34
@@ -60,12 +46,29 b' class Widget(LoggingConfigurable):'
60
46
61
47
62 # Public declarations (Instance level)
48 # Public declarations (Instance level)
63 target_name = Unicode('widget', help="""Name of the backbone model
49 model_name = Unicode('widget', help="""Name of the backbone model
64 registered in the frontend to create and sync this widget with.""")
50 registered in the front-end to create and sync this widget with.""")
65 # model_name
51 view_name = Unicode(help="""Default view registered in the front-end
66 view_name = Unicode(help="""Default view registered in the frontend
67 to use to represent the widget.""")
52 to use to represent the widget.""")
68
53
54 @contextmanager
55 def property_lock(self, key, value):
56 """Lock a property-value pair.
57
58 NOTE: This, in addition to the single lock for all state changes, is
59 flawed. In the future we may want to look into buffering state changes
60 back to the front-end."""
61 self._property_lock = (key, value)
62 try:
63 yield
64 finally:
65 self._property_lock = (None, None)
66
67 def should_send_property(self, key, value):
68 """Check the property lock (property_lock)"""
69 return key != self._property_lock[0] or \
70 value != self._property_lock[1]
71
69 # Private/protected declarations
72 # Private/protected declarations
70 _comm = Instance('IPython.kernel.comm.Comm')
73 _comm = Instance('IPython.kernel.comm.Comm')
71
74
@@ -73,12 +76,12 b' class Widget(LoggingConfigurable):'
73 """Public constructor
76 """Public constructor
74 """
77 """
75 self.closed = False
78 self.closed = False
79 self._property_lock = (None, None)
76 self._display_callbacks = []
80 self._display_callbacks = []
77 self._msg_callbacks = []
81 self._msg_callbacks = []
78 super(Widget, self).__init__(**kwargs)
82 super(Widget, self).__init__(**kwargs)
79
83
80 self.on_trait_change(self._handle_property_changed, self.keys)
84 self.on_trait_change(self._handle_property_changed, self.keys)
81 Widget.widgets.append(self)
82 Widget._call_widget_constructed(self)
85 Widget._call_widget_constructed(self)
83
86
84 def __del__(self):
87 def __del__(self):
@@ -88,16 +91,30 b' class Widget(LoggingConfigurable):'
88 def close(self):
91 def close(self):
89 """Close method. Closes the widget which closes the underlying comm.
92 """Close method. Closes the widget which closes the underlying comm.
90 When the comm is closed, all of the widget views are automatically
93 When the comm is closed, all of the widget views are automatically
91 removed from the frontend."""
94 removed from the front-end."""
92 if not self.closed:
95 if not self.closed:
93 self.closed = True
96 self._comm.close()
94 self._close_communication()
97 self._close()
95 Widget.widgets.remove(self)
98
96
99
100 def _close(self):
101 """Unsafe close"""
102 del Widget.widgets[self.model_id]
103 self._comm = None
104 self.closed = True
105
106
97 @property
107 @property
98 def comm(self):
108 def comm(self):
99 if self._comm is None:
109 if self._comm is None:
100 self._open_communication()
110 # Create a comm.
111 self._comm = Comm(target_name=self.model_name)
112 self._comm.on_msg(self._handle_msg)
113 self._comm.on_close(self._close)
114 Widget.widgets[self.model_id] = self
115
116 # first update
117 self.send_state()
101 return self._comm
118 return self._comm
102
119
103 @property
120 @property
@@ -106,11 +123,11 b' class Widget(LoggingConfigurable):'
106
123
107 # Event handlers
124 # Event handlers
108 def _handle_msg(self, msg):
125 def _handle_msg(self, msg):
109 """Called when a msg is received from the frontend"""
126 """Called when a msg is received from the front-end"""
110 data = msg['content']['data']
127 data = msg['content']['data']
111 method = data['method']
128 method = data['method']
112
129 if not method in ['backbone', 'custom']:
113 # TODO: Log unrecog.
130 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
114
131
115 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
132 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
116 if method == 'backbone' and 'sync_data' in data:
133 if method == 'backbone' and 'sync_data' in data:
@@ -124,69 +141,40 b' class Widget(LoggingConfigurable):'
124
141
125
142
126 def _handle_receive_state(self, sync_data):
143 def _handle_receive_state(self, sync_data):
127 """Called when a state is received from the frontend."""
144 """Called when a state is received from the front-end."""
128 for name in self.keys:
145 for name in self.keys:
129 if name in sync_data:
146 if name in sync_data:
130 value = self._unpack_widgets(sync_data[name])
147 value = self._unpack_widgets(sync_data[name])
131 with PropertyLock(self, name, value):
148 with self.property_lock(name, value):
132 setattr(self, name, value)
149 setattr(self, name, value)
133
150
134
151
135 def _handle_custom_msg(self, content):
152 def _handle_custom_msg(self, content):
136 """Called when a custom msg is received."""
153 """Called when a custom msg is received."""
137 for handler in self._msg_callbacks:
154 for handler in self._msg_callbacks:
138 if callable(handler):
155 handler(self, content)
139 argspec = inspect.getargspec(handler)
140 nargs = len(argspec[0])
141
142 # Bound methods have an additional 'self' argument
143 if isinstance(handler, types.MethodType):
144 nargs -= 1
145
146 # Call the callback
147 if nargs == 1:
148 handler(content)
149 elif nargs == 2:
150 handler(self, content)
151 else:
152 raise TypeError('Widget msg callback must ' \
153 'accept 1 or 2 arguments, not %d.' % nargs)
154
156
155
157
156 def _handle_property_changed(self, name, old, new):
158 def _handle_property_changed(self, name, old, new):
157 """Called when a property has been changed."""
159 """Called when a property has been changed."""
158 # Make sure this isn't information that the front-end just sent us.
160 # Make sure this isn't information that the front-end just sent us.
159 if should_send_property(self, name, new):
161 if should_send_property(self, name, new):
160 # Send new state to frontend
162 # Send new state to front-end
161 self.send_state(key=name)
163 self.send_state(key=name)
162
164
163 def _handle_displayed(self, **kwargs):
165 def _handle_displayed(self, **kwargs):
164 """Called when a view has been displayed for this widget instance"""
166 """Called when a view has been displayed for this widget instance"""
165 for handler in self._display_callbacks:
167 for handler in self._display_callbacks:
166 if callable(handler):
168 handler(self, **kwargs)
167 argspec = inspect.getargspec(handler)
168 nargs = len(argspec[0])
169
170 # Bound methods have an additional 'self' argument
171 if isinstance(handler, types.MethodType):
172 nargs -= 1
173
174 # Call the callback
175 if nargs == 0:
176 handler()
177 elif nargs == 1:
178 handler(self)
179 else:
180 handler(self, **kwargs)
181
169
182 # Public methods
170 # Public methods
183 def send_state(self, key=None):
171 def send_state(self, key=None):
184 """Sends the widget state, or a piece of it, to the frontend.
172 """Sends the widget state, or a piece of it, to the front-end.
185
173
186 Parameters
174 Parameters
187 ----------
175 ----------
188 key : unicode (optional)
176 key : unicode (optional)
189 A single property's name to sync with the frontend.
177 A single property's name to sync with the front-end.
190 """
178 """
191 self._send({"method": "update",
179 self._send({"method": "update",
192 "state": self.get_state()})
180 "state": self.get_state()})
@@ -199,16 +187,8 b' class Widget(LoggingConfigurable):'
199 key : unicode (optional)
187 key : unicode (optional)
200 A single property's name to get.
188 A single property's name to get.
201 """
189 """
202 state = {}
190 keys = self.keys if key is None else [key]
203
191 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
204 # If a key is provided, just send the state of that key.
205 if key is None:
206 keys = self.keys[:]
207 else:
208 keys = [key]
209 for k in keys:
210 state[k] = self._pack_widgets(getattr(self, k))
211 return state
212
192
213
193
214 def _pack_widgets(self, values):
194 def _pack_widgets(self, values):
@@ -219,8 +199,8 b' class Widget(LoggingConfigurable):'
219 their model ids."""
199 their model ids."""
220 if isinstance(values, dict):
200 if isinstance(values, dict):
221 new_dict = {}
201 new_dict = {}
222 for key in values.keys():
202 for key, value in values.items():
223 new_dict[key] = self._pack_widgets(values[key])
203 new_dict[key] = self._pack_widgets(value)
224 return new_dict
204 return new_dict
225 elif isinstance(values, list):
205 elif isinstance(values, list):
226 new_list = []
206 new_list = []
@@ -241,7 +221,7 b' class Widget(LoggingConfigurable):'
241 their model ids."""
221 their model ids."""
242 if isinstance(values, dict):
222 if isinstance(values, dict):
243 new_dict = {}
223 new_dict = {}
244 for key in values.keys():
224 for key, values in values.items():
245 new_dict[key] = self._unpack_widgets(values[key])
225 new_dict[key] = self._unpack_widgets(values[key])
246 return new_dict
226 return new_dict
247 elif isinstance(values, list):
227 elif isinstance(values, list):
@@ -250,10 +230,10 b' class Widget(LoggingConfigurable):'
250 new_list.append(self._unpack_widgets(value))
230 new_list.append(self._unpack_widgets(value))
251 return new_list
231 return new_list
252 elif isinstance(values, string_types):
232 elif isinstance(values, string_types):
253 for widget in Widget.widgets:
233 if widget.model_id in Widget.widgets:
254 if widget.model_id == values:
234 return Widget.widgets[widget.model_id]
255 return widget
235 else:
256 return values
236 return values
257 else:
237 else:
258 return values
238 return values
259
239
@@ -266,12 +246,11 b' class Widget(LoggingConfigurable):'
266 content : dict
246 content : dict
267 Content of the message to send.
247 Content of the message to send.
268 """
248 """
269 self._send({"method": "custom",
249 self._send({"method": "custom", "custom_content": content})
270 "custom_content": content})
271
250
272
251
273 def on_msg(self, callback, remove=False): # TODO: Use lambdas and inspect here
252 def on_msg(self, callback, remove=False):
274 """Register or unregister a callback for when a custom msg is received
253 """Register or unregister a callback for when a custom msg is recieved
275 from the front-end.
254 from the front-end.
276
255
277 Parameters
256 Parameters
@@ -285,7 +264,24 b' class Widget(LoggingConfigurable):'
285 if remove and callback in self._msg_callbacks:
264 if remove and callback in self._msg_callbacks:
286 self._msg_callbacks.remove(callback)
265 self._msg_callbacks.remove(callback)
287 elif not remove and not callback in self._msg_callbacks:
266 elif not remove and not callback in self._msg_callbacks:
288 self._msg_callbacks.append(callback)
267 if callable(callback):
268 argspec = inspect.getargspec(callback)
269 nargs = len(argspec[0])
270
271 # Bound methods have an additional 'self' argument
272 if isinstance(callback, types.MethodType):
273 nargs -= 1
274
275 # Call the callback
276 if nargs == 1:
277 self._msg_callbacks.append(lambda sender, content: callback(content))
278 elif nargs == 2:
279 self._msg_callbacks.append(callback)
280 else:
281 raise TypeError('Widget msg callback must ' \
282 'accept 1 or 2 arguments, not %d.' % nargs)
283 else:
284 raise Exception('Callback must be callable.')
289
285
290
286
291 def on_displayed(self, callback, remove=False):
287 def on_displayed(self, callback, remove=False):
@@ -296,8 +292,6 b' class Widget(LoggingConfigurable):'
296 ----------
292 ----------
297 callback: method handler
293 callback: method handler
298 Can have a signature of:
294 Can have a signature of:
299 - callback()
300 - callback(sender)
301 - callback(sender, **kwargs)
295 - callback(sender, **kwargs)
302 kwargs from display call passed through without modification.
296 kwargs from display call passed through without modification.
303 remove: bool
297 remove: bool
@@ -305,11 +299,14 b' class Widget(LoggingConfigurable):'
305 if remove and callback in self._display_callbacks:
299 if remove and callback in self._display_callbacks:
306 self._display_callbacks.remove(callback)
300 self._display_callbacks.remove(callback)
307 elif not remove and not callback in self._display_callbacks:
301 elif not remove and not callback in self._display_callbacks:
308 self._display_callbacks.append(callback)
302 if callable(handler):
303 self._display_callbacks.append(callback)
304 else:
305 raise Exception('Callback must be callable.')
309
306
310
307
311 # Support methods
308 # Support methods
312 def _repr_widget_(self, **kwargs):
309 def _ipython_display_(self, **kwargs):
313 """Function that is called when `IPython.display.display` is called on
310 """Function that is called when `IPython.display.display` is called on
314 the widget."""
311 the widget."""
315
312
@@ -319,26 +316,6 b' class Widget(LoggingConfigurable):'
319 self._handle_displayed(**kwargs)
316 self._handle_displayed(**kwargs)
320
317
321
318
322 def _open_communication(self):
323 """Opens a communication with the front-end."""
324 # Create a comm.
325 self._comm = Comm(target_name=self.target_name)
326 self._comm.on_msg(self._handle_msg)
327 self._comm.on_close(self._close_communication)
328
329 # first update
330 self.send_state()
331
332
333 def _close_communication(self):
334 """Closes a communication with the front-end."""
335 if self._comm is not None:
336 try:
337 self._comm.close() # TODO: Check
338 finally:
339 self._comm = None
340
341
342 def _send(self, msg):
319 def _send(self, msg):
343 """Sends a message to the model in the front-end"""
320 """Sends a message to the model in the front-end"""
344 self.comm.send(msg)
321 self.comm.send(msg)
General Comments 0
You need to be logged in to leave comments. Login now