diff --git a/IPython/html/static/widgets/js/init.js b/IPython/html/static/widgets/js/init.js
index 9dde6b0..da09f54 100644
--- a/IPython/html/static/widgets/js/init.js
+++ b/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};
});
diff --git a/IPython/html/static/widgets/js/widget.js b/IPython/html/static/widgets/js/widget.js
index e5f758d..a643ea0 100644
--- a/IPython/html/static/widgets/js/widget.js
+++ b/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 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 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");
+ }
+
});
diff --git a/IPython/html/widgets/widget.py b/IPython/html/widgets/widget.py
index 5c9ec61..28a24fb 100644
--- a/IPython/html/widgets/widget.py
+++ b/IPython/html/widgets/widget.py
@@ -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):
diff --git a/IPython/html/widgets/widget_box.py b/IPython/html/widgets/widget_box.py
index ca73e0d..7ae342e 100644
--- a/IPython/html/widgets/widget_box.py
+++ b/IPython/html/widgets/widget_box.py
@@ -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(
diff --git a/IPython/html/widgets/widget_button.py b/IPython/html/widgets/widget_button.py
index 4d5549c..3b3ad66 100644
--- a/IPython/html/widgets/widget_button.py
+++ b/IPython/html/widgets/widget_button.py
@@ -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