Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #7757 from jasongrout/custom-serialization

Custom serialization
  • Loading branch information...
commit d6e249b0bdc1fa18cf2183c66a377626e58175e8 2 parents 6c15133 + 7a6f5bc
@jdfreder jdfreder authored
View
24 IPython/html/static/widgets/js/init.js
@@ -3,6 +3,7 @@
define([
"widgets/js/manager",
+ "widgets/js/widget",
"widgets/js/widget_link",
"widgets/js/widget_bool",
"widgets/js/widget_button",
@@ -14,21 +15,20 @@ define([
"widgets/js/widget_selection",
"widgets/js/widget_selectioncontainer",
"widgets/js/widget_string",
-], function(widgetmanager, linkModels) {
- for (var target_name in linkModels) {
- if (linkModels.hasOwnProperty(target_name)) {
- widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]);
- }
- }
-
- // Register all of the loaded views with the widget manager.
+], function(widgetmanager, widget) {
+ // Register all of the loaded models and views with the widget manager.
for (var i = 2; i < arguments.length; i++) {
- for (var target_name in arguments[i]) {
- if (arguments[i].hasOwnProperty(target_name)) {
- widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
+ var module = arguments[i];
+ for (var target_name in module) {
+ if (module.hasOwnProperty(target_name)) {
+ var target = module[target_name];
+ if (target.prototype instanceof widget.WidgetModel) {
+ widgetmanager.WidgetManager.register_widget_model(target_name, target);
+ } else if (target.prototype instanceof widget.WidgetView) {
+ widgetmanager.WidgetManager.register_widget_view(target_name, target);
+ }
}
}
}
-
return {'WidgetManager': widgetmanager.WidgetManager};
});
View
177 IPython/html/static/widgets/js/widget.js
@@ -62,13 +62,13 @@ define(["widgets/js/manager",
return Backbone.Model.apply(this);
},
- send: function (content, callbacks) {
+ send: function (content, callbacks, buffers) {
/**
* Send a custom msg over the comm.
*/
if (this.comm !== undefined) {
var data = {method: 'custom', content: content};
- this.comm.send(data, callbacks);
+ this.comm.send(data, callbacks, {}, buffers);
this.pending_msgs++;
}
},
@@ -136,12 +136,31 @@ define(["widgets/js/manager",
* Handle incoming comm msg.
*/
var method = msg.content.data.method;
+
var that = this;
switch (method) {
case 'update':
this.state_change = this.state_change
.then(function() {
- return that.set_state(msg.content.data.state);
+ var state = msg.content.data.state || {};
+ var buffer_keys = msg.content.data.buffers || [];
+ var buffers = msg.buffers || [];
+ for (var i=0; i<buffer_keys.length; i++) {
+ state[buffer_keys[i]] = buffers[i];
+ }
+
+ // deserialize fields that have custom deserializers
+ var serializers = that.constructor.serializers;
+ if (serializers) {
+ for (var k in state) {
+ if (serializers[k] && serializers[k].deserialize) {
+ state[k] = (serializers[k].deserialize)(state[k], that);
+ }
+ }
+ }
+ return utils.resolve_promises_dict(state);
+ }).then(function(state) {
+ return that.set_state(state);
}).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
.then(function() {
var parent_id = msg.parent_header.msg_id;
@@ -152,7 +171,7 @@ define(["widgets/js/manager",
}).catch(utils.reject("Couldn't resolve state request promise", true));
break;
case 'custom':
- this.trigger('msg:custom', msg.content.data.content);
+ this.trigger('msg:custom', msg.content.data.content, msg.buffers);
break;
case 'display':
this.state_change = this.state_change.then(function() {
@@ -165,27 +184,23 @@ define(["widgets/js/manager",
set_state: function (state) {
var that = this;
// Handle when a widget is updated via the python side.
- return this._unpack_models(state).then(function(state) {
+ return new Promise(function(resolve, reject) {
that.state_lock = state;
try {
WidgetModel.__super__.set.call(that, state);
} finally {
that.state_lock = null;
}
+ resolve();
}).catch(utils.reject("Couldn't set model state", true));
},
get_state: function() {
// Get the serializable state of the model.
- var state = this.toJSON();
- for (var key in state) {
- if (state.hasOwnProperty(key)) {
- state[key] = this._pack_models(state[key]);
- }
- }
- return state;
+ // Equivalent to Backbone.Model.toJSON()
+ return _.clone(this.attributes);
},
-
+
_handle_status: function (msg, callbacks) {
/**
* Handle status msgs.
@@ -243,6 +258,19 @@ define(["widgets/js/manager",
* Handle sync to the back-end. Called when a model.save() is called.
*
* Make sure a comm exists.
+
+ * Parameters
+ * ----------
+ * method : create, update, patch, delete, read
+ * create/update always send the full attribute set
+ * patch - only send attributes listed in options.attrs, and if we are queuing
+ * up messages, combine with previous messages that have not been sent yet
+ * model : the model we are syncing
+ * will normally be the same as `this`
+ * options : dict
+ * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
+ * otherwise, sync all attributes
+ *
*/
var error = options.error || function() {
console.error('Backbone sync error:', arguments);
@@ -252,8 +280,11 @@ define(["widgets/js/manager",
return false;
}
- // Delete any key value pairs that the back-end already knows about.
- var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
+ var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
+
+ // the state_lock lists attributes that are currently be changed right now from a kernel message
+ // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
+ // (but we only delete them if the value hasn't changed from the value stored in the state_lock
if (this.state_lock !== null) {
var keys = Object.keys(this.state_lock);
for (var i=0; i<keys.length; i++) {
@@ -263,9 +294,7 @@ define(["widgets/js/manager",
}
}
}
-
- // Only sync if there are attributes to send to the back-end.
- attrs = this._pack_models(attrs);
+
if (_.size(attrs) > 0) {
// If this message was sent via backbone itself, it will not
@@ -297,8 +326,7 @@ define(["widgets/js/manager",
} else {
// We haven't exceeded the throttle, send the message like
// normal.
- var data = {method: 'backbone', sync_data: attrs};
- this.comm.send(data, callbacks);
+ this.send_sync_message(attrs, callbacks);
this.pending_msgs++;
}
}
@@ -308,6 +336,42 @@ define(["widgets/js/manager",
this._buffered_state_diff = {};
},
+
+ send_sync_message: function(attrs, callbacks) {
+ // prepare and send a comm message syncing attrs
+ var that = this;
+ // first, build a state dictionary with key=the attribute and the value
+ // being the value or the promise of the serialized value
+ var serializers = this.constructor.serializers;
+ if (serializers) {
+ for (k in attrs) {
+ if (serializers[k] && serializers[k].serialize) {
+ attrs[k] = (serializers[k].serialize)(attrs[k], this);
+ }
+ }
+ }
+ utils.resolve_promises_dict(attrs).then(function(state) {
+ // get binary values, then send
+ var keys = Object.keys(state);
+ var buffers = [];
+ var buffer_keys = [];
+ for (var i=0; i<keys.length; i++) {
+ var key = keys[i];
+ var value = state[key];
+ if (value.buffer instanceof ArrayBuffer
+ || value instanceof ArrayBuffer) {
+ buffers.push(value);
+ buffer_keys.push(key);
+ delete state[key];
+ }
+ }
+ that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
+ }).catch(function(error) {
+ that.pending_msgs--;
+ return (utils.reject("Couldn't send widget sync message", true))(error);
+ });
+ },
+
save_changes: function(callbacks) {
/**
* Push this model's state to the back-end
@@ -317,61 +381,6 @@ define(["widgets/js/manager",
this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
},
- _pack_models: function(value) {
- /**
- * Replace models with model ids recursively.
- */
- var that = this;
- var packed;
- if (value instanceof Backbone.Model) {
- return "IPY_MODEL_" + value.id;
-
- } else if ($.isArray(value)) {
- packed = [];
- _.each(value, function(sub_value, key) {
- packed.push(that._pack_models(sub_value));
- });
- return packed;
- } else if (value instanceof Date || value instanceof String) {
- return value;
- } else if (value instanceof Object) {
- packed = {};
- _.each(value, function(sub_value, key) {
- packed[key] = that._pack_models(sub_value);
- });
- return packed;
-
- } else {
- return value;
- }
- },
-
- _unpack_models: function(value) {
- /**
- * Replace model ids with models recursively.
- */
- var that = this;
- var unpacked;
- if ($.isArray(value)) {
- unpacked = [];
- _.each(value, function(sub_value, key) {
- unpacked.push(that._unpack_models(sub_value));
- });
- return Promise.all(unpacked);
- } else if (value instanceof Object) {
- unpacked = {};
- _.each(value, function(sub_value, key) {
- unpacked[key] = that._unpack_models(sub_value);
- });
- return utils.resolve_promises_dict(unpacked);
- } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
- // get_model returns a promise already
- return this.widget_manager.get_model(value.slice(10, value.length));
- } else {
- return Promise.resolve(value);
- }
- },
-
on_some_change: function(keys, callback, context) {
/**
* on_some_change(["key1", "key2"], foo, context) differs from
@@ -386,7 +395,15 @@ define(["widgets/js/manager",
}
}, this);
- },
+ },
+
+ toJSON: function(options) {
+ /**
+ * Serialize the model. See the types.js deserialization function
+ * and the kernel-side serializer/deserializer
+ */
+ return "IPY_MODEL_"+this.id;
+ }
});
widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
@@ -426,7 +443,7 @@ define(["widgets/js/manager",
*/
var that = this;
options = $.extend({ parent: this }, options || {});
- return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
+ return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true));
},
callbacks: function(){
@@ -444,11 +461,11 @@ define(["widgets/js/manager",
*/
},
- send: function (content) {
+ send: function (content, buffers) {
/**
* Send a custom msg associated with this view.
*/
- this.model.send(content, this.callbacks());
+ this.model.send(content, this.callbacks(), buffers);
},
touch: function () {
@@ -558,7 +575,7 @@ define(["widgets/js/manager",
/**
* Makes browser interpret a numerical string as a pixel value.
*/
- if (/^\d+\.?(\d+)?$/.test(value.trim())) {
+ if (value && /^\d+\.?(\d+)?$/.test(value.trim())) {
return value.trim() + 'px';
}
return value;
View
35 IPython/html/static/widgets/js/widget_box.js
@@ -4,10 +4,41 @@
define([
"widgets/js/widget",
"jqueryui",
+ "underscore",
"base/js/utils",
"bootstrap",
-], function(widget, $, utils){
+], function(widget, $, _, utils){
"use strict";
+ var unpack_models = function unpack_models(value, model) {
+ /**
+ * Replace model ids with models recursively.
+ */
+ var unpacked;
+ if ($.isArray(value)) {
+ unpacked = [];
+ _.each(value, function(sub_value, key) {
+ unpacked.push(unpack_models(sub_value, model));
+ });
+ return Promise.all(unpacked);
+ } else if (value instanceof Object) {
+ unpacked = {};
+ _.each(value, function(sub_value, key) {
+ unpacked[key] = unpack_models(sub_value, model);
+ });
+ return utils.resolve_promises_dict(unpacked);
+ } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
+ // get_model returns a promise already
+ return model.widget_manager.get_model(value.slice(10, value.length));
+ } else {
+ return Promise.resolve(value);
+ }
+ };
+
+ var BoxModel = widget.WidgetModel.extend({}, {
+ serializers: _.extend({
+ children: {deserialize: unpack_models}
+ }, widget.WidgetModel.serializers)
+ });
var BoxView = widget.DOMWidgetView.extend({
initialize: function(){
@@ -148,6 +179,8 @@ define([
});
return {
+ 'unpack_models': unpack_models,
+ 'BoxModel': BoxModel,
'BoxView': BoxView,
'FlexBoxView': FlexBoxView,
};
View
13 IPython/html/tests/util.js
@@ -337,6 +337,19 @@ casper.execute_cell_then = function(index, then_callback, expect_failure) {
return return_val;
};
+casper.append_cell_execute_then = function(text, then_callback, expect_failure) {
+ // Append a code cell and execute it, optionally calling a then_callback
+ var c = this.append_cell(text);
+ return this.execute_cell_then(c, then_callback, expect_failure);
+};
+
+casper.assert_output_equals = function(text, output_text, message) {
+ // Append a code cell with the text, then assert the output is equal to output_text
+ this.append_cell_execute_then(text, function(index) {
+ this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message);
+ });
+};
+
casper.wait_for_element = function(index, selector){
// Utility function that allows us to easily wait for an element
// within a cell. Uses JQuery selector to look for the element.
View
276 IPython/html/tests/widgets/widget.js
@@ -40,63 +40,12 @@ casper.notebook_test(function () {
var index;
index = this.append_cell(
- 'from IPython.html import widgets\n' +
- 'from IPython.display import display, clear_output\n' +
- 'print("Success")');
+ ['from IPython.html import widgets',
+ 'from IPython.display import display, clear_output',
+ 'print("Success")'].join('\n'));
this.execute_cell_then(index);
this.then(function () {
-
- // Functions that can be used to test the packing and unpacking APIs
- var that = this;
- var test_pack = function (input) {
- var output = that.evaluate(function(input) {
- var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
- var results = model._pack_models(input);
- return results;
- }, {input: input});
- that.test.assert(recursive_compare(input, output),
- JSON.stringify(input) + ' passed through Model._pack_model unchanged');
- };
- var test_unpack = function (input) {
- that.thenEvaluate(function(input) {
- window.results = undefined;
- var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
- model._unpack_models(input).then(function(results) {
- window.results = results;
- });
- }, {input: input});
-
- that.waitFor(function check() {
- return that.evaluate(function() {
- return window.results;
- });
- });
-
- that.then(function() {
- var results = that.evaluate(function() {
- return window.results;
- });
- that.test.assert(recursive_compare(input, results),
- JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
- });
- };
- var test_packing = function(input) {
- test_pack(input);
- test_unpack(input);
- };
-
- test_packing({0: 'hi', 1: 'bye'});
- test_packing(['hi', 'bye']);
- test_packing(['hi', 5]);
- test_packing(['hi', '5']);
- test_packing([1.0, 0]);
- test_packing([1.0, false]);
- test_packing([1, false]);
- test_packing([1, false, {a: 'hi'}]);
- test_packing([1, false, ['hi']]);
- test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")])
-
// Test multi-set, single touch code. First create a custom widget.
this.thenEvaluate(function() {
var MultiSetView = IPython.DOMWidgetView.extend({
@@ -113,20 +62,20 @@ casper.notebook_test(function () {
// Try creating the multiset widget, verify that sets the values correctly.
var multiset = {};
- multiset.index = this.append_cell(
- 'from IPython.utils.traitlets import Unicode, CInt\n' +
- 'class MultiSetWidget(widgets.Widget):\n' +
- ' _view_name = Unicode("MultiSetView", sync=True)\n' +
- ' a = CInt(0, sync=True)\n' +
- ' b = CInt(0, sync=True)\n' +
- ' c = CInt(0, sync=True)\n' +
- ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
- ' def set_state(self, sync_data):\n' +
- ' widgets.Widget.set_state(self, sync_data)\n'+
- ' self.d = len(sync_data)\n' +
- 'multiset = MultiSetWidget()\n' +
- 'display(multiset)\n' +
- 'print(multiset.model_id)');
+ multiset.index = this.append_cell([
+ 'from IPython.utils.traitlets import Unicode, CInt',
+ 'class MultiSetWidget(widgets.Widget):',
+ ' _view_name = Unicode("MultiSetView", sync=True)',
+ ' a = CInt(0, sync=True)',
+ ' b = CInt(0, sync=True)',
+ ' c = CInt(0, sync=True)',
+ ' d = CInt(-1, sync=True)', // See if it sends a full state.
+ ' def set_state(self, sync_data):',
+ ' widgets.Widget.set_state(self, sync_data)',
+ ' self.d = len(sync_data)',
+ 'multiset = MultiSetWidget()',
+ 'display(multiset)',
+ 'print(multiset.model_id)'].join('\n'));
this.execute_cell_then(multiset.index, function(index) {
multiset.model_id = this.get_output_cell(index).text.trim();
});
@@ -148,16 +97,16 @@ casper.notebook_test(function () {
});
var textbox = {};
- throttle_index = this.append_cell(
- 'import time\n' +
- 'textbox = widgets.Text()\n' +
- 'display(textbox)\n' +
- 'textbox._dom_classes = ["my-throttle-textbox"]\n' +
- 'def handle_change(name, old, new):\n' +
- ' display(len(new))\n' +
- ' time.sleep(0.5)\n' +
- 'textbox.on_trait_change(handle_change, "value")\n' +
- 'print(textbox.model_id)');
+ throttle_index = this.append_cell([
+ 'import time',
+ 'textbox = widgets.Text()',
+ 'display(textbox)',
+ 'textbox._dom_classes = ["my-throttle-textbox"]',
+ 'def handle_change(name, old, new):',
+ ' display(len(new))',
+ ' time.sleep(0.5)',
+ 'textbox.on_trait_change(handle_change, "value")',
+ 'print(textbox.model_id)'].join('\n'));
this.execute_cell_then(throttle_index, function(index){
textbox.model_id = this.get_output_cell(index).text.trim();
@@ -169,7 +118,7 @@ casper.notebook_test(function () {
'.my-throttle-textbox'), 'Textbox exists.');
// Send 20 characters
- this.sendKeys('.my-throttle-textbox input', '....................');
+ this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
});
this.wait_for_widget(textbox);
@@ -188,4 +137,173 @@ casper.notebook_test(function () {
var last_state = outputs[outputs.length-1].data['text/plain'];
this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
});
+
+
+ this.thenEvaluate(function() {
+ define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) {
+ var floatArray = {
+ deserialize: function (value, model) {
+ if (value===null) {return null;}
+ // DataView -> float64 typed array
+ return new Float64Array(value.buffer);
+ },
+ // serialization automatically handled since the
+ // attribute is an ArrayBuffer view
+ };
+
+ var floatList = {
+ deserialize: function (value, model) {
+ // list of floats -> list of strings
+ return value.map(function(x) {return x.toString()});
+ },
+ serialize: function(value, model) {
+ // list of strings -> list of floats
+ return value.map(function(x) {return parseFloat(x);})
+ }
+ };
+
+ var TestWidgetModel = widget.WidgetModel.extend({}, {
+ serializers: _.extend({
+ array_list: floatList,
+ array_binary: floatArray
+ }, widget.WidgetModel.serializers)
+ });
+
+ var TestWidgetView = widget.DOMWidgetView.extend({
+ render: function () {
+ this.listenTo(this.model, 'msg:custom', this.handle_msg);
+ },
+ handle_msg: function(content, buffers) {
+ this.msg = [content, buffers];
+ }
+ });
+
+ return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView};
+ });
+ });
+
+ var testwidget = {};
+ this.append_cell_execute_then([
+ 'from IPython.html import widgets',
+ 'from IPython.utils.traitlets import Unicode, Instance, List',
+ 'from IPython.display import display',
+ 'from array import array',
+ 'def _array_to_memoryview(x):',
+ ' if x is None: return None',
+ ' try:',
+ ' y = memoryview(x)',
+ ' except TypeError:',
+ ' # in python 2, arrays do not support the new buffer protocol',
+ ' y = memoryview(buffer(x))',
+ ' return y',
+ 'def _memoryview_to_array(x):',
+ ' if x is None: return None',
+ ' return array("d", x.tobytes())',
+ 'arrays_binary = {',
+ ' "from_json": _memoryview_to_array,',
+ ' "to_json": _array_to_memoryview',
+ '}',
+ '',
+ 'def _array_to_list(x):',
+ ' return list(x)',
+ 'def _list_to_array(x):',
+ ' return array("d",x)',
+ 'arrays_list = {',
+ ' "from_json": _list_to_array,',
+ ' "to_json": _array_to_list',
+ '}',
+ '',
+ 'class TestWidget(widgets.DOMWidget):',
+ ' _model_module = Unicode("TestWidget", sync=True)',
+ ' _model_name = Unicode("TestWidgetModel", sync=True)',
+ ' _view_module = Unicode("TestWidget", sync=True)',
+ ' _view_name = Unicode("TestWidgetView", sync=True)',
+ ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)',
+ ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)',
+ ' msg = {}',
+ ' def __init__(self, **kwargs):',
+ ' super(widgets.DOMWidget, self).__init__(**kwargs)',
+ ' self.on_msg(self._msg)',
+ ' def _msg(self, _, content, buffers):',
+ ' self.msg = [content, buffers]',
+ 'x=TestWidget()',
+ 'display(x)',
+ 'print(x.model_id)'].join('\n'), function(index){
+ testwidget.index = index;
+ testwidget.model_id = this.get_output_cell(index).text.trim();
+ });
+ this.wait_for_widget(testwidget);
+
+
+ this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])');
+ this.wait_for_widget(testwidget);
+ this.then(function() {
+ var result = this.evaluate(function(index) {
+ var v = IPython.notebook.get_cell(index).widget_views[0];
+ var result = v.model.get('array_list');
+ var z = result.slice();
+ z[0]+="1234";
+ z[1]+="5678";
+ v.model.set('array_list', z);
+ v.touch();
+ return result;
+ }, testwidget.index);
+ this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js");
+ });
+
+ this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])',
+ 'True', 'JSON custom serializer js -> kernel');
+
+ if (this.slimerjs) {
+ this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() {
+ this.evaluate(function(index) {
+ var v = IPython.notebook.get_cell(index).widget_views[0];
+ var z = v.model.get('array_binary');
+ z[0]*=3;
+ z[1]*=3;
+ z[2]*=3;
+ // we set to null so that we recognize the change
+ // when we set data back to z
+ v.model.set('array_binary', null);
+ v.model.set('array_binary', z);
+ v.touch();
+ }, textwidget.index);
+ });
+ this.wait_for_widget(testwidget);
+ this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]',
+ 'True\n', 'Binary custom serializer js -> kernel')
+
+ this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])');
+ this.wait_for_widget(testwidget);
+
+ this.then(function() {
+ var result = this.evaluate(function(index) {
+ var v = IPython.notebook.get_cell(index).widget_views[0];
+ var d = new TextDecoder('utf-8');
+ return {text: v.msg[0],
+ binary0: d.decode(v.msg[1][0]),
+ binary1: d.decode(v.msg[1][1])};
+ }, testwidget.index);
+ this.test.assertEquals(result, {text: 'some content',
+ binary0: 'binarycontent',
+ binary1: 'morecontent'},
+ "Binary widget messages kernel -> js");
+ });
+
+ this.then(function() {
+ this.evaluate(function(index) {
+ var v = IPython.notebook.get_cell(index).widget_views[0];
+ v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])])
+ }, testwidget.index);
+ });
+ this.wait_for_widget(testwidget);
+ this.assert_output_equals([
+ 'all([x.msg[0] == "content back",',
+ ' x.msg[1][0].tolist() == [1,2,3,4],',
+ ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'),
+ 'True', 'Binary buffers message js -> kernel');
+ } else {
+ console.log("skipping binary websocket tests on phantomjs");
+ }
+
});
View
84 IPython/html/widgets/widget.py
@@ -216,10 +216,11 @@ def send_state(self, key=None):
key : unicode, or iterable (optional)
A single property's name or iterable of property names to sync with the front-end.
"""
- self._send({
- "method" : "update",
- "state" : self.get_state(key=key)
- })
+ state, buffer_keys, buffers = self.get_state(key=key)
+ msg = {"method": "update", "state": state}
+ if buffer_keys:
+ msg['buffers'] = buffer_keys
+ self._send(msg, buffers=buffers)
def get_state(self, key=None):
"""Gets the widget state, or a piece of it.
@@ -228,6 +229,16 @@ def get_state(self, key=None):
----------
key : unicode or iterable (optional)
A single property's name or iterable of property names to get.
+
+ Returns
+ -------
+ state : dict of states
+ buffer_keys : list of strings
+ the values that are stored in buffers
+ buffers : list of binary memoryviews
+ values to transmit in binary
+ metadata : dict
+ metadata for each field: {key: metadata}
"""
if key is None:
keys = self.keys
@@ -238,11 +249,18 @@ def get_state(self, key=None):
else:
raise ValueError("key must be a string, an iterable of keys, or None")
state = {}
+ buffers = []
+ buffer_keys = []
for k in keys:
f = self.trait_metadata(k, 'to_json', self._trait_to_json)
value = getattr(self, k)
- state[k] = f(value)
- return state
+ serialized = f(value)
+ if isinstance(serialized, memoryview):
+ buffers.append(serialized)
+ buffer_keys.append(k)
+ else:
+ state[k] = serialized
+ return state, buffer_keys, buffers
def set_state(self, sync_data):
"""Called when a state is received from the front-end."""
@@ -253,15 +271,17 @@ def set_state(self, sync_data):
with self._lock_property(name, json_value):
setattr(self, name, from_json(json_value))
- def send(self, content):
+ def send(self, content, buffers=None):
"""Sends a custom msg to the widget model in the front-end.
Parameters
----------
content : dict
Content of the message to send.
+ buffers : list of binary buffers
+ Binary buffers to send with message
"""
- self._send({"method": "custom", "content": content})
+ self._send({"method": "custom", "content": content}, buffers=buffers)
def on_msg(self, callback, remove=False):
"""(Un)Register a custom msg receive callback.
@@ -269,9 +289,9 @@ def on_msg(self, callback, remove=False):
Parameters
----------
callback: callable
- callback will be passed two arguments when a message arrives::
+ callback will be passed three arguments when a message arrives::
- callback(widget, content)
+ callback(widget, content, buffers)
remove: bool
True if the callback should be unregistered."""
@@ -353,7 +373,10 @@ def _handle_msg(self, msg):
# Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
if method == 'backbone':
if 'sync_data' in data:
+ # get binary buffers too
sync_data = data['sync_data']
+ for i,k in enumerate(data.get('buffer_keys', [])):
+ sync_data[k] = msg['buffers'][i]
self.set_state(sync_data) # handles all methods
# Handle a state request.
@@ -363,15 +386,15 @@ def _handle_msg(self, msg):
# Handle a custom msg from the front-end.
elif method == 'custom':
if 'content' in data:
- self._handle_custom_msg(data['content'])
+ self._handle_custom_msg(data['content'], msg['buffers'])
# Catch remainder.
else:
self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
- def _handle_custom_msg(self, content):
+ def _handle_custom_msg(self, content, buffers):
"""Called when a custom msg is received."""
- self._msg_callbacks(self, content)
+ self._msg_callbacks(self, content, buffers)
def _notify_trait(self, name, old_value, new_value):
"""Called when a property has been changed."""
@@ -393,35 +416,12 @@ def _handle_displayed(self, **kwargs):
self._display_callbacks(self, **kwargs)
def _trait_to_json(self, x):
- """Convert a trait value to json
-
- Traverse lists/tuples and dicts and serialize their values as well.
- Replace any widgets with their model_id
- """
- if isinstance(x, dict):
- return {k: self._trait_to_json(v) for k, v in x.items()}
- elif isinstance(x, (list, tuple)):
- return [self._trait_to_json(v) for v in x]
- elif isinstance(x, Widget):
- return "IPY_MODEL_" + x.model_id
- else:
- return x # Value must be JSON-able
+ """Convert a trait value to json."""
+ return x
def _trait_from_json(self, x):
- """Convert json values to objects
-
- Replace any strings representing valid model id values to Widget references.
- """
- if isinstance(x, dict):
- return {k: self._trait_from_json(v) for k, v in x.items()}
- elif isinstance(x, (list, tuple)):
- return [self._trait_from_json(v) for v in x]
- elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
- # we want to support having child widgets at any level in a hierarchy
- # trusting that a widget UUID will not appear out in the wild
- return Widget.widgets[x[10:]]
- else:
- return x
+ """Convert json values to objects."""
+ return x
def _ipython_display_(self, **kwargs):
"""Called when `IPython.display.display` is called on the widget."""
@@ -430,9 +430,9 @@ def _ipython_display_(self, **kwargs):
self._send({"method": "display"})
self._handle_displayed(**kwargs)
- def _send(self, msg):
+ def _send(self, msg, buffers=None):
"""Sends a message to the model in the front-end."""
- self.comm.send(msg)
+ self.comm.send(data=msg, buffers=buffers)
class DOMWidget(Widget):
View
31 IPython/html/widgets/widget_box.py
@@ -6,19 +6,46 @@
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
-from .widget import DOMWidget, register
+from .widget import DOMWidget, Widget, register
from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
from IPython.utils.warn import DeprecatedClass
+def _widget_to_json(x):
+ if isinstance(x, dict):
+ return {k: _widget_to_json(v) for k, v in x.items()}
+ elif isinstance(x, (list, tuple)):
+ return [_widget_to_json(v) for v in x]
+ elif isinstance(x, Widget):
+ return "IPY_MODEL_" + x.model_id
+ else:
+ return x
+
+def _json_to_widget(x):
+ if isinstance(x, dict):
+ return {k: _json_to_widget(v) for k, v in x.items()}
+ elif isinstance(x, (list, tuple)):
+ return [_json_to_widget(v) for v in x]
+ elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
+ return Widget.widgets[x[10:]]
+ else:
+ return x
+
+widget_serialization = {
+ 'from_json': _json_to_widget,
+ 'to_json': _widget_to_json
+}
+
+
@register('IPython.Box')
class Box(DOMWidget):
"""Displays multiple widgets in a group."""
+ _model_name = Unicode('BoxModel', sync=True)
_view_name = Unicode('BoxView', sync=True)
# Child widgets in the container.
# Using a tuple here to force reassignment to update the list.
# When a proper notifying-list trait exists, that is what should be used here.
- children = Tuple(sync=True)
+ children = Tuple(sync=True, **widget_serialization)
_overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
overflow_x = CaselessStrEnum(
View
2  IPython/html/widgets/widget_button.py
@@ -67,7 +67,7 @@ def on_click(self, callback, remove=False):
Set to true to remove the callback from the list of callbacks."""
self._click_handlers.register_callback(callback, remove=remove)
- def _handle_button_msg(self, _, content):
+ def _handle_button_msg(self, _, content, buffers):
"""Handle a msg from the front-end.
Parameters
Please sign in to comment.
Something went wrong with that request. Please try again.