Change custom serialization to use custom models, rather than transmitting the serializer name across the wire

This separates the kernel and the js much more cleanly, and doesn't use as much space on the wire as well!
This commit is contained in:
Jason Grout 2015-03-27 21:03:12 +00:00
parent d6def13dbd
commit 15c29f685c
5 changed files with 107 additions and 110 deletions

View File

@ -14,18 +14,33 @@ 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]);
}
}
], function(widgetmanager) {
// Register all of the loaded 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]);
/**
* From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
* Can be removed with the string endsWith function is implemented in major browsers
*/
var endsWith = function(target, searchString, position) {
var subjectString = target.toString();
if (position === undefined || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
// Register all of the loaded models and views with the widget manager.
for (var i = 1; i < arguments.length; i++) {
var module = arguments[i];
for (var target_name in module) {
if (module.hasOwnProperty(target_name)) {
if (endsWith(target_name, "View")) {
widgetmanager.WidgetManager.register_widget_view(target_name, module[target_name]);
} else if (endsWith(target_name, "Model")) {
widgetmanager.WidgetManager.register_widget_model(target_name, module[target_name]);
}
}
}
}

View File

@ -32,7 +32,6 @@ define(["widgets/js/manager",
this.state_lock = null;
this.id = model_id;
this.views = {};
this.serializers = {};
this._resolve_received_state = {};
if (comm !== undefined) {
@ -146,23 +145,17 @@ define(["widgets/js/manager",
var state = msg.content.data.state || {};
var buffer_keys = msg.content.data.buffers || [];
var buffers = msg.buffers || [];
var metadata = msg.content.data.metadata || {};
var i,k;
for (var i=0; i<buffer_keys.length; i++) {
k = buffer_keys[i];
state[k] = buffers[i];
state[buffer_keys[i]] = buffers[i];
}
// for any metadata specifying a deserializer, set the
// state to a promise that resolves to the deserialized version
// also, store the serialization function for the attribute
var keys = Object.keys(metadata);
for (var i=0; i<keys.length; i++) {
k = keys[i];
if (metadata[k] && metadata[k].serialization) {
that.serializers[k] = utils.load_class.apply(that,
metadata[k].serialization);
state[k] = that.deserialize(that.serializers[k], state[k]);
// 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);
@ -188,19 +181,6 @@ define(["widgets/js/manager",
}
},
deserialize: function(serializer, value) {
// given a serializer dict and a value,
// return a promise for the deserialized value
var that = this;
return serializer.then(function(s) {
if (s.deserialize) {
return s.deserialize(value, that);
} else {
return value;
}
});
},
set_state: function (state) {
var that = this;
// Handle when a widget is updated via the python side.
@ -362,25 +342,15 @@ define(["widgets/js/manager",
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 state_promise_dict = {};
var keys = Object.keys(attrs);
for (var i=0; i<keys.length; i++) {
// bind k and v locally; needed since we have an inner async function using v
(function(k,v) {
if (that.serializers[k]) {
state_promise_dict[k] = that.serializers[k].then(function(f) {
if (f.serialize) {
return f.serialize(v, that);
} else {
return v;
}
})
} else {
state_promise_dict[k] = v;
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);
}
})(keys[i], attrs[keys[i]])
}
}
utils.resolve_promises_dict(state_promise_dict).then(function(state) {
utils.resolve_promises_dict(attrs).then(function(state) {
// get binary values, then send
var keys = Object.keys(state);
var buffers = [];
@ -402,20 +372,6 @@ define(["widgets/js/manager",
});
},
serialize: function(model, attrs) {
// Serialize the attributes into a sync message
var keys = Object.keys(attrs);
var key, value;
var buffers, metadata, buffer_keys, serialize;
for (var i=0; i<keys.length; i++) {
key = keys[i];
serialize = model.serializers[key];
if (serialize && serialize.serialize) {
attrs[key] = serialize.serialize(attrs[key]);
}
}
},
save_changes: function(callbacks) {
/**
* Push this model's state to the back-end

View File

@ -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,7 @@ define([
});
return {
'BoxModel': BoxModel,
'BoxView': BoxView,
'FlexBoxView': FlexBoxView,
};

View File

@ -89,31 +89,6 @@ def register(key=None):
return wrap
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': lambda x: (_widget_to_json(x), {'serialization': ('models', 'widgets/js/types')})
}
class Widget(LoggingConfigurable):
#-------------------------------------------------------------------------
# Class attributes
@ -241,12 +216,10 @@ class Widget(LoggingConfigurable):
key : unicode, or iterable (optional)
A single property's name or iterable of property names to sync with the front-end.
"""
state, buffer_keys, buffers, metadata = 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
if metadata:
msg['metadata'] = metadata
self._send(msg, buffers=buffers)
def get_state(self, key=None):
@ -278,19 +251,16 @@ class Widget(LoggingConfigurable):
state = {}
buffers = []
buffer_keys = []
metadata = {}
for k in keys:
f = self.trait_metadata(k, 'to_json', self._trait_to_json)
value = getattr(self, k)
serialized, md = f(value)
serialized = f(value)
if isinstance(serialized, memoryview):
buffers.append(serialized)
buffer_keys.append(k)
else:
state[k] = serialized
if md is not None:
metadata[k] = md
return state, buffer_keys, buffers, metadata
return state, buffer_keys, buffers
def set_state(self, sync_data):
"""Called when a state is received from the front-end."""
@ -439,11 +409,8 @@ class Widget(LoggingConfigurable):
self._display_callbacks(self, **kwargs)
def _trait_to_json(self, x):
"""Convert a trait value to json.
Metadata (the second return value) is not sent
"""
return x, None
"""Convert a trait value to json."""
return x
def _trait_from_json(self, x):
"""Convert json values to objects."""

View File

@ -6,13 +6,40 @@ 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, widget_serialization
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.