Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix binary synchronization and make Image use binary values #1643

Merged
merged 6 commits into from
Aug 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ If you are developing a custom widget or widget manager, here are some major cha
- Widgets will now need correct `_model_module` and `_view_module` Unicode traits defined.
- Selection widgets now sync the index of the selected item, rather than the label. ([#1262](https://github.com/jupyter-widgets/ipywidgets/pull/1262))
- The Python `ipywidget.domwidget.LabeledWidget` is now `ipywidget.widget_description.DescriptionWidget`, and there is a new `ipywidget.widget_description.DescriptionStyle` that lets the user set the CSS width of the description.
- Custom serializers can now return a structure that contains binary objects (`memoryview`, `bytearray`, or Python 3 `bytes` object). In this case, the sync message will be a binary message, which is much more efficient for binary data than base64-encoding. ([#1194](https://github.com/jupyter-widgets/ipywidgets/pull/1194), [#1595](https://github.com/jupyter-widgets/ipywidgets/pull/1595))
- Custom serializers can now return a structure that contains binary objects (`memoryview`, `bytearray`, or Python 3 `bytes` object). In this case, the sync message will be a binary message, which is much more efficient for binary data than base64-encoding. The Image widget now uses this binary synchronization. ([#1194](https://github.com/jupyter-widgets/ipywidgets/pull/1194), [#1595](https://github.com/jupyter-widgets/ipywidgets/pull/1595), [#1643](https://github.com/jupyter-widgets/ipywidgets/pull/1643))
- On the Javascript side:
- The `jupyter-js-widgets` Javascript package has been split into `@jupyter-widgets/base` package (containing base widget classes, the DOM widget, and the associated layout and style classes), and the `@jupyter-widgets/controls` package (containing the rest of the Jupyter widgets controls). Authors of custom widgets will need to update to depend on `@jupyter-widgets/base` instead of `jupyter-js-widgets` (if you use a class from the controls package, you will also need to depend on `@jupyter-widgets/controls`). See the [cookie cutter](https://github.com/jupyter-widgets/widget-cookiecutter) to generate a simple example custom widget using the new packages.
- Custom serializers in Javascript are now synchronous, and should return a snapshot of the widget state. The default serializer makes a copy of JSONable objects. ([#1270](https://github.com/jupyter-widgets/ipywidgets/pull/1270))
- Custom serializers can now return a structure that contains binary objects (`ArrayBuffer`, `DataView`, or a typed array such as `Int8Array`, `Float64Array`, etc.). In this case, the sync message will be a binary message, which is much more efficient for binary data than base64-encoding. ([#1194](https://github.com/jupyter-widgets/ipywidgets/pull/1194))
- Custom serializers can now return a structure that contains binary objects (`ArrayBuffer`, `DataView`, or a typed array such as `Int8Array`, `Float64Array`, etc.). In this case, the sync message will be a binary message, which is much more efficient for binary data than base64-encoding. The Image widget now uses this binary synchronization. ([#1194](https://github.com/jupyter-widgets/ipywidgets/pull/1194), [#1643](https://github.com/jupyter-widgets/ipywidgets/pull/1643))
- A custom serializer is given the widget instance as its second argument, and a custom deserializer is given the widget manager as its second argument.
- The Javascript model `.id` attribute has been renamed to `.model_id` to avoid conflicting with the Backbone `.id` attribute. ([#1410](https://github.com/jupyter-widgets/ipywidgets/pull/1410))
- Regarding widget managers and the syncing message protocol:
Expand Down
8 changes: 1 addition & 7 deletions ipywidgets/widgets/widget_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,4 @@ class Image(DOMWidget, ValueWidget, CoreWidget):
format = Unicode('png', help="The format of the image.").tag(sync=True)
width = CUnicode(help="Width of the image in pixels.").tag(sync=True)
height = CUnicode(help="Height of the image in pixels.").tag(sync=True)
_b64value = Unicode(help="The base64 encoded image data.").tag(sync=True)

value = Bytes(help="The image data as a byte string.")

@observe('value')
def _value_changed(self, change):
self._b64value = base64.b64encode(change['new'])
value = Bytes(help="The image data as a byte string.").tag(sync=True)
4 changes: 2 additions & 2 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"@types/semver": "^5.3.30",
"backbone": "1.2.3",
"jquery": "^3.1.1",
"semver": "^5.1.0",
"underscore": "^1.8.3"
"lodash": "^4.17.4",
"semver": "^5.1.0"
}
}
117 changes: 117 additions & 0 deletions packages/base/src/backbone-patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// This file contains a modified version of the set function from the Backbone
// (see
// https://github.com/jashkenas/backbone/blob/05fde9e201f7e2137796663081105cd6dad12a98/backbone.js#L460,
// with changes below marked with an EDIT comment). This file in Backbone has the following license.

// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org

// Backbone's full license is below (from https://github.com/jashkenas/backbone/blob/05fde9e201f7e2137796663081105cd6dad12a98/LICENSE)

/*
Copyright (c) 2010-2015 Jeremy Ashkenas, DocumentCloud

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/


import * as utils from './utils';

// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
// This *MUST* be called with the model as the `this` context.
export
function set(key, val, options) {
if (key == null) return this;

// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}

options || (options = {});

// Run validation.
if (!this._validate(attrs, options)) return false;

// Extract attributes and options.
var unset = options.unset;
var silent = options.silent;
var changes = [];
var changing = this._changing;
this._changing = true;

if (!changing) {
// EDIT: changed to use object spread instead of _.clone
this._previousAttributes = {...this.attributes};
this.changed = {};
}

var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;

// For each `set` attribute, update or delete the current value.
for (var attr in attrs) {
val = attrs[attr];
// EDIT: the following two lines use our isEqual instead of _.isEqual
if (!utils.isEqual(current[attr], val)) changes.push(attr);
if (!utils.isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}

// Update the `id`.
this.id = this.get(this.idAttribute);

// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}

// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
}
7 changes: 4 additions & 3 deletions packages/base/src/manager-base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as _ from 'underscore';
import * as utils from './utils';
import * as semver from 'semver';
import * as Backbone from 'backbone';
Expand Down Expand Up @@ -248,7 +247,9 @@ abstract class ManagerBase<T> {
let commPromise;
// we check to make sure the view information is provided, to help catch
// backwards incompatibility errors.
if (_.contains([options.view_name, options.view_module, options.view_module_version], undefined)) {
if (options.view_name === undefined
|| options.view_module === undefined
|| options.view_module_version === undefined) {
return Promise.reject("new_widget(...) must be given view information in the options.");
}
// If no comm is provided, a new comm is opened for the jupyter.widget
Expand All @@ -273,7 +274,7 @@ abstract class ManagerBase<T> {
);
}
// The options dictionary is copied since data will be added to it.
let options_clone = _.clone(options);
let options_clone = {...options};
// Create the model. In the case where the comm promise is rejected a
// comm-less model is still created with the required model id.
return commPromise.then((comm) => {
Expand Down
1 change: 0 additions & 1 deletion packages/base/src/services-shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* embed live widgets in a context outside of the notebook.
*/

import * as _ from 'underscore';
import * as utils from './utils';
import {
Kernel
Expand Down
38 changes: 30 additions & 8 deletions packages/base/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as _ from 'underscore';
export {
isEqual, difference
} from 'lodash';

import {
isPlainObject
} from 'lodash';

import {
toByteArray, fromByteArray
} from 'base64-js';

/**
* A polyfill for Object.assign
*
* This is from code that Typescript 2.4 generates for a polyfill.
*/
export
let assign = (Object as any).assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};

/**
* http://www.ietf.org/rfc/rfc4122.txt
*/
Expand Down Expand Up @@ -103,6 +124,8 @@ function put_buffers(state, buffer_paths: (string | number)[][], buffers: DataVi
}
}



/**
* The inverse of put_buffers, return an objects with the new state where all buffers(ArrayBuffer)
* are removed. If a buffer is a member of an object, that object is cloned, and the key removed. If a buffer
Expand Down Expand Up @@ -131,7 +154,7 @@ function remove_buffers(state): {state: any, buffers: ArrayBuffer[], buffer_path
if(value) {
if (value.buffer instanceof ArrayBuffer || value instanceof ArrayBuffer) {
if(!is_cloned) {
obj = _.clone(obj);
obj = obj.slice();
is_cloned = true;
}
buffers.push(value instanceof ArrayBuffer ? value : value.buffer);
Expand All @@ -144,35 +167,34 @@ function remove_buffers(state): {state: any, buffers: ArrayBuffer[], buffer_path
// only assigned when the value changes, we may serialize objects that don't support assignment
if(new_value !== value) {
if(!is_cloned) {
obj = _.clone(obj);
obj = obj.slice();
is_cloned = true;
}
obj[i] = new_value;
}
}
}
}
} else if(_.isObject(obj)) {
} else if(isPlainObject(obj)) {
for (let key in obj) {
let is_cloned = false;
if (obj.hasOwnProperty(key)) {
let value = obj[key];
if(value) {
if (value.buffer instanceof ArrayBuffer || value instanceof ArrayBuffer) {
if(!is_cloned) {
obj = _.clone(obj);
obj = {...obj};
is_cloned = true;
}
buffers.push(value instanceof ArrayBuffer ? value : value.buffer);
buffer_paths.push(path.concat([key]));
delete obj[key]; // for objects/dicts we just delete them
}
else {
} else {
let new_value = remove(value, path.concat([key]));
// only assigned when the value changes, we may serialize objects that don't support assignment
if(new_value !== value) {
if(!is_cloned) {
obj = _.clone(obj);
obj = {...obj};
is_cloned = true;
}
obj[key] = new_value;
Expand Down
Loading