##// END OF EJS Templates
work-in-progress for custom js serializers
Jason Grout -
Show More
@@ -0,0 +1,50 b''
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
3
4 define([
5 "base/js/utils"
6 ], function(utils){
7
8 return {
9 widget_serialization: {
10 deserialize: function deserialize_models(value, model) {
11 /**
12 * Replace model ids with models recursively.
13 */
14 var unpacked;
15 if ($.isArray(value)) {
16 unpacked = [];
17 _.each(value, function(sub_value, key) {
18 unpacked.push(deserialize_models(sub_value, model));
19 });
20 return Promise.all(unpacked);
21 } else if (value instanceof Object) {
22 unpacked = {};
23 _.each(value, function(sub_value, key) {
24 unpacked[key] = deserialize_models(sub_value, model);
25 });
26 return utils.resolve_promises_dict(unpacked);
27 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
28 // get_model returns a promise already
29 return model.widget_manager.get_model(value.slice(10, value.length));
30 } else {
31 return Promise.resolve(value);
32 }
33 },
34 },
35
36 list_of_numbers: {
37 deserialize: function (value, model) {
38 /* value is a DataView */
39 /* create a float64 typed array */
40 return new Float64Array(value.buffer)
41 },
42 serialize: function (value, model) {
43 return value;
44 },
45 }
46 }
47
48
49
50 });
@@ -32,6 +32,7 b' define(["widgets/js/manager",'
32 32 this.state_lock = null;
33 33 this.id = model_id;
34 34 this.views = {};
35 this.serializers = {};
35 36 this._resolve_received_state = {};
36 37
37 38 if (comm !== undefined) {
@@ -62,13 +63,13 b' define(["widgets/js/manager",'
62 63 return Backbone.Model.apply(this);
63 64 },
64 65
65 send: function (content, callbacks) {
66 send: function (content, callbacks, buffers) {
66 67 /**
67 68 * Send a custom msg over the comm.
68 69 */
69 70 if (this.comm !== undefined) {
70 71 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
72 this.comm.send(data, callbacks, {}, buffers);
72 73 this.pending_msgs++;
73 74 }
74 75 },
@@ -136,12 +137,37 b' define(["widgets/js/manager",'
136 137 * Handle incoming comm msg.
137 138 */
138 139 var method = msg.content.data.method;
140
139 141 var that = this;
140 142 switch (method) {
141 143 case 'update':
142 144 this.state_change = this.state_change
143 145 .then(function() {
144 return that.set_state(msg.content.data.state);
146 var state = msg.content.data.state || {};
147 var buffer_keys = msg.content.data.buffers || [];
148 var buffers = msg.buffers || [];
149 var metadata = msg.content.data.metadata || {};
150 var i,k;
151 for (var i=0; i<buffer_keys.length; i++) {
152 k = buffer_keys[i];
153 state[k] = buffers[i];
154 }
155
156 // for any metadata specifying a deserializer, set the
157 // state to a promise that resolves to the deserialized version
158 // also, store the serialization function for the attribute
159 var keys = Object.keys(metadata);
160 for (var i=0; i<keys.length; i++) {
161 k = keys[i];
162 if (metadata[k] && metadata[k].serialization) {
163 that.serializers[k] = utils.load_class.apply(that,
164 metadata[k].serialization);
165 state[k] = that.deserialize(that.serializers[k], state[k]);
166 }
167 }
168 return utils.resolve_promises_dict(state);
169 }).then(function(state) {
170 return that.set_state(state);
145 171 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 172 .then(function() {
147 173 var parent_id = msg.parent_header.msg_id;
@@ -152,7 +178,7 b' define(["widgets/js/manager",'
152 178 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 179 break;
154 180 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
181 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 182 break;
157 183 case 'display':
158 184 this.state_change = this.state_change.then(function() {
@@ -162,30 +188,39 b' define(["widgets/js/manager",'
162 188 }
163 189 },
164 190
191 deserialize: function(serializer, value) {
192 // given a serializer dict and a value,
193 // return a promise for the deserialized value
194 var that = this;
195 return serializer.then(function(s) {
196 if (s.deserialize) {
197 return s.deserialize(value, that);
198 } else {
199 return value;
200 }
201 });
202 },
203
165 204 set_state: function (state) {
166 205 var that = this;
167 206 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
207 return new Promise(function(resolve, reject) {
169 208 that.state_lock = state;
170 209 try {
171 210 WidgetModel.__super__.set.call(that, state);
172 211 } finally {
173 212 that.state_lock = null;
174 213 }
214 resolve();
175 215 }).catch(utils.reject("Couldn't set model state", true));
176 216 },
177 217
178 218 get_state: function() {
179 219 // Get the serializable state of the model.
180 var state = this.toJSON();
181 for (var key in state) {
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
220 // Equivalent to Backbone.Model.toJSON()
221 return _.clone(this.attributes);
187 222 },
188
223
189 224 _handle_status: function (msg, callbacks) {
190 225 /**
191 226 * Handle status msgs.
@@ -243,6 +278,19 b' define(["widgets/js/manager",'
243 278 * Handle sync to the back-end. Called when a model.save() is called.
244 279 *
245 280 * Make sure a comm exists.
281
282 * Parameters
283 * ----------
284 * method : create, update, patch, delete, read
285 * create/update always send the full attribute set
286 * patch - only send attributes listed in options.attrs, and if we are queuing
287 * up messages, combine with previous messages that have not been sent yet
288 * model : the model we are syncing
289 * will normally be the same as `this`
290 * options : dict
291 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
292 * otherwise, sync all attributes
293 *
246 294 */
247 295 var error = options.error || function() {
248 296 console.error('Backbone sync error:', arguments);
@@ -252,8 +300,11 b' define(["widgets/js/manager",'
252 300 return false;
253 301 }
254 302
255 // Delete any key value pairs that the back-end already knows about.
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
303 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
304
305 // the state_lock lists attributes that are currently be changed right now from a kernel message
306 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
307 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 308 if (this.state_lock !== null) {
258 309 var keys = Object.keys(this.state_lock);
259 310 for (var i=0; i<keys.length; i++) {
@@ -263,9 +314,7 b' define(["widgets/js/manager",'
263 314 }
264 315 }
265 316 }
266
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
317
269 318 if (_.size(attrs) > 0) {
270 319
271 320 // If this message was sent via backbone itself, it will not
@@ -297,9 +346,7 b' define(["widgets/js/manager",'
297 346 } else {
298 347 // We haven't exceeded the throttle, send the message like
299 348 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
301 this.comm.send(data, callbacks);
302 this.pending_msgs++;
349 this.send_sync_message(attrs, callbacks);
303 350 }
304 351 }
305 352 // Since the comm is a one-way communication, assume the message
@@ -308,68 +355,71 b' define(["widgets/js/manager",'
308 355 this._buffered_state_diff = {};
309 356 },
310 357
311 save_changes: function(callbacks) {
312 /**
313 * Push this model's state to the back-end
314 *
315 * This invokes a Backbone.Sync.
316 */
317 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
318 },
319 358
320 _pack_models: function(value) {
321 /**
322 * Replace models with model ids recursively.
323 */
359 send_sync_message: function(attrs, callbacks) {
360 // prepare and send a comm message syncing attrs
324 361 var that = this;
325 var packed;
326 if (value instanceof Backbone.Model) {
327 return "IPY_MODEL_" + value.id;
328
329 } else if ($.isArray(value)) {
330 packed = [];
331 _.each(value, function(sub_value, key) {
332 packed.push(that._pack_models(sub_value));
333 });
334 return packed;
335 } else if (value instanceof Date || value instanceof String) {
336 return value;
337 } else if (value instanceof Object) {
338 packed = {};
339 _.each(value, function(sub_value, key) {
340 packed[key] = that._pack_models(sub_value);
341 });
342 return packed;
343
344 } else {
345 return value;
362 // first, build a state dictionary with key=the attribute and the value
363 // being the value or the promise of the serialized value
364 var state_promise_dict = {};
365 var keys = Object.keys(attrs);
366 for (var i=0; i<keys.length; i++) {
367 // bind k and v locally; needed since we have an inner async function using v
368 (function(k,v) {
369 if (that.serializers[k]) {
370 state_promise_dict[k] = that.serializers[k].then(function(f) {
371 if (f.serialize) {
372 return f.serialize(v, that);
373 } else {
374 return v;
375 }
376 })
377 } else {
378 state_promise_dict[k] = v;
379 }
380 })(keys[i], attrs[keys[i]])
381 }
382 utils.resolve_promises_dict(state_promise_dict).then(function(state) {
383 // get binary values, then send
384 var keys = Object.keys(state);
385 var buffers = [];
386 var buffer_keys = [];
387 for (var i=0; i<keys.length; i++) {
388 var key = keys[i];
389 var value = state[key];
390 if (value.buffer instanceof ArrayBuffer
391 || value instanceof ArrayBuffer) {
392 buffers.push(value);
393 buffer_keys.push(key);
394 delete state[key];
395 }
396 }
397 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
398 that.pending_msgs++;
399 })
400 },
401
402 serialize: function(model, attrs) {
403 // Serialize the attributes into a sync message
404 var keys = Object.keys(attrs);
405 var key, value;
406 var buffers, metadata, buffer_keys, serialize;
407 for (var i=0; i<keys.length; i++) {
408 key = keys[i];
409 serialize = model.serializers[key];
410 if (serialize && serialize.serialize) {
411 attrs[key] = serialize.serialize(attrs[key]);
412 }
346 413 }
347 414 },
348 415
349 _unpack_models: function(value) {
416 save_changes: function(callbacks) {
350 417 /**
351 * Replace model ids with models recursively.
418 * Push this model's state to the back-end
419 *
420 * This invokes a Backbone.Sync.
352 421 */
353 var that = this;
354 var unpacked;
355 if ($.isArray(value)) {
356 unpacked = [];
357 _.each(value, function(sub_value, key) {
358 unpacked.push(that._unpack_models(sub_value));
359 });
360 return Promise.all(unpacked);
361 } else if (value instanceof Object) {
362 unpacked = {};
363 _.each(value, function(sub_value, key) {
364 unpacked[key] = that._unpack_models(sub_value);
365 });
366 return utils.resolve_promises_dict(unpacked);
367 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
368 // get_model returns a promise already
369 return this.widget_manager.get_model(value.slice(10, value.length));
370 } else {
371 return Promise.resolve(value);
372 }
422 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
373 423 },
374 424
375 425 on_some_change: function(keys, callback, context) {
@@ -386,7 +436,7 b' define(["widgets/js/manager",'
386 436 }
387 437 }, this);
388 438
389 },
439 },
390 440 });
391 441 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392 442
@@ -444,11 +494,11 b' define(["widgets/js/manager",'
444 494 */
445 495 },
446 496
447 send: function (content) {
497 send: function (content, buffers) {
448 498 /**
449 499 * Send a custom msg associated with this view.
450 500 */
451 this.model.send(content, this.callbacks());
501 this.model.send(content, this.callbacks(), buffers);
452 502 },
453 503
454 504 touch: function () {
@@ -89,6 +89,47 b' def register(key=None):'
89 89 return wrap
90 90
91 91
92 def _widget_to_json(x):
93 if isinstance(x, dict):
94 return {k: _widget_to_json(v) for k, v in x.items()}
95 elif isinstance(x, (list, tuple)):
96 return [_widget_to_json(v) for v in x]
97 elif isinstance(x, Widget):
98 return "IPY_MODEL_" + x.model_id
99 else:
100 return x
101
102 def _json_to_widget(x):
103 if isinstance(x, dict):
104 return {k: _json_to_widget(v) for k, v in x.items()}
105 elif isinstance(x, (list, tuple)):
106 return [_json_to_widget(v) for v in x]
107 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
108 return Widget.widgets[x[10:]]
109 else:
110 return x
111
112 widget_serialization = {
113 'from_json': _json_to_widget,
114 'to_json': lambda x: (_widget_to_json(x), {'serialization': ('widget_serialization', 'widgets/js/types')})
115 }
116
117 def _to_binary_list(x):
118 import numpy
119 return memoryview(numpy.array(x, dtype=float)), {'serialization': ('list_of_numbers', 'widgets/js/types')}
120
121 def _from_binary_list(x):
122 import numpy
123 a = numpy.frombuffer(x.tobytes(), dtype=float)
124 return list(a)
125
126 list_of_numbers = {
127 'from_json': _from_binary_list,
128 'to_json': _to_binary_list
129 }
130
131
132
92 133 class Widget(LoggingConfigurable):
93 134 #-------------------------------------------------------------------------
94 135 # Class attributes
@@ -216,10 +257,13 b' class Widget(LoggingConfigurable):'
216 257 key : unicode, or iterable (optional)
217 258 A single property's name or iterable of property names to sync with the front-end.
218 259 """
219 self._send({
220 "method" : "update",
221 "state" : self.get_state(key=key)
222 })
260 state, buffer_keys, buffers, metadata = self.get_state(key=key)
261 msg = {"method": "update", "state": state}
262 if buffer_keys:
263 msg['buffers'] = buffer_keys
264 if metadata:
265 msg['metadata'] = metadata
266 self._send(msg, buffers=buffers)
223 267
224 268 def get_state(self, key=None):
225 269 """Gets the widget state, or a piece of it.
@@ -228,6 +272,16 b' class Widget(LoggingConfigurable):'
228 272 ----------
229 273 key : unicode or iterable (optional)
230 274 A single property's name or iterable of property names to get.
275
276 Returns
277 -------
278 state : dict of states
279 buffer_keys : list of strings
280 the values that are stored in buffers
281 buffers : list of binary memoryviews
282 values to transmit in binary
283 metadata : dict
284 metadata for each field: {key: metadata}
231 285 """
232 286 if key is None:
233 287 keys = self.keys
@@ -238,11 +292,21 b' class Widget(LoggingConfigurable):'
238 292 else:
239 293 raise ValueError("key must be a string, an iterable of keys, or None")
240 294 state = {}
295 buffers = []
296 buffer_keys = []
297 metadata = {}
241 298 for k in keys:
242 299 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 300 value = getattr(self, k)
244 state[k] = f(value)
245 return state
301 serialized, md = f(value)
302 if isinstance(serialized, memoryview):
303 buffers.append(serialized)
304 buffer_keys.append(k)
305 else:
306 state[k] = serialized
307 if md is not None:
308 metadata[k] = md
309 return state, buffer_keys, buffers, metadata
246 310
247 311 def set_state(self, sync_data):
248 312 """Called when a state is received from the front-end."""
@@ -253,15 +317,17 b' class Widget(LoggingConfigurable):'
253 317 with self._lock_property(name, json_value):
254 318 setattr(self, name, from_json(json_value))
255 319
256 def send(self, content):
320 def send(self, content, buffers=None):
257 321 """Sends a custom msg to the widget model in the front-end.
258 322
259 323 Parameters
260 324 ----------
261 325 content : dict
262 326 Content of the message to send.
327 buffers : list of binary buffers
328 Binary buffers to send with message
263 329 """
264 self._send({"method": "custom", "content": content})
330 self._send({"method": "custom", "content": content}, buffers=buffers)
265 331
266 332 def on_msg(self, callback, remove=False):
267 333 """(Un)Register a custom msg receive callback.
@@ -269,9 +335,9 b' class Widget(LoggingConfigurable):'
269 335 Parameters
270 336 ----------
271 337 callback: callable
272 callback will be passed two arguments when a message arrives::
338 callback will be passed three arguments when a message arrives::
273 339
274 callback(widget, content)
340 callback(widget, content, buffers)
275 341
276 342 remove: bool
277 343 True if the callback should be unregistered."""
@@ -346,7 +412,10 b' class Widget(LoggingConfigurable):'
346 412 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
347 413 if method == 'backbone':
348 414 if 'sync_data' in data:
415 # get binary buffers too
349 416 sync_data = data['sync_data']
417 for i,k in enumerate(data.get('buffer_keys', [])):
418 sync_data[k] = msg['buffers'][i]
350 419 self.set_state(sync_data) # handles all methods
351 420
352 421 # Handle a state request.
@@ -356,15 +425,15 b' class Widget(LoggingConfigurable):'
356 425 # Handle a custom msg from the front-end.
357 426 elif method == 'custom':
358 427 if 'content' in data:
359 self._handle_custom_msg(data['content'])
428 self._handle_custom_msg(data['content'], msg['buffers'])
360 429
361 430 # Catch remainder.
362 431 else:
363 432 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
364 433
365 def _handle_custom_msg(self, content):
434 def _handle_custom_msg(self, content, buffers):
366 435 """Called when a custom msg is received."""
367 self._msg_callbacks(self, content)
436 self._msg_callbacks(self, content, buffers)
368 437
369 438 def _notify_trait(self, name, old_value, new_value):
370 439 """Called when a property has been changed."""
@@ -391,30 +460,14 b' class Widget(LoggingConfigurable):'
391 460 Traverse lists/tuples and dicts and serialize their values as well.
392 461 Replace any widgets with their model_id
393 462 """
394 if isinstance(x, dict):
395 return {k: self._trait_to_json(v) for k, v in x.items()}
396 elif isinstance(x, (list, tuple)):
397 return [self._trait_to_json(v) for v in x]
398 elif isinstance(x, Widget):
399 return "IPY_MODEL_" + x.model_id
400 else:
401 return x # Value must be JSON-able
463 return x, None
402 464
403 465 def _trait_from_json(self, x):
404 466 """Convert json values to objects
405 467
406 468 Replace any strings representing valid model id values to Widget references.
407 469 """
408 if isinstance(x, dict):
409 return {k: self._trait_from_json(v) for k, v in x.items()}
410 elif isinstance(x, (list, tuple)):
411 return [self._trait_from_json(v) for v in x]
412 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
413 # we want to support having child widgets at any level in a hierarchy
414 # trusting that a widget UUID will not appear out in the wild
415 return Widget.widgets[x[10:]]
416 else:
417 return x
470 return x
418 471
419 472 def _ipython_display_(self, **kwargs):
420 473 """Called when `IPython.display.display` is called on the widget."""
@@ -423,9 +476,9 b' class Widget(LoggingConfigurable):'
423 476 self._send({"method": "display"})
424 477 self._handle_displayed(**kwargs)
425 478
426 def _send(self, msg):
479 def _send(self, msg, buffers=None):
427 480 """Sends a message to the model in the front-end."""
428 self.comm.send(msg)
481 self.comm.send(data=msg, buffers=buffers)
429 482
430 483
431 484 class DOMWidget(Widget):
@@ -6,7 +6,7 b' Represents a container that can be used to group other widgets.'
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, register, widget_serialization
10 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 11 from IPython.utils.warn import DeprecatedClass
12 12
@@ -18,7 +18,9 b' class Box(DOMWidget):'
18 18 # Child widgets in the container.
19 19 # Using a tuple here to force reassignment to update the list.
20 20 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
21 # TODO: make this tuple serialize models
22 # TODO: enforce that tuples here have a single datatype
23 children = Tuple(sync=True, **widget_serialization)
22 24
23 25 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 26 overflow_x = CaselessStrEnum(
@@ -67,7 +67,7 b' class Button(DOMWidget):'
67 67 Set to true to remove the callback from the list of callbacks."""
68 68 self._click_handlers.register_callback(callback, remove=remove)
69 69
70 def _handle_button_msg(self, _, content):
70 def _handle_button_msg(self, _, content, buffers):
71 71 """Handle a msg from the front-end.
72 72
73 73 Parameters
General Comments 0
You need to be logged in to leave comments. Login now