##// END OF EJS Templates
s/long thing/set_state
Jonathan Frederic -
Show More
@@ -1,439 +1,439 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 collections
17 17
18 18 from IPython.core.getipython import get_ipython
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, List, \
22 22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 23 from IPython.utils.py3compat import string_types
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28 class CallbackDispatcher(LoggingConfigurable):
29 29 """A structure for registering and running callbacks"""
30 30 callbacks = List()
31 31
32 32 def __call__(self, *args, **kwargs):
33 33 """Call all of the registered callbacks."""
34 34 value = None
35 35 for callback in self.callbacks:
36 36 try:
37 37 local_value = callback(*args, **kwargs)
38 38 except Exception as e:
39 39 ip = get_ipython()
40 40 if ip is None:
41 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 42 else:
43 43 ip.showtraceback()
44 44 else:
45 45 value = local_value if local_value is not None else value
46 46 return value
47 47
48 48 def register_callback(self, callback, remove=False):
49 49 """(Un)Register a callback
50 50
51 51 Parameters
52 52 ----------
53 53 callback: method handle
54 54 Method to be registered or unregistered.
55 55 remove=False: bool
56 56 Whether to unregister the callback."""
57 57
58 58 # (Un)Register the callback.
59 59 if remove and callback in self.callbacks:
60 60 self.callbacks.remove(callback)
61 61 elif not remove and callback not in self.callbacks:
62 62 self.callbacks.append(callback)
63 63
64 64 def _show_traceback(method):
65 65 """decorator for showing tracebacks in IPython"""
66 66 def m(self, *args, **kwargs):
67 67 try:
68 68 return(method(self, *args, **kwargs))
69 69 except Exception as e:
70 70 ip = get_ipython()
71 71 if ip is None:
72 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 73 else:
74 74 ip.showtraceback()
75 75 return m
76 76
77 77 class Widget(LoggingConfigurable):
78 78 #-------------------------------------------------------------------------
79 79 # Class attributes
80 80 #-------------------------------------------------------------------------
81 81 _widget_construction_callback = None
82 82 widgets = {}
83 83
84 84 @staticmethod
85 85 def on_widget_constructed(callback):
86 86 """Registers a callback to be called when a widget is constructed.
87 87
88 88 The callback must have the following signature:
89 89 callback(widget)"""
90 90 Widget._widget_construction_callback = callback
91 91
92 92 @staticmethod
93 93 def _call_widget_constructed(widget):
94 94 """Static method, called when a widget is constructed."""
95 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 96 Widget._widget_construction_callback(widget)
97 97
98 98 #-------------------------------------------------------------------------
99 99 # Traits
100 100 #-------------------------------------------------------------------------
101 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 102 registered in the front-end to create and sync this widget with.""")
103 103 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
104 104 to use to represent the widget.""", sync=True)
105 105 comm = Instance('IPython.kernel.comm.Comm')
106 106
107 107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 108 front-end can send before receiving an idle msg from the back-end.""")
109 109
110 110 keys = List()
111 111 def _keys_default(self):
112 112 return [name for name in self.traits(sync=True)]
113 113
114 114 _property_lock = Tuple((None, None))
115 115 _send_state_lock = Int(0)
116 116 _states_to_send = Set(allow_none=False)
117 117 _display_callbacks = Instance(CallbackDispatcher, ())
118 118 _msg_callbacks = Instance(CallbackDispatcher, ())
119 119
120 120 #-------------------------------------------------------------------------
121 121 # (Con/de)structor
122 122 #-------------------------------------------------------------------------
123 123 def __init__(self, **kwargs):
124 124 """Public constructor"""
125 125 self._model_id = kwargs.pop('model_id', None)
126 126 super(Widget, self).__init__(**kwargs)
127 127
128 128 self.on_trait_change(self._handle_property_changed, self.keys)
129 129 Widget._call_widget_constructed(self)
130 130 self.open()
131 131
132 132 def __del__(self):
133 133 """Object disposal"""
134 134 self.close()
135 135
136 136 #-------------------------------------------------------------------------
137 137 # Properties
138 138 #-------------------------------------------------------------------------
139 139
140 140 def open(self):
141 141 """Open a comm to the frontend if one isn't already open."""
142 142 if self.comm is None:
143 143 if self._model_id is None:
144 144 self.comm = Comm(target_name=self._model_name)
145 145 self._model_id = self.model_id
146 146 else:
147 147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
148 148 self.comm.on_msg(self._handle_msg)
149 149 Widget.widgets[self.model_id] = self
150 150
151 151 # first update
152 152 self.send_state()
153 153
154 154 @property
155 155 def model_id(self):
156 156 """Gets the model id of this widget.
157 157
158 158 If a Comm doesn't exist yet, a Comm will be created automagically."""
159 159 return self.comm.comm_id
160 160
161 161 #-------------------------------------------------------------------------
162 162 # Methods
163 163 #-------------------------------------------------------------------------
164 164
165 165 def close(self):
166 166 """Close method.
167 167
168 168 Closes the underlying comm.
169 169 When the comm is closed, all of the widget views are automatically
170 170 removed from the front-end."""
171 171 if self.comm is not None:
172 172 Widget.widgets.pop(self.model_id, None)
173 173 self.comm.close()
174 174 self.comm = None
175 175
176 176 def send_state(self, key=None):
177 177 """Sends the widget state, or a piece of it, to the front-end.
178 178
179 179 Parameters
180 180 ----------
181 181 key : unicode, or iterable (optional)
182 182 A single property's name or iterable of property names to sync with the front-end.
183 183 """
184 184 self._send({
185 185 "method" : "update",
186 186 "state" : self.get_state(key=key)
187 187 })
188 188
189 189 def get_state(self, key=None):
190 190 """Gets the widget state, or a piece of it.
191 191
192 192 Parameters
193 193 ----------
194 194 key : unicode or iterable (optional)
195 195 A single property's name or iterable of property names to get.
196 196 """
197 197 if key is None:
198 198 keys = self.keys
199 199 elif isinstance(key, string_types):
200 200 keys = [key]
201 201 elif isinstance(key, collections.Iterable):
202 202 keys = key
203 203 else:
204 204 raise ValueError("key must be a string, an iterable of keys, or None")
205 205 state = {}
206 206 for k in keys:
207 207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
208 208 value = getattr(self, k)
209 209 state[k] = f(value)
210 210 return state
211
212 def set_state(self, sync_data):
213 """Called when a state is received from the front-end."""
214 for name in self.keys:
215 if name in sync_data:
216 json_value = sync_data[name]
217 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
218 with self._lock_property(name, json_value):
219 setattr(self, name, from_json(json_value))
211 220
212 221 def send(self, content):
213 222 """Sends a custom msg to the widget model in the front-end.
214 223
215 224 Parameters
216 225 ----------
217 226 content : dict
218 227 Content of the message to send.
219 228 """
220 229 self._send({"method": "custom", "content": content})
221 230
222 231 def on_msg(self, callback, remove=False):
223 232 """(Un)Register a custom msg receive callback.
224 233
225 234 Parameters
226 235 ----------
227 236 callback: callable
228 237 callback will be passed two arguments when a message arrives::
229 238
230 239 callback(widget, content)
231 240
232 241 remove: bool
233 242 True if the callback should be unregistered."""
234 243 self._msg_callbacks.register_callback(callback, remove=remove)
235 244
236 245 def on_displayed(self, callback, remove=False):
237 246 """(Un)Register a widget displayed callback.
238 247
239 248 Parameters
240 249 ----------
241 250 callback: method handler
242 251 Must have a signature of::
243 252
244 253 callback(widget, **kwargs)
245 254
246 255 kwargs from display are passed through without modification.
247 256 remove: bool
248 257 True if the callback should be unregistered."""
249 258 self._display_callbacks.register_callback(callback, remove=remove)
250 259
251 260 #-------------------------------------------------------------------------
252 261 # Support methods
253 262 #-------------------------------------------------------------------------
254 263 @contextmanager
255 264 def _lock_property(self, key, value):
256 265 """Lock a property-value pair.
257 266
258 267 The value should be the JSON state of the property.
259 268
260 269 NOTE: This, in addition to the single lock for all state changes, is
261 270 flawed. In the future we may want to look into buffering state changes
262 271 back to the front-end."""
263 272 self._property_lock = (key, value)
264 273 try:
265 274 yield
266 275 finally:
267 276 self._property_lock = (None, None)
268 277
269 278 @contextmanager
270 279 def hold_sync(self):
271 280 """Hold syncing any state until the context manager is released"""
272 281 # We increment a value so that this can be nested. Syncing will happen when
273 282 # all levels have been released.
274 283 self._send_state_lock += 1
275 284 try:
276 285 yield
277 286 finally:
278 287 self._send_state_lock -=1
279 288 if self._send_state_lock == 0:
280 289 self.send_state(self._states_to_send)
281 290 self._states_to_send.clear()
282 291
283 292 def _should_send_property(self, key, value):
284 293 """Check the property lock (property_lock)"""
285 294 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
286 295 if (key == self._property_lock[0]
287 296 and to_json(value) == self._property_lock[1]):
288 297 return False
289 298 elif self._send_state_lock > 0:
290 299 self._states_to_send.add(key)
291 300 return False
292 301 else:
293 302 return True
294 303
295 304 # Event handlers
296 305 @_show_traceback
297 306 def _handle_msg(self, msg):
298 307 """Called when a msg is received from the front-end"""
299 308 data = msg['content']['data']
300 309 method = data['method']
301 310 if not method in ['backbone', 'custom']:
302 311 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
303 312
304 313 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
305 314 if method == 'backbone' and 'sync_data' in data:
306 315 sync_data = data['sync_data']
307 self._handle_receive_state(sync_data) # handles all methods
316 self.set_state(sync_data) # handles all methods
308 317
309 318 # Handle a custom msg from the front-end
310 319 elif method == 'custom':
311 320 if 'content' in data:
312 321 self._handle_custom_msg(data['content'])
313 322
314 def _handle_receive_state(self, sync_data):
315 """Called when a state is received from the front-end."""
316 for name in self.keys:
317 if name in sync_data:
318 json_value = sync_data[name]
319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
320 with self._lock_property(name, json_value):
321 setattr(self, name, from_json(json_value))
322
323 323 def _handle_custom_msg(self, content):
324 324 """Called when a custom msg is received."""
325 325 self._msg_callbacks(self, content)
326 326
327 327 def _handle_property_changed(self, name, old, new):
328 328 """Called when a property has been changed."""
329 329 # Make sure this isn't information that the front-end just sent us.
330 330 if self._should_send_property(name, new):
331 331 # Send new state to front-end
332 332 self.send_state(key=name)
333 333
334 334 def _handle_displayed(self, **kwargs):
335 335 """Called when a view has been displayed for this widget instance"""
336 336 self._display_callbacks(self, **kwargs)
337 337
338 338 def _trait_to_json(self, x):
339 339 """Convert a trait value to json
340 340
341 341 Traverse lists/tuples and dicts and serialize their values as well.
342 342 Replace any widgets with their model_id
343 343 """
344 344 if isinstance(x, dict):
345 345 return {k: self._trait_to_json(v) for k, v in x.items()}
346 346 elif isinstance(x, (list, tuple)):
347 347 return [self._trait_to_json(v) for v in x]
348 348 elif isinstance(x, Widget):
349 349 return "IPY_MODEL_" + x.model_id
350 350 else:
351 351 return x # Value must be JSON-able
352 352
353 353 def _trait_from_json(self, x):
354 354 """Convert json values to objects
355 355
356 356 Replace any strings representing valid model id values to Widget references.
357 357 """
358 358 if isinstance(x, dict):
359 359 return {k: self._trait_from_json(v) for k, v in x.items()}
360 360 elif isinstance(x, (list, tuple)):
361 361 return [self._trait_from_json(v) for v in x]
362 362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
363 363 # we want to support having child widgets at any level in a hierarchy
364 364 # trusting that a widget UUID will not appear out in the wild
365 365 return Widget.widgets[x[10:]]
366 366 else:
367 367 return x
368 368
369 369 def _ipython_display_(self, **kwargs):
370 370 """Called when `IPython.display.display` is called on the widget."""
371 371 # Show view.
372 372 if self._view_name is not None:
373 373 self._send({"method": "display"})
374 374 self._handle_displayed(**kwargs)
375 375
376 376 def _send(self, msg):
377 377 """Sends a message to the model in the front-end."""
378 378 self.comm.send(msg)
379 379
380 380
381 381 class DOMWidget(Widget):
382 382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
383 383 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
384 384 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
385 385
386 386 width = CUnicode(sync=True)
387 387 height = CUnicode(sync=True)
388 388 padding = CUnicode(sync=True)
389 389 margin = CUnicode(sync=True)
390 390
391 391 color = Unicode(sync=True)
392 392 background_color = Unicode(sync=True)
393 393 border_color = Unicode(sync=True)
394 394
395 395 border_width = CUnicode(sync=True)
396 396 border_radius = CUnicode(sync=True)
397 397 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
398 398 'none',
399 399 'hidden',
400 400 'dotted',
401 401 'dashed',
402 402 'solid',
403 403 'double',
404 404 'groove',
405 405 'ridge',
406 406 'inset',
407 407 'outset',
408 408 'initial',
409 409 'inherit', ''],
410 410 default_value='', sync=True)
411 411
412 412 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
413 413 'normal',
414 414 'italic',
415 415 'oblique',
416 416 'initial',
417 417 'inherit', ''],
418 418 default_value='', sync=True)
419 419 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
420 420 'normal',
421 421 'bold',
422 422 'bolder',
423 423 'lighter',
424 424 'initial',
425 425 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
426 426 default_value='', sync=True)
427 427 font_size = CUnicode(sync=True)
428 428 font_family = Unicode(sync=True)
429 429
430 430 def __init__(self, *pargs, **kwargs):
431 431 super(DOMWidget, self).__init__(*pargs, **kwargs)
432 432
433 433 def _validate_border(name, old, new):
434 434 if new is not None and new != '':
435 435 if name != 'border_width' and not self.border_width:
436 436 self.border_width = 1
437 437 if name != 'border_style' and self.border_style == '':
438 438 self.border_style = 'solid'
439 439 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now