Skip to content

Commit 1a357c9

Browse files
authored
Merge pull request #1643 from jasongrout/binaryimage
Fix binary synchronization and make Image use binary values
2 parents 0f58eb0 + 54fdce3 commit 1a357c9

16 files changed

+223
-73
lines changed

docs/source/changelog.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ If you are developing a custom widget or widget manager, here are some major cha
4444
- Widgets will now need correct `_model_module` and `_view_module` Unicode traits defined.
4545
- Selection widgets now sync the index of the selected item, rather than the label. ([#1262](https://github.com/jupyter-widgets/ipywidgets/pull/1262))
4646
- 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.
47-
- 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))
47+
- 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))
4848
- On the Javascript side:
4949
- 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.
5050
- 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))
51-
- 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))
51+
- 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))
5252
- 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.
5353
- 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))
5454
- Regarding widget managers and the syncing message protocol:

ipywidgets/widgets/widget_image.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,4 @@ class Image(DOMWidget, ValueWidget, CoreWidget):
3131
format = Unicode('png', help="The format of the image.").tag(sync=True)
3232
width = CUnicode(help="Width of the image in pixels.").tag(sync=True)
3333
height = CUnicode(help="Height of the image in pixels.").tag(sync=True)
34-
_b64value = Unicode(help="The base64 encoded image data.").tag(sync=True)
35-
36-
value = Bytes(help="The image data as a byte string.")
37-
38-
@observe('value')
39-
def _value_changed(self, change):
40-
self._b64value = base64.b64encode(change['new'])
34+
value = Bytes(help="The image data as a byte string.").tag(sync=True)

packages/base/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"@types/semver": "^5.3.30",
6666
"backbone": "1.2.3",
6767
"jquery": "^3.1.1",
68-
"semver": "^5.1.0",
69-
"underscore": "^1.8.3"
68+
"lodash": "^4.17.4",
69+
"semver": "^5.1.0"
7070
}
7171
}

packages/base/src/backbone-patch.ts

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// This file contains a modified version of the set function from the Backbone
2+
// (see
3+
// https://github.com/jashkenas/backbone/blob/05fde9e201f7e2137796663081105cd6dad12a98/backbone.js#L460,
4+
// with changes below marked with an EDIT comment). This file in Backbone has the following license.
5+
6+
// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
7+
// Backbone may be freely distributed under the MIT license.
8+
// For all details and documentation:
9+
// http://backbonejs.org
10+
11+
// Backbone's full license is below (from https://github.com/jashkenas/backbone/blob/05fde9e201f7e2137796663081105cd6dad12a98/LICENSE)
12+
13+
/*
14+
Copyright (c) 2010-2015 Jeremy Ashkenas, DocumentCloud
15+
16+
Permission is hereby granted, free of charge, to any person
17+
obtaining a copy of this software and associated documentation
18+
files (the "Software"), to deal in the Software without
19+
restriction, including without limitation the rights to use,
20+
copy, modify, merge, publish, distribute, sublicense, and/or sell
21+
copies of the Software, and to permit persons to whom the
22+
Software is furnished to do so, subject to the following
23+
conditions:
24+
25+
The above copyright notice and this permission notice shall be
26+
included in all copies or substantial portions of the Software.
27+
28+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35+
OTHER DEALINGS IN THE SOFTWARE.
36+
*/
37+
38+
39+
import * as utils from './utils';
40+
41+
// Set a hash of model attributes on the object, firing `"change"`. This is
42+
// the core primitive operation of a model, updating the data and notifying
43+
// anyone who needs to know about the change in state. The heart of the beast.
44+
// This *MUST* be called with the model as the `this` context.
45+
export
46+
function set(key, val, options) {
47+
if (key == null) return this;
48+
49+
// Handle both `"key", value` and `{key: value}` -style arguments.
50+
var attrs;
51+
if (typeof key === 'object') {
52+
attrs = key;
53+
options = val;
54+
} else {
55+
(attrs = {})[key] = val;
56+
}
57+
58+
options || (options = {});
59+
60+
// Run validation.
61+
if (!this._validate(attrs, options)) return false;
62+
63+
// Extract attributes and options.
64+
var unset = options.unset;
65+
var silent = options.silent;
66+
var changes = [];
67+
var changing = this._changing;
68+
this._changing = true;
69+
70+
if (!changing) {
71+
// EDIT: changed to use object spread instead of _.clone
72+
this._previousAttributes = {...this.attributes};
73+
this.changed = {};
74+
}
75+
76+
var current = this.attributes;
77+
var changed = this.changed;
78+
var prev = this._previousAttributes;
79+
80+
// For each `set` attribute, update or delete the current value.
81+
for (var attr in attrs) {
82+
val = attrs[attr];
83+
// EDIT: the following two lines use our isEqual instead of _.isEqual
84+
if (!utils.isEqual(current[attr], val)) changes.push(attr);
85+
if (!utils.isEqual(prev[attr], val)) {
86+
changed[attr] = val;
87+
} else {
88+
delete changed[attr];
89+
}
90+
unset ? delete current[attr] : current[attr] = val;
91+
}
92+
93+
// Update the `id`.
94+
this.id = this.get(this.idAttribute);
95+
96+
// Trigger all relevant attribute changes.
97+
if (!silent) {
98+
if (changes.length) this._pending = options;
99+
for (var i = 0; i < changes.length; i++) {
100+
this.trigger('change:' + changes[i], this, current[changes[i]], options);
101+
}
102+
}
103+
104+
// You might be wondering why there's a `while` loop here. Changes can
105+
// be recursively nested within `"change"` events.
106+
if (changing) return this;
107+
if (!silent) {
108+
while (this._pending) {
109+
options = this._pending;
110+
this._pending = false;
111+
this.trigger('change', this, options);
112+
}
113+
}
114+
this._pending = false;
115+
this._changing = false;
116+
return this;
117+
}

packages/base/src/manager-base.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
import * as _ from 'underscore';
54
import * as utils from './utils';
65
import * as semver from 'semver';
76
import * as Backbone from 'backbone';
@@ -248,7 +247,9 @@ abstract class ManagerBase<T> {
248247
let commPromise;
249248
// we check to make sure the view information is provided, to help catch
250249
// backwards incompatibility errors.
251-
if (_.contains([options.view_name, options.view_module, options.view_module_version], undefined)) {
250+
if (options.view_name === undefined
251+
|| options.view_module === undefined
252+
|| options.view_module_version === undefined) {
252253
return Promise.reject("new_widget(...) must be given view information in the options.");
253254
}
254255
// If no comm is provided, a new comm is opened for the jupyter.widget
@@ -273,7 +274,7 @@ abstract class ManagerBase<T> {
273274
);
274275
}
275276
// The options dictionary is copied since data will be added to it.
276-
let options_clone = _.clone(options);
277+
let options_clone = {...options};
277278
// Create the model. In the case where the comm promise is rejected a
278279
// comm-less model is still created with the required model id.
279280
return commPromise.then((comm) => {

packages/base/src/services-shim.ts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* embed live widgets in a context outside of the notebook.
88
*/
99

10-
import * as _ from 'underscore';
1110
import * as utils from './utils';
1211
import {
1312
Kernel

packages/base/src/utils.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
11
// Copyright (c) Jupyter Development Team.
22
// Distributed under the terms of the Modified BSD License.
33

4-
import * as _ from 'underscore';
4+
export {
5+
isEqual, difference
6+
} from 'lodash';
7+
8+
import {
9+
isPlainObject
10+
} from 'lodash';
511

612
import {
713
toByteArray, fromByteArray
814
} from 'base64-js';
915

16+
/**
17+
* A polyfill for Object.assign
18+
*
19+
* This is from code that Typescript 2.4 generates for a polyfill.
20+
*/
21+
export
22+
let assign = (Object as any).assign || function(t) {
23+
for (var s, i = 1, n = arguments.length; i < n; i++) {
24+
s = arguments[i];
25+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
26+
t[p] = s[p];
27+
}
28+
return t;
29+
};
30+
1031
/**
1132
* http://www.ietf.org/rfc/rfc4122.txt
1233
*/
@@ -103,6 +124,8 @@ function put_buffers(state, buffer_paths: (string | number)[][], buffers: DataVi
103124
}
104125
}
105126

127+
128+
106129
/**
107130
* The inverse of put_buffers, return an objects with the new state where all buffers(ArrayBuffer)
108131
* are removed. If a buffer is a member of an object, that object is cloned, and the key removed. If a buffer
@@ -131,7 +154,7 @@ function remove_buffers(state): {state: any, buffers: ArrayBuffer[], buffer_path
131154
if(value) {
132155
if (value.buffer instanceof ArrayBuffer || value instanceof ArrayBuffer) {
133156
if(!is_cloned) {
134-
obj = _.clone(obj);
157+
obj = obj.slice();
135158
is_cloned = true;
136159
}
137160
buffers.push(value instanceof ArrayBuffer ? value : value.buffer);
@@ -144,35 +167,34 @@ function remove_buffers(state): {state: any, buffers: ArrayBuffer[], buffer_path
144167
// only assigned when the value changes, we may serialize objects that don't support assignment
145168
if(new_value !== value) {
146169
if(!is_cloned) {
147-
obj = _.clone(obj);
170+
obj = obj.slice();
148171
is_cloned = true;
149172
}
150173
obj[i] = new_value;
151174
}
152175
}
153176
}
154177
}
155-
} else if(_.isObject(obj)) {
178+
} else if(isPlainObject(obj)) {
156179
for (let key in obj) {
157180
let is_cloned = false;
158181
if (obj.hasOwnProperty(key)) {
159182
let value = obj[key];
160183
if(value) {
161184
if (value.buffer instanceof ArrayBuffer || value instanceof ArrayBuffer) {
162185
if(!is_cloned) {
163-
obj = _.clone(obj);
186+
obj = {...obj};
164187
is_cloned = true;
165188
}
166189
buffers.push(value instanceof ArrayBuffer ? value : value.buffer);
167190
buffer_paths.push(path.concat([key]));
168191
delete obj[key]; // for objects/dicts we just delete them
169-
}
170-
else {
192+
} else {
171193
let new_value = remove(value, path.concat([key]));
172194
// only assigned when the value changes, we may serialize objects that don't support assignment
173195
if(new_value !== value) {
174196
if(!is_cloned) {
175-
obj = _.clone(obj);
197+
obj = {...obj};
176198
is_cloned = true;
177199
}
178200
obj[key] = new_value;

0 commit comments

Comments
 (0)