##// 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 4 define([
5 5 "widgets/js/manager",
6 "widgets/js/widget",
6 7 "widgets/js/widget_link",
7 8 "widgets/js/widget_bool",
8 9 "widgets/js/widget_button",
@@ -14,21 +15,20 b' define(['
14 15 "widgets/js/widget_selection",
15 16 "widgets/js/widget_selectioncontainer",
16 17 "widgets/js/widget_string",
17 ], function(widgetmanager, linkModels) {
18 for (var target_name in linkModels) {
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.
18 ], function(widgetmanager, widget) {
19 // Register all of the loaded models and views with the widget manager.
25 20 for (var i = 2; i < arguments.length; i++) {
26 for (var target_name in arguments[i]) {
27 if (arguments[i].hasOwnProperty(target_name)) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
21 var module = arguments[i];
22 for (var target_name in module) {
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 33 return {'WidgetManager': widgetmanager.WidgetManager};
34 34 });
@@ -62,13 +62,13 b' define(["widgets/js/manager",'
62 62 return Backbone.Model.apply(this);
63 63 },
64 64
65 send: function (content, callbacks) {
65 send: function (content, callbacks, buffers) {
66 66 /**
67 67 * Send a custom msg over the comm.
68 68 */
69 69 if (this.comm !== undefined) {
70 70 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
71 this.comm.send(data, callbacks, {}, buffers);
72 72 this.pending_msgs++;
73 73 }
74 74 },
@@ -136,12 +136,31 b' define(["widgets/js/manager",'
136 136 * Handle incoming comm msg.
137 137 */
138 138 var method = msg.content.data.method;
139
139 140 var that = this;
140 141 switch (method) {
141 142 case 'update':
142 143 this.state_change = this.state_change
143 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 164 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 165 .then(function() {
147 166 var parent_id = msg.parent_header.msg_id;
@@ -152,7 +171,7 b' define(["widgets/js/manager",'
152 171 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 172 break;
154 173 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
174 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 175 break;
157 176 case 'display':
158 177 this.state_change = this.state_change.then(function() {
@@ -165,27 +184,23 b' define(["widgets/js/manager",'
165 184 set_state: function (state) {
166 185 var that = this;
167 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 188 that.state_lock = state;
170 189 try {
171 190 WidgetModel.__super__.set.call(that, state);
172 191 } finally {
173 192 that.state_lock = null;
174 193 }
194 resolve();
175 195 }).catch(utils.reject("Couldn't set model state", true));
176 196 },
177 197
178 198 get_state: function() {
179 199 // 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;
200 // Equivalent to Backbone.Model.toJSON()
201 return _.clone(this.attributes);
187 202 },
188
203
189 204 _handle_status: function (msg, callbacks) {
190 205 /**
191 206 * Handle status msgs.
@@ -243,6 +258,19 b' define(["widgets/js/manager",'
243 258 * Handle sync to the back-end. Called when a model.save() is called.
244 259 *
245 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 275 var error = options.error || function() {
248 276 console.error('Backbone sync error:', arguments);
@@ -252,8 +280,11 b' define(["widgets/js/manager",'
252 280 return false;
253 281 }
254 282
255 // Delete any key value pairs that the back-end already knows about.
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
283 var attrs = (method === 'patch') ? options.attrs : model.get_state(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 288 if (this.state_lock !== null) {
258 289 var keys = Object.keys(this.state_lock);
259 290 for (var i=0; i<keys.length; i++) {
@@ -263,9 +294,7 b' define(["widgets/js/manager",'
263 294 }
264 295 }
265 296 }
266
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
297
269 298 if (_.size(attrs) > 0) {
270 299
271 300 // If this message was sent via backbone itself, it will not
@@ -297,8 +326,7 b' define(["widgets/js/manager",'
297 326 } else {
298 327 // We haven't exceeded the throttle, send the message like
299 328 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
301 this.comm.send(data, callbacks);
329 this.send_sync_message(attrs, callbacks);
302 330 this.pending_msgs++;
303 331 }
304 332 }
@@ -308,6 +336,42 b' define(["widgets/js/manager",'
308 336 this._buffered_state_diff = {};
309 337 },
310 338
339
340 send_sync_message: function(attrs, callbacks) {
341 // prepare and send a comm message syncing attrs
342 var that = this;
343 // first, build a state dictionary with key=the attribute and the value
344 // being the value or the promise of the serialized value
345 var serializers = this.constructor.serializers;
346 if (serializers) {
347 for (k in attrs) {
348 if (serializers[k] && serializers[k].serialize) {
349 attrs[k] = (serializers[k].serialize)(attrs[k], this);
350 }
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 });
373 },
374
311 375 save_changes: function(callbacks) {
312 376 /**
313 377 * Push this model's state to the back-end
@@ -317,61 +381,6 b' define(["widgets/js/manager",'
317 381 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
318 382 },
319 383
320 _pack_models: function(value) {
321 /**
322 * Replace models with model ids recursively.
323 */
324 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;
346 }
347 },
348
349 _unpack_models: function(value) {
350 /**
351 * Replace model ids with models recursively.
352 */
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 }
373 },
374
375 384 on_some_change: function(keys, callback, context) {
376 385 /**
377 386 * on_some_change(["key1", "key2"], foo, context) differs from
@@ -386,7 +395,15 b' define(["widgets/js/manager",'
386 395 }
387 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 408 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392 409
@@ -426,7 +443,7 b' define(["widgets/js/manager",'
426 443 */
427 444 var that = this;
428 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 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 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 471 touch: function () {
@@ -558,7 +575,7 b' define(["widgets/js/manager",'
558 575 /**
559 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 579 return value.trim() + 'px';
563 580 }
564 581 return value;
@@ -4,10 +4,41 b''
4 4 define([
5 5 "widgets/js/widget",
6 6 "jqueryui",
7 "underscore",
7 8 "base/js/utils",
8 9 "bootstrap",
9 ], function(widget, $, utils){
10 ], function(widget, $, _, utils){
10 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 43 var BoxView = widget.DOMWidgetView.extend({
13 44 initialize: function(){
@@ -148,6 +179,8 b' define(['
148 179 });
149 180
150 181 return {
182 'unpack_models': unpack_models,
183 'BoxModel': BoxModel,
151 184 'BoxView': BoxView,
152 185 'FlexBoxView': FlexBoxView,
153 186 };
@@ -337,6 +337,19 b' casper.execute_cell_then = function(index, then_callback, expect_failure) {'
337 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 353 casper.wait_for_element = function(index, selector){
341 354 // Utility function that allows us to easily wait for an element
342 355 // within a cell. Uses JQuery selector to look for the element.
@@ -40,63 +40,12 b' casper.notebook_test(function () {'
40 40 var index;
41 41
42 42 index = this.append_cell(
43 'from IPython.html import widgets\n' +
44 'from IPython.display import display, clear_output\n' +
45 'print("Success")');
43 ['from IPython.html import widgets',
44 'from IPython.display import display, clear_output',
45 'print("Success")'].join('\n'));
46 46 this.execute_cell_then(index);
47 47
48 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 49 // Test multi-set, single touch code. First create a custom widget.
101 50 this.thenEvaluate(function() {
102 51 var MultiSetView = IPython.DOMWidgetView.extend({
@@ -113,20 +62,20 b' casper.notebook_test(function () {'
113 62
114 63 // Try creating the multiset widget, verify that sets the values correctly.
115 64 var multiset = {};
116 multiset.index = this.append_cell(
117 'from IPython.utils.traitlets import Unicode, CInt\n' +
118 'class MultiSetWidget(widgets.Widget):\n' +
119 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
120 ' a = CInt(0, sync=True)\n' +
121 ' b = CInt(0, sync=True)\n' +
122 ' c = CInt(0, sync=True)\n' +
123 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
124 ' def set_state(self, sync_data):\n' +
125 ' widgets.Widget.set_state(self, sync_data)\n'+
126 ' self.d = len(sync_data)\n' +
127 'multiset = MultiSetWidget()\n' +
128 'display(multiset)\n' +
129 'print(multiset.model_id)');
65 multiset.index = this.append_cell([
66 'from IPython.utils.traitlets import Unicode, CInt',
67 'class MultiSetWidget(widgets.Widget):',
68 ' _view_name = Unicode("MultiSetView", sync=True)',
69 ' a = CInt(0, sync=True)',
70 ' b = CInt(0, sync=True)',
71 ' c = CInt(0, sync=True)',
72 ' d = CInt(-1, sync=True)', // See if it sends a full state.
73 ' def set_state(self, sync_data):',
74 ' widgets.Widget.set_state(self, sync_data)',
75 ' self.d = len(sync_data)',
76 'multiset = MultiSetWidget()',
77 'display(multiset)',
78 'print(multiset.model_id)'].join('\n'));
130 79 this.execute_cell_then(multiset.index, function(index) {
131 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 99 var textbox = {};
151 throttle_index = this.append_cell(
152 'import time\n' +
153 'textbox = widgets.Text()\n' +
154 'display(textbox)\n' +
155 'textbox._dom_classes = ["my-throttle-textbox"]\n' +
156 'def handle_change(name, old, new):\n' +
157 ' display(len(new))\n' +
158 ' time.sleep(0.5)\n' +
159 'textbox.on_trait_change(handle_change, "value")\n' +
160 'print(textbox.model_id)');
100 throttle_index = this.append_cell([
101 'import time',
102 'textbox = widgets.Text()',
103 'display(textbox)',
104 'textbox._dom_classes = ["my-throttle-textbox"]',
105 'def handle_change(name, old, new):',
106 ' display(len(new))',
107 ' time.sleep(0.5)',
108 'textbox.on_trait_change(handle_change, "value")',
109 'print(textbox.model_id)'].join('\n'));
161 110 this.execute_cell_then(throttle_index, function(index){
162 111 textbox.model_id = this.get_output_cell(index).text.trim();
163 112
@@ -169,7 +118,7 b' casper.notebook_test(function () {'
169 118 '.my-throttle-textbox'), 'Textbox exists.');
170 119
171 120 // Send 20 characters
172 this.sendKeys('.my-throttle-textbox input', '....................');
121 this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
173 122 });
174 123
175 124 this.wait_for_widget(textbox);
@@ -188,4 +137,173 b' casper.notebook_test(function () {'
188 137 var last_state = outputs[outputs.length-1].data['text/plain'];
189 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 216 key : unicode, or iterable (optional)
217 217 A single property's name or iterable of property names to sync with the front-end.
218 218 """
219 self._send({
220 "method" : "update",
221 "state" : self.get_state(key=key)
222 })
219 state, buffer_keys, buffers = self.get_state(key=key)
220 msg = {"method": "update", "state": state}
221 if buffer_keys:
222 msg['buffers'] = buffer_keys
223 self._send(msg, buffers=buffers)
223 224
224 225 def get_state(self, key=None):
225 226 """Gets the widget state, or a piece of it.
@@ -228,6 +229,16 b' class Widget(LoggingConfigurable):'
228 229 ----------
229 230 key : unicode or iterable (optional)
230 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 243 if key is None:
233 244 keys = self.keys
@@ -238,11 +249,18 b' class Widget(LoggingConfigurable):'
238 249 else:
239 250 raise ValueError("key must be a string, an iterable of keys, or None")
240 251 state = {}
252 buffers = []
253 buffer_keys = []
241 254 for k in keys:
242 255 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 256 value = getattr(self, k)
244 state[k] = f(value)
245 return state
257 serialized = f(value)
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 265 def set_state(self, sync_data):
248 266 """Called when a state is received from the front-end."""
@@ -253,15 +271,17 b' class Widget(LoggingConfigurable):'
253 271 with self._lock_property(name, json_value):
254 272 setattr(self, name, from_json(json_value))
255 273
256 def send(self, content):
274 def send(self, content, buffers=None):
257 275 """Sends a custom msg to the widget model in the front-end.
258 276
259 277 Parameters
260 278 ----------
261 279 content : dict
262 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 286 def on_msg(self, callback, remove=False):
267 287 """(Un)Register a custom msg receive callback.
@@ -269,9 +289,9 b' class Widget(LoggingConfigurable):'
269 289 Parameters
270 290 ----------
271 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 296 remove: bool
277 297 True if the callback should be unregistered."""
@@ -353,7 +373,10 b' class Widget(LoggingConfigurable):'
353 373 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
354 374 if method == 'backbone':
355 375 if 'sync_data' in data:
376 # get binary buffers too
356 377 sync_data = data['sync_data']
378 for i,k in enumerate(data.get('buffer_keys', [])):
379 sync_data[k] = msg['buffers'][i]
357 380 self.set_state(sync_data) # handles all methods
358 381
359 382 # Handle a state request.
@@ -363,15 +386,15 b' class Widget(LoggingConfigurable):'
363 386 # Handle a custom msg from the front-end.
364 387 elif method == 'custom':
365 388 if 'content' in data:
366 self._handle_custom_msg(data['content'])
389 self._handle_custom_msg(data['content'], msg['buffers'])
367 390
368 391 # Catch remainder.
369 392 else:
370 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 396 """Called when a custom msg is received."""
374 self._msg_callbacks(self, content)
397 self._msg_callbacks(self, content, buffers)
375 398
376 399 def _notify_trait(self, name, old_value, new_value):
377 400 """Called when a property has been changed."""
@@ -393,35 +416,12 b' class Widget(LoggingConfigurable):'
393 416 self._display_callbacks(self, **kwargs)
394 417
395 418 def _trait_to_json(self, x):
396 """Convert a trait value to json
397
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
419 """Convert a trait value to json."""
420 return x
409 421
410 422 def _trait_from_json(self, x):
411 """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
423 """Convert json values to objects."""
424 return x
425 425
426 426 def _ipython_display_(self, **kwargs):
427 427 """Called when `IPython.display.display` is called on the widget."""
@@ -430,9 +430,9 b' class Widget(LoggingConfigurable):'
430 430 self._send({"method": "display"})
431 431 self._handle_displayed(**kwargs)
432 432
433 def _send(self, msg):
433 def _send(self, msg, buffers=None):
434 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 438 class DOMWidget(Widget):
@@ -6,19 +6,46 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, Widget, register
10 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 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 39 @register('IPython.Box')
14 40 class Box(DOMWidget):
15 41 """Displays multiple widgets in a group."""
42 _model_name = Unicode('BoxModel', sync=True)
16 43 _view_name = Unicode('BoxView', sync=True)
17 44
18 45 # Child widgets in the container.
19 46 # Using a tuple here to force reassignment to update the list.
20 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 50 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 51 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