##// END OF EJS Templates
Merge pull request #7757 from jasongrout/custom-serialization...
Jonathan Frederic -
r21035:d6e249b0 merge
parent child Browse files
Show More
@@ -3,6 +3,7 b''
3
3
4 define([
4 define([
5 "widgets/js/manager",
5 "widgets/js/manager",
6 "widgets/js/widget",
6 "widgets/js/widget_link",
7 "widgets/js/widget_link",
7 "widgets/js/widget_bool",
8 "widgets/js/widget_bool",
8 "widgets/js/widget_button",
9 "widgets/js/widget_button",
@@ -14,21 +15,20 b' define(['
14 "widgets/js/widget_selection",
15 "widgets/js/widget_selection",
15 "widgets/js/widget_selectioncontainer",
16 "widgets/js/widget_selectioncontainer",
16 "widgets/js/widget_string",
17 "widgets/js/widget_string",
17 ], function(widgetmanager, linkModels) {
18 ], function(widgetmanager, widget) {
18 for (var target_name in linkModels) {
19 // Register all of the loaded models and views with the widget manager.
19 if (linkModels.hasOwnProperty(target_name)) {
20 widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]);
21 }
22 }
23
24 // Register all of the loaded views with the widget manager.
25 for (var i = 2; i < arguments.length; i++) {
20 for (var i = 2; i < arguments.length; i++) {
26 for (var target_name in arguments[i]) {
21 var module = arguments[i];
27 if (arguments[i].hasOwnProperty(target_name)) {
22 for (var target_name in module) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
23 if (module.hasOwnProperty(target_name)) {
24 var target = module[target_name];
25 if (target.prototype instanceof widget.WidgetModel) {
26 widgetmanager.WidgetManager.register_widget_model(target_name, target);
27 } else if (target.prototype instanceof widget.WidgetView) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, target);
29 }
29 }
30 }
30 }
31 }
31 }
32 }
32
33 return {'WidgetManager': widgetmanager.WidgetManager};
33 return {'WidgetManager': widgetmanager.WidgetManager};
34 });
34 });
@@ -62,13 +62,13 b' define(["widgets/js/manager",'
62 return Backbone.Model.apply(this);
62 return Backbone.Model.apply(this);
63 },
63 },
64
64
65 send: function (content, callbacks) {
65 send: function (content, callbacks, buffers) {
66 /**
66 /**
67 * Send a custom msg over the comm.
67 * Send a custom msg over the comm.
68 */
68 */
69 if (this.comm !== undefined) {
69 if (this.comm !== undefined) {
70 var data = {method: 'custom', content: content};
70 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
71 this.comm.send(data, callbacks, {}, buffers);
72 this.pending_msgs++;
72 this.pending_msgs++;
73 }
73 }
74 },
74 },
@@ -136,12 +136,31 b' define(["widgets/js/manager",'
136 * Handle incoming comm msg.
136 * Handle incoming comm msg.
137 */
137 */
138 var method = msg.content.data.method;
138 var method = msg.content.data.method;
139
139 var that = this;
140 var that = this;
140 switch (method) {
141 switch (method) {
141 case 'update':
142 case 'update':
142 this.state_change = this.state_change
143 this.state_change = this.state_change
143 .then(function() {
144 .then(function() {
144 return that.set_state(msg.content.data.state);
145 var state = msg.content.data.state || {};
146 var buffer_keys = msg.content.data.buffers || [];
147 var buffers = msg.buffers || [];
148 for (var i=0; i<buffer_keys.length; i++) {
149 state[buffer_keys[i]] = buffers[i];
150 }
151
152 // deserialize fields that have custom deserializers
153 var serializers = that.constructor.serializers;
154 if (serializers) {
155 for (var k in state) {
156 if (serializers[k] && serializers[k].deserialize) {
157 state[k] = (serializers[k].deserialize)(state[k], that);
158 }
159 }
160 }
161 return utils.resolve_promises_dict(state);
162 }).then(function(state) {
163 return that.set_state(state);
145 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
164 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 .then(function() {
165 .then(function() {
147 var parent_id = msg.parent_header.msg_id;
166 var parent_id = msg.parent_header.msg_id;
@@ -152,7 +171,7 b' define(["widgets/js/manager",'
152 }).catch(utils.reject("Couldn't resolve state request promise", true));
171 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 break;
172 break;
154 case 'custom':
173 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
174 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 break;
175 break;
157 case 'display':
176 case 'display':
158 this.state_change = this.state_change.then(function() {
177 this.state_change = this.state_change.then(function() {
@@ -165,25 +184,21 b' define(["widgets/js/manager",'
165 set_state: function (state) {
184 set_state: function (state) {
166 var that = this;
185 var that = this;
167 // Handle when a widget is updated via the python side.
186 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
187 return new Promise(function(resolve, reject) {
169 that.state_lock = state;
188 that.state_lock = state;
170 try {
189 try {
171 WidgetModel.__super__.set.call(that, state);
190 WidgetModel.__super__.set.call(that, state);
172 } finally {
191 } finally {
173 that.state_lock = null;
192 that.state_lock = null;
174 }
193 }
194 resolve();
175 }).catch(utils.reject("Couldn't set model state", true));
195 }).catch(utils.reject("Couldn't set model state", true));
176 },
196 },
177
197
178 get_state: function() {
198 get_state: function() {
179 // Get the serializable state of the model.
199 // Get the serializable state of the model.
180 var state = this.toJSON();
200 // Equivalent to Backbone.Model.toJSON()
181 for (var key in state) {
201 return _.clone(this.attributes);
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
187 },
202 },
188
203
189 _handle_status: function (msg, callbacks) {
204 _handle_status: function (msg, callbacks) {
@@ -243,6 +258,19 b' define(["widgets/js/manager",'
243 * Handle sync to the back-end. Called when a model.save() is called.
258 * Handle sync to the back-end. Called when a model.save() is called.
244 *
259 *
245 * Make sure a comm exists.
260 * Make sure a comm exists.
261
262 * Parameters
263 * ----------
264 * method : create, update, patch, delete, read
265 * create/update always send the full attribute set
266 * patch - only send attributes listed in options.attrs, and if we are queuing
267 * up messages, combine with previous messages that have not been sent yet
268 * model : the model we are syncing
269 * will normally be the same as `this`
270 * options : dict
271 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
272 * otherwise, sync all attributes
273 *
246 */
274 */
247 var error = options.error || function() {
275 var error = options.error || function() {
248 console.error('Backbone sync error:', arguments);
276 console.error('Backbone sync error:', arguments);
@@ -252,8 +280,11 b' define(["widgets/js/manager",'
252 return false;
280 return false;
253 }
281 }
254
282
255 // Delete any key value pairs that the back-end already knows about.
283 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
284
285 // the state_lock lists attributes that are currently be changed right now from a kernel message
286 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
287 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 if (this.state_lock !== null) {
288 if (this.state_lock !== null) {
258 var keys = Object.keys(this.state_lock);
289 var keys = Object.keys(this.state_lock);
259 for (var i=0; i<keys.length; i++) {
290 for (var i=0; i<keys.length; i++) {
@@ -264,8 +295,6 b' define(["widgets/js/manager",'
264 }
295 }
265 }
296 }
266
297
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
269 if (_.size(attrs) > 0) {
298 if (_.size(attrs) > 0) {
270
299
271 // If this message was sent via backbone itself, it will not
300 // If this message was sent via backbone itself, it will not
@@ -297,8 +326,7 b' define(["widgets/js/manager",'
297 } else {
326 } else {
298 // We haven't exceeded the throttle, send the message like
327 // We haven't exceeded the throttle, send the message like
299 // normal.
328 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
329 this.send_sync_message(attrs, callbacks);
301 this.comm.send(data, callbacks);
302 this.pending_msgs++;
330 this.pending_msgs++;
303 }
331 }
304 }
332 }
@@ -308,68 +336,49 b' define(["widgets/js/manager",'
308 this._buffered_state_diff = {};
336 this._buffered_state_diff = {};
309 },
337 },
310
338
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
339
320 _pack_models: function(value) {
340 send_sync_message: function(attrs, callbacks) {
321 /**
341 // prepare and send a comm message syncing attrs
322 * Replace models with model ids recursively.
323 */
324 var that = this;
342 var that = this;
325 var packed;
343 // first, build a state dictionary with key=the attribute and the value
326 if (value instanceof Backbone.Model) {
344 // being the value or the promise of the serialized value
327 return "IPY_MODEL_" + value.id;
345 var serializers = this.constructor.serializers;
328
346 if (serializers) {
329 } else if ($.isArray(value)) {
347 for (k in attrs) {
330 packed = [];
348 if (serializers[k] && serializers[k].serialize) {
331 _.each(value, function(sub_value, key) {
349 attrs[k] = (serializers[k].serialize)(attrs[k], this);
332 packed.push(that._pack_models(sub_value));
350 }
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;
346 }
351 }
352 }
353 utils.resolve_promises_dict(attrs).then(function(state) {
354 // get binary values, then send
355 var keys = Object.keys(state);
356 var buffers = [];
357 var buffer_keys = [];
358 for (var i=0; i<keys.length; i++) {
359 var key = keys[i];
360 var value = state[key];
361 if (value.buffer instanceof ArrayBuffer
362 || value instanceof ArrayBuffer) {
363 buffers.push(value);
364 buffer_keys.push(key);
365 delete state[key];
366 }
367 }
368 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
369 }).catch(function(error) {
370 that.pending_msgs--;
371 return (utils.reject("Couldn't send widget sync message", true))(error);
372 });
347 },
373 },
348
374
349 _unpack_models: function(value) {
375 save_changes: function(callbacks) {
350 /**
376 /**
351 * Replace model ids with models recursively.
377 * Push this model's state to the back-end
378 *
379 * This invokes a Backbone.Sync.
352 */
380 */
353 var that = this;
381 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
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 }
373 },
382 },
374
383
375 on_some_change: function(keys, callback, context) {
384 on_some_change: function(keys, callback, context) {
@@ -387,6 +396,14 b' define(["widgets/js/manager",'
387 }, this);
396 }, this);
388
397
389 },
398 },
399
400 toJSON: function(options) {
401 /**
402 * Serialize the model. See the types.js deserialization function
403 * and the kernel-side serializer/deserializer
404 */
405 return "IPY_MODEL_"+this.id;
406 }
390 });
407 });
391 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
408 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392
409
@@ -426,7 +443,7 b' define(["widgets/js/manager",'
426 */
443 */
427 var that = this;
444 var that = this;
428 options = $.extend({ parent: this }, options || {});
445 options = $.extend({ parent: this }, options || {});
429 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
446 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true));
430 },
447 },
431
448
432 callbacks: function(){
449 callbacks: function(){
@@ -444,11 +461,11 b' define(["widgets/js/manager",'
444 */
461 */
445 },
462 },
446
463
447 send: function (content) {
464 send: function (content, buffers) {
448 /**
465 /**
449 * Send a custom msg associated with this view.
466 * Send a custom msg associated with this view.
450 */
467 */
451 this.model.send(content, this.callbacks());
468 this.model.send(content, this.callbacks(), buffers);
452 },
469 },
453
470
454 touch: function () {
471 touch: function () {
@@ -558,7 +575,7 b' define(["widgets/js/manager",'
558 /**
575 /**
559 * Makes browser interpret a numerical string as a pixel value.
576 * Makes browser interpret a numerical string as a pixel value.
560 */
577 */
561 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
578 if (value && /^\d+\.?(\d+)?$/.test(value.trim())) {
562 return value.trim() + 'px';
579 return value.trim() + 'px';
563 }
580 }
564 return value;
581 return value;
@@ -4,10 +4,41 b''
4 define([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "underscore",
7 "base/js/utils",
8 "base/js/utils",
8 "bootstrap",
9 "bootstrap",
9 ], function(widget, $, utils){
10 ], function(widget, $, _, utils){
10 "use strict";
11 "use strict";
12 var unpack_models = function unpack_models(value, model) {
13 /**
14 * Replace model ids with models recursively.
15 */
16 var unpacked;
17 if ($.isArray(value)) {
18 unpacked = [];
19 _.each(value, function(sub_value, key) {
20 unpacked.push(unpack_models(sub_value, model));
21 });
22 return Promise.all(unpacked);
23 } else if (value instanceof Object) {
24 unpacked = {};
25 _.each(value, function(sub_value, key) {
26 unpacked[key] = unpack_models(sub_value, model);
27 });
28 return utils.resolve_promises_dict(unpacked);
29 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
30 // get_model returns a promise already
31 return model.widget_manager.get_model(value.slice(10, value.length));
32 } else {
33 return Promise.resolve(value);
34 }
35 };
36
37 var BoxModel = widget.WidgetModel.extend({}, {
38 serializers: _.extend({
39 children: {deserialize: unpack_models}
40 }, widget.WidgetModel.serializers)
41 });
11
42
12 var BoxView = widget.DOMWidgetView.extend({
43 var BoxView = widget.DOMWidgetView.extend({
13 initialize: function(){
44 initialize: function(){
@@ -148,6 +179,8 b' define(['
148 });
179 });
149
180
150 return {
181 return {
182 'unpack_models': unpack_models,
183 'BoxModel': BoxModel,
151 'BoxView': BoxView,
184 'BoxView': BoxView,
152 'FlexBoxView': FlexBoxView,
185 'FlexBoxView': FlexBoxView,
153 };
186 };
@@ -337,6 +337,19 b' casper.execute_cell_then = function(index, then_callback, expect_failure) {'
337 return return_val;
337 return return_val;
338 };
338 };
339
339
340 casper.append_cell_execute_then = function(text, then_callback, expect_failure) {
341 // Append a code cell and execute it, optionally calling a then_callback
342 var c = this.append_cell(text);
343 return this.execute_cell_then(c, then_callback, expect_failure);
344 };
345
346 casper.assert_output_equals = function(text, output_text, message) {
347 // Append a code cell with the text, then assert the output is equal to output_text
348 this.append_cell_execute_then(text, function(index) {
349 this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message);
350 });
351 };
352
340 casper.wait_for_element = function(index, selector){
353 casper.wait_for_element = function(index, selector){
341 // Utility function that allows us to easily wait for an element
354 // Utility function that allows us to easily wait for an element
342 // within a cell. Uses JQuery selector to look for the element.
355 // within a cell. Uses JQuery selector to look for the element.
@@ -40,63 +40,12 b' casper.notebook_test(function () {'
40 var index;
40 var index;
41
41
42 index = this.append_cell(
42 index = this.append_cell(
43 'from IPython.html import widgets\n' +
43 ['from IPython.html import widgets',
44 'from IPython.display import display, clear_output\n' +
44 'from IPython.display import display, clear_output',
45 'print("Success")');
45 'print("Success")'].join('\n'));
46 this.execute_cell_then(index);
46 this.execute_cell_then(index);
47
47
48 this.then(function () {
48 this.then(function () {
49
50 // Functions that can be used to test the packing and unpacking APIs
51 var that = this;
52 var test_pack = function (input) {
53 var output = that.evaluate(function(input) {
54 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
55 var results = model._pack_models(input);
56 return results;
57 }, {input: input});
58 that.test.assert(recursive_compare(input, output),
59 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
60 };
61 var test_unpack = function (input) {
62 that.thenEvaluate(function(input) {
63 window.results = undefined;
64 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
65 model._unpack_models(input).then(function(results) {
66 window.results = results;
67 });
68 }, {input: input});
69
70 that.waitFor(function check() {
71 return that.evaluate(function() {
72 return window.results;
73 });
74 });
75
76 that.then(function() {
77 var results = that.evaluate(function() {
78 return window.results;
79 });
80 that.test.assert(recursive_compare(input, results),
81 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
82 });
83 };
84 var test_packing = function(input) {
85 test_pack(input);
86 test_unpack(input);
87 };
88
89 test_packing({0: 'hi', 1: 'bye'});
90 test_packing(['hi', 'bye']);
91 test_packing(['hi', 5]);
92 test_packing(['hi', '5']);
93 test_packing([1.0, 0]);
94 test_packing([1.0, false]);
95 test_packing([1, false]);
96 test_packing([1, false, {a: 'hi'}]);
97 test_packing([1, false, ['hi']]);
98 test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")])
99
100 // Test multi-set, single touch code. First create a custom widget.
49 // Test multi-set, single touch code. First create a custom widget.
101 this.thenEvaluate(function() {
50 this.thenEvaluate(function() {
102 var MultiSetView = IPython.DOMWidgetView.extend({
51 var MultiSetView = IPython.DOMWidgetView.extend({
@@ -113,20 +62,20 b' casper.notebook_test(function () {'
113
62
114 // Try creating the multiset widget, verify that sets the values correctly.
63 // Try creating the multiset widget, verify that sets the values correctly.
115 var multiset = {};
64 var multiset = {};
116 multiset.index = this.append_cell(
65 multiset.index = this.append_cell([
117 'from IPython.utils.traitlets import Unicode, CInt\n' +
66 'from IPython.utils.traitlets import Unicode, CInt',
118 'class MultiSetWidget(widgets.Widget):\n' +
67 'class MultiSetWidget(widgets.Widget):',
119 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
68 ' _view_name = Unicode("MultiSetView", sync=True)',
120 ' a = CInt(0, sync=True)\n' +
69 ' a = CInt(0, sync=True)',
121 ' b = CInt(0, sync=True)\n' +
70 ' b = CInt(0, sync=True)',
122 ' c = CInt(0, sync=True)\n' +
71 ' c = CInt(0, sync=True)',
123 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
72 ' d = CInt(-1, sync=True)', // See if it sends a full state.
124 ' def set_state(self, sync_data):\n' +
73 ' def set_state(self, sync_data):',
125 ' widgets.Widget.set_state(self, sync_data)\n'+
74 ' widgets.Widget.set_state(self, sync_data)',
126 ' self.d = len(sync_data)\n' +
75 ' self.d = len(sync_data)',
127 'multiset = MultiSetWidget()\n' +
76 'multiset = MultiSetWidget()',
128 'display(multiset)\n' +
77 'display(multiset)',
129 'print(multiset.model_id)');
78 'print(multiset.model_id)'].join('\n'));
130 this.execute_cell_then(multiset.index, function(index) {
79 this.execute_cell_then(multiset.index, function(index) {
131 multiset.model_id = this.get_output_cell(index).text.trim();
80 multiset.model_id = this.get_output_cell(index).text.trim();
132 });
81 });
@@ -148,16 +97,16 b' casper.notebook_test(function () {'
148 });
97 });
149
98
150 var textbox = {};
99 var textbox = {};
151 throttle_index = this.append_cell(
100 throttle_index = this.append_cell([
152 'import time\n' +
101 'import time',
153 'textbox = widgets.Text()\n' +
102 'textbox = widgets.Text()',
154 'display(textbox)\n' +
103 'display(textbox)',
155 'textbox._dom_classes = ["my-throttle-textbox"]\n' +
104 'textbox._dom_classes = ["my-throttle-textbox"]',
156 'def handle_change(name, old, new):\n' +
105 'def handle_change(name, old, new):',
157 ' display(len(new))\n' +
106 ' display(len(new))',
158 ' time.sleep(0.5)\n' +
107 ' time.sleep(0.5)',
159 'textbox.on_trait_change(handle_change, "value")\n' +
108 'textbox.on_trait_change(handle_change, "value")',
160 'print(textbox.model_id)');
109 'print(textbox.model_id)'].join('\n'));
161 this.execute_cell_then(throttle_index, function(index){
110 this.execute_cell_then(throttle_index, function(index){
162 textbox.model_id = this.get_output_cell(index).text.trim();
111 textbox.model_id = this.get_output_cell(index).text.trim();
163
112
@@ -169,7 +118,7 b' casper.notebook_test(function () {'
169 '.my-throttle-textbox'), 'Textbox exists.');
118 '.my-throttle-textbox'), 'Textbox exists.');
170
119
171 // Send 20 characters
120 // Send 20 characters
172 this.sendKeys('.my-throttle-textbox input', '....................');
121 this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
173 });
122 });
174
123
175 this.wait_for_widget(textbox);
124 this.wait_for_widget(textbox);
@@ -188,4 +137,173 b' casper.notebook_test(function () {'
188 var last_state = outputs[outputs.length-1].data['text/plain'];
137 var last_state = outputs[outputs.length-1].data['text/plain'];
189 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
138 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
190 });
139 });
140
141
142 this.thenEvaluate(function() {
143 define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) {
144 var floatArray = {
145 deserialize: function (value, model) {
146 if (value===null) {return null;}
147 // DataView -> float64 typed array
148 return new Float64Array(value.buffer);
149 },
150 // serialization automatically handled since the
151 // attribute is an ArrayBuffer view
152 };
153
154 var floatList = {
155 deserialize: function (value, model) {
156 // list of floats -> list of strings
157 return value.map(function(x) {return x.toString()});
158 },
159 serialize: function(value, model) {
160 // list of strings -> list of floats
161 return value.map(function(x) {return parseFloat(x);})
162 }
163 };
164
165 var TestWidgetModel = widget.WidgetModel.extend({}, {
166 serializers: _.extend({
167 array_list: floatList,
168 array_binary: floatArray
169 }, widget.WidgetModel.serializers)
170 });
171
172 var TestWidgetView = widget.DOMWidgetView.extend({
173 render: function () {
174 this.listenTo(this.model, 'msg:custom', this.handle_msg);
175 },
176 handle_msg: function(content, buffers) {
177 this.msg = [content, buffers];
178 }
179 });
180
181 return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView};
182 });
183 });
184
185 var testwidget = {};
186 this.append_cell_execute_then([
187 'from IPython.html import widgets',
188 'from IPython.utils.traitlets import Unicode, Instance, List',
189 'from IPython.display import display',
190 'from array import array',
191 'def _array_to_memoryview(x):',
192 ' if x is None: return None',
193 ' try:',
194 ' y = memoryview(x)',
195 ' except TypeError:',
196 ' # in python 2, arrays do not support the new buffer protocol',
197 ' y = memoryview(buffer(x))',
198 ' return y',
199 'def _memoryview_to_array(x):',
200 ' if x is None: return None',
201 ' return array("d", x.tobytes())',
202 'arrays_binary = {',
203 ' "from_json": _memoryview_to_array,',
204 ' "to_json": _array_to_memoryview',
205 '}',
206 '',
207 'def _array_to_list(x):',
208 ' return list(x)',
209 'def _list_to_array(x):',
210 ' return array("d",x)',
211 'arrays_list = {',
212 ' "from_json": _list_to_array,',
213 ' "to_json": _array_to_list',
214 '}',
215 '',
216 'class TestWidget(widgets.DOMWidget):',
217 ' _model_module = Unicode("TestWidget", sync=True)',
218 ' _model_name = Unicode("TestWidgetModel", sync=True)',
219 ' _view_module = Unicode("TestWidget", sync=True)',
220 ' _view_name = Unicode("TestWidgetView", sync=True)',
221 ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)',
222 ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)',
223 ' msg = {}',
224 ' def __init__(self, **kwargs):',
225 ' super(widgets.DOMWidget, self).__init__(**kwargs)',
226 ' self.on_msg(self._msg)',
227 ' def _msg(self, _, content, buffers):',
228 ' self.msg = [content, buffers]',
229 'x=TestWidget()',
230 'display(x)',
231 'print(x.model_id)'].join('\n'), function(index){
232 testwidget.index = index;
233 testwidget.model_id = this.get_output_cell(index).text.trim();
234 });
235 this.wait_for_widget(testwidget);
236
237
238 this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])');
239 this.wait_for_widget(testwidget);
240 this.then(function() {
241 var result = this.evaluate(function(index) {
242 var v = IPython.notebook.get_cell(index).widget_views[0];
243 var result = v.model.get('array_list');
244 var z = result.slice();
245 z[0]+="1234";
246 z[1]+="5678";
247 v.model.set('array_list', z);
248 v.touch();
249 return result;
250 }, testwidget.index);
251 this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js");
252 });
253
254 this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])',
255 'True', 'JSON custom serializer js -> kernel');
256
257 if (this.slimerjs) {
258 this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() {
259 this.evaluate(function(index) {
260 var v = IPython.notebook.get_cell(index).widget_views[0];
261 var z = v.model.get('array_binary');
262 z[0]*=3;
263 z[1]*=3;
264 z[2]*=3;
265 // we set to null so that we recognize the change
266 // when we set data back to z
267 v.model.set('array_binary', null);
268 v.model.set('array_binary', z);
269 v.touch();
270 }, textwidget.index);
271 });
272 this.wait_for_widget(testwidget);
273 this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]',
274 'True\n', 'Binary custom serializer js -> kernel')
275
276 this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])');
277 this.wait_for_widget(testwidget);
278
279 this.then(function() {
280 var result = this.evaluate(function(index) {
281 var v = IPython.notebook.get_cell(index).widget_views[0];
282 var d = new TextDecoder('utf-8');
283 return {text: v.msg[0],
284 binary0: d.decode(v.msg[1][0]),
285 binary1: d.decode(v.msg[1][1])};
286 }, testwidget.index);
287 this.test.assertEquals(result, {text: 'some content',
288 binary0: 'binarycontent',
289 binary1: 'morecontent'},
290 "Binary widget messages kernel -> js");
291 });
292
293 this.then(function() {
294 this.evaluate(function(index) {
295 var v = IPython.notebook.get_cell(index).widget_views[0];
296 v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])])
297 }, testwidget.index);
298 });
299 this.wait_for_widget(testwidget);
300 this.assert_output_equals([
301 'all([x.msg[0] == "content back",',
302 ' x.msg[1][0].tolist() == [1,2,3,4],',
303 ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'),
304 'True', 'Binary buffers message js -> kernel');
305 } else {
306 console.log("skipping binary websocket tests on phantomjs");
307 }
308
191 });
309 });
@@ -216,10 +216,11 b' class Widget(LoggingConfigurable):'
216 key : unicode, or iterable (optional)
216 key : unicode, or iterable (optional)
217 A single property's name or iterable of property names to sync with the front-end.
217 A single property's name or iterable of property names to sync with the front-end.
218 """
218 """
219 self._send({
219 state, buffer_keys, buffers = self.get_state(key=key)
220 "method" : "update",
220 msg = {"method": "update", "state": state}
221 "state" : self.get_state(key=key)
221 if buffer_keys:
222 })
222 msg['buffers'] = buffer_keys
223 self._send(msg, buffers=buffers)
223
224
224 def get_state(self, key=None):
225 def get_state(self, key=None):
225 """Gets the widget state, or a piece of it.
226 """Gets the widget state, or a piece of it.
@@ -228,6 +229,16 b' class Widget(LoggingConfigurable):'
228 ----------
229 ----------
229 key : unicode or iterable (optional)
230 key : unicode or iterable (optional)
230 A single property's name or iterable of property names to get.
231 A single property's name or iterable of property names to get.
232
233 Returns
234 -------
235 state : dict of states
236 buffer_keys : list of strings
237 the values that are stored in buffers
238 buffers : list of binary memoryviews
239 values to transmit in binary
240 metadata : dict
241 metadata for each field: {key: metadata}
231 """
242 """
232 if key is None:
243 if key is None:
233 keys = self.keys
244 keys = self.keys
@@ -238,11 +249,18 b' class Widget(LoggingConfigurable):'
238 else:
249 else:
239 raise ValueError("key must be a string, an iterable of keys, or None")
250 raise ValueError("key must be a string, an iterable of keys, or None")
240 state = {}
251 state = {}
252 buffers = []
253 buffer_keys = []
241 for k in keys:
254 for k in keys:
242 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
255 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 value = getattr(self, k)
256 value = getattr(self, k)
244 state[k] = f(value)
257 serialized = f(value)
245 return state
258 if isinstance(serialized, memoryview):
259 buffers.append(serialized)
260 buffer_keys.append(k)
261 else:
262 state[k] = serialized
263 return state, buffer_keys, buffers
246
264
247 def set_state(self, sync_data):
265 def set_state(self, sync_data):
248 """Called when a state is received from the front-end."""
266 """Called when a state is received from the front-end."""
@@ -253,15 +271,17 b' class Widget(LoggingConfigurable):'
253 with self._lock_property(name, json_value):
271 with self._lock_property(name, json_value):
254 setattr(self, name, from_json(json_value))
272 setattr(self, name, from_json(json_value))
255
273
256 def send(self, content):
274 def send(self, content, buffers=None):
257 """Sends a custom msg to the widget model in the front-end.
275 """Sends a custom msg to the widget model in the front-end.
258
276
259 Parameters
277 Parameters
260 ----------
278 ----------
261 content : dict
279 content : dict
262 Content of the message to send.
280 Content of the message to send.
281 buffers : list of binary buffers
282 Binary buffers to send with message
263 """
283 """
264 self._send({"method": "custom", "content": content})
284 self._send({"method": "custom", "content": content}, buffers=buffers)
265
285
266 def on_msg(self, callback, remove=False):
286 def on_msg(self, callback, remove=False):
267 """(Un)Register a custom msg receive callback.
287 """(Un)Register a custom msg receive callback.
@@ -269,9 +289,9 b' class Widget(LoggingConfigurable):'
269 Parameters
289 Parameters
270 ----------
290 ----------
271 callback: callable
291 callback: callable
272 callback will be passed two arguments when a message arrives::
292 callback will be passed three arguments when a message arrives::
273
293
274 callback(widget, content)
294 callback(widget, content, buffers)
275
295
276 remove: bool
296 remove: bool
277 True if the callback should be unregistered."""
297 True if the callback should be unregistered."""
@@ -353,7 +373,10 b' class Widget(LoggingConfigurable):'
353 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
373 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
354 if method == 'backbone':
374 if method == 'backbone':
355 if 'sync_data' in data:
375 if 'sync_data' in data:
376 # get binary buffers too
356 sync_data = data['sync_data']
377 sync_data = data['sync_data']
378 for i,k in enumerate(data.get('buffer_keys', [])):
379 sync_data[k] = msg['buffers'][i]
357 self.set_state(sync_data) # handles all methods
380 self.set_state(sync_data) # handles all methods
358
381
359 # Handle a state request.
382 # Handle a state request.
@@ -363,15 +386,15 b' class Widget(LoggingConfigurable):'
363 # Handle a custom msg from the front-end.
386 # Handle a custom msg from the front-end.
364 elif method == 'custom':
387 elif method == 'custom':
365 if 'content' in data:
388 if 'content' in data:
366 self._handle_custom_msg(data['content'])
389 self._handle_custom_msg(data['content'], msg['buffers'])
367
390
368 # Catch remainder.
391 # Catch remainder.
369 else:
392 else:
370 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
393 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
371
394
372 def _handle_custom_msg(self, content):
395 def _handle_custom_msg(self, content, buffers):
373 """Called when a custom msg is received."""
396 """Called when a custom msg is received."""
374 self._msg_callbacks(self, content)
397 self._msg_callbacks(self, content, buffers)
375
398
376 def _notify_trait(self, name, old_value, new_value):
399 def _notify_trait(self, name, old_value, new_value):
377 """Called when a property has been changed."""
400 """Called when a property has been changed."""
@@ -393,34 +416,11 b' class Widget(LoggingConfigurable):'
393 self._display_callbacks(self, **kwargs)
416 self._display_callbacks(self, **kwargs)
394
417
395 def _trait_to_json(self, x):
418 def _trait_to_json(self, x):
396 """Convert a trait value to json
419 """Convert a trait value to json."""
397
420 return x
398 Traverse lists/tuples and dicts and serialize their values as well.
399 Replace any widgets with their model_id
400 """
401 if isinstance(x, dict):
402 return {k: self._trait_to_json(v) for k, v in x.items()}
403 elif isinstance(x, (list, tuple)):
404 return [self._trait_to_json(v) for v in x]
405 elif isinstance(x, Widget):
406 return "IPY_MODEL_" + x.model_id
407 else:
408 return x # Value must be JSON-able
409
421
410 def _trait_from_json(self, x):
422 def _trait_from_json(self, x):
411 """Convert json values to objects
423 """Convert json values to objects."""
412
413 Replace any strings representing valid model id values to Widget references.
414 """
415 if isinstance(x, dict):
416 return {k: self._trait_from_json(v) for k, v in x.items()}
417 elif isinstance(x, (list, tuple)):
418 return [self._trait_from_json(v) for v in x]
419 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
420 # we want to support having child widgets at any level in a hierarchy
421 # trusting that a widget UUID will not appear out in the wild
422 return Widget.widgets[x[10:]]
423 else:
424 return x
424 return x
425
425
426 def _ipython_display_(self, **kwargs):
426 def _ipython_display_(self, **kwargs):
@@ -430,9 +430,9 b' class Widget(LoggingConfigurable):'
430 self._send({"method": "display"})
430 self._send({"method": "display"})
431 self._handle_displayed(**kwargs)
431 self._handle_displayed(**kwargs)
432
432
433 def _send(self, msg):
433 def _send(self, msg, buffers=None):
434 """Sends a message to the model in the front-end."""
434 """Sends a message to the model in the front-end."""
435 self.comm.send(msg)
435 self.comm.send(data=msg, buffers=buffers)
436
436
437
437
438 class DOMWidget(Widget):
438 class DOMWidget(Widget):
@@ -6,19 +6,46 b' Represents a container that can be used to group other widgets.'
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, Widget, register
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 from IPython.utils.warn import DeprecatedClass
11 from IPython.utils.warn import DeprecatedClass
12
12
13 def _widget_to_json(x):
14 if isinstance(x, dict):
15 return {k: _widget_to_json(v) for k, v in x.items()}
16 elif isinstance(x, (list, tuple)):
17 return [_widget_to_json(v) for v in x]
18 elif isinstance(x, Widget):
19 return "IPY_MODEL_" + x.model_id
20 else:
21 return x
22
23 def _json_to_widget(x):
24 if isinstance(x, dict):
25 return {k: _json_to_widget(v) for k, v in x.items()}
26 elif isinstance(x, (list, tuple)):
27 return [_json_to_widget(v) for v in x]
28 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
29 return Widget.widgets[x[10:]]
30 else:
31 return x
32
33 widget_serialization = {
34 'from_json': _json_to_widget,
35 'to_json': _widget_to_json
36 }
37
38
13 @register('IPython.Box')
39 @register('IPython.Box')
14 class Box(DOMWidget):
40 class Box(DOMWidget):
15 """Displays multiple widgets in a group."""
41 """Displays multiple widgets in a group."""
42 _model_name = Unicode('BoxModel', sync=True)
16 _view_name = Unicode('BoxView', sync=True)
43 _view_name = Unicode('BoxView', sync=True)
17
44
18 # Child widgets in the container.
45 # Child widgets in the container.
19 # Using a tuple here to force reassignment to update the list.
46 # Using a tuple here to force reassignment to update the list.
20 # When a proper notifying-list trait exists, that is what should be used here.
47 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
48 children = Tuple(sync=True, **widget_serialization)
22
49
23 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
50 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 overflow_x = CaselessStrEnum(
51 overflow_x = CaselessStrEnum(
@@ -67,7 +67,7 b' class Button(DOMWidget):'
67 Set to true to remove the callback from the list of callbacks."""
67 Set to true to remove the callback from the list of callbacks."""
68 self._click_handlers.register_callback(callback, remove=remove)
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 """Handle a msg from the front-end.
71 """Handle a msg from the front-end.
72
72
73 Parameters
73 Parameters
General Comments 0
You need to be logged in to leave comments. Login now