Merge pull request #7757 from jasongrout/custom-serialization

Custom serialization
Jonathan Frederic 11 years ago
commit 0c278a050d

@ -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};
});

@ -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;

@ -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,
};

@ -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.

@ -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");
}
});

@ -216,10 +216,11 @@ class Widget(LoggingConfigurable):
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 @@ class Widget(LoggingConfigurable):
----------
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 @@ class Widget(LoggingConfigurable):
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 @@ class Widget(LoggingConfigurable):
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 @@ class Widget(LoggingConfigurable):
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 @@ class Widget(LoggingConfigurable):
# 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 @@ class Widget(LoggingConfigurable):
# 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 @@ class Widget(LoggingConfigurable):
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 @@ class Widget(LoggingConfigurable):
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):

@ -6,19 +6,46 @@ Represents a container that can be used to group other widgets.
# 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(

@ -67,7 +67,7 @@ class Button(DOMWidget):
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

Loading…
Cancel
Save