Skip to content

Commit

Permalink
fix: backport of jupyter-widgets#3738 (pack_models/structuredClone fix)
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed Mar 28, 2023
1 parent 560e43a commit 2f1d55a
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 18 deletions.
81 changes: 65 additions & 16 deletions packages/base/src/widget.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as backbonePatch from './backbone-patch';
import * as managerBase from './manager-base';
import * as utils from './utils';
import * as backbonePatch from './backbone-patch';

import * as Backbone from 'backbone';
import $ from 'jquery';

import {
NativeView
NativeView
} from './nativeview';

import {
Widget, Panel
} from '@lumino/widgets';
import { Panel, Widget } from '@lumino/widgets';

import { JSONObject, JSONValue } from '@lumino/coreutils';

import { Dict } from './utils';

import {
Message, MessageLoop
Message, MessageLoop
} from '@lumino/messaging';

import {
IClassicComm, ICallbacks
} from './services-shim';
import { ICallbacks, IClassicComm } from './services-shim';

import {
JUPYTER_WIDGETS_VERSION
JUPYTER_WIDGETS_VERSION
} from './version';

import {
KernelMessage
KernelMessage
} from '@jupyterlab/services';

/**
* The magic key used in the widget graph serialization.
*/
const IPY_MODEL_ = 'IPY_MODEL_';

/**
* A best-effort method for performing deep copies.
*/
const deepcopyJSON = (x: JSONValue) => JSON.parse(JSON.stringify(x));

const deepcopy = (globalThis as any).structuredClone || deepcopyJSON;

/**
* Replace model ids with models recursively.
*/
Expand All @@ -57,6 +69,37 @@ function unpack_models(value: any, manager: managerBase.ManagerBase<any>): Promi
}
}

/** Replace models with ids recursively.
*
* If the commonly-used `unpack_models` is given as the `deseralize` method,
* pack_models would be the appropriate `serialize`.
* However, the default serialize method will have the same effect, when
* `unpack_models` is used as the deserialize method.
* This is to ensure backwards compatibility, see:
* https://github.com/jupyter-widgets/ipywidgets/pull/3738/commits/f9e27328bb631eb5247a7a6563595d3e655492c7#diff-efb19099381ae8911dd7f69b015a0138d08da7164512c1ee112aa75100bc9be2
*/
export function pack_models(
value: WidgetModel | Dict<WidgetModel> | WidgetModel[] | any,
widget?: WidgetModel
): any | Dict<unknown> | string | (Dict<unknown> | string)[] {
if (Array.isArray(value)) {
const model_ids: string[] = [];
for (const model of value) {
model_ids.push(pack_models(model, widget));
}
return model_ids;
} else if (value instanceof WidgetModel) {
return `${IPY_MODEL_}${value.model_id}`;
} else if (value instanceof Object && typeof value !== 'string') {
const packed: { [key: string]: string } = {};
Object.keys(value).forEach((key) => {
packed[key] = pack_models(value[key], widget);
});
} else {
return value;
}
}


/**
* Type declaration for general widget serializers.
Expand Down Expand Up @@ -491,18 +534,24 @@ class WidgetModel extends Backbone.Model {
* primitive object that is a snapshot of the widget state that may have
* binary array buffers.
*/
serialize(state: {[key: string]: any}) {
const deepcopy =
(globalThis as any).structuredClone || ((x: any) => JSON.parse(JSON.stringify(x)));
serialize(state: {[key: string]: any}) : JSONObject {
const serializers = (this.constructor as typeof WidgetModel).serializers || {};
for (const k of Object.keys(state)) {
try {
if (serializers[k] && serializers[k].serialize) {
state[k] = (serializers[k].serialize)(state[k], this);
const keySerializers : any = serializers[k] || null;
let serialize = keySerializers?.serialize || null;
if (serialize == null && keySerializers?.deserialize === unpack_models) {
// handle https://github.com/jupyter-widgets/ipywidgets/issues/3735
serialize = deepcopyJSON;
}

if (serialize) {
state[k] = serialize(state[k], this);
} else {
// the default serializer just deep-copies the object
state[k] = deepcopy(state[k]);
}

if (state[k] && state[k].toJSON) {
state[k] = state[k].toJSON();
}
Expand Down
3 changes: 3 additions & 0 deletions packages/base/test/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module.exports = function (config) {
basePath: '..',
frameworks: ['mocha'],
reporters: ['mocha'],
mochaReporter: {
showDiff: true
},
files: ['test/build/bundle.js'],
port: 9876,
colors: true,
Expand Down
27 changes: 25 additions & 2 deletions packages/base/test/src/dummy-manager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import * as widgets from '../../lib';
import * as services from '@jupyterlab/services';
import * as Backbone from 'backbone';
import * as widgets from '../../lib';

import * as sinon from 'sinon';
void sinon;
Expand Down Expand Up @@ -177,4 +177,27 @@ class BinaryWidgetView extends TestWidgetView {
_rendered = 0;
}

let testWidgets = {TestWidget, TestWidgetView, BinaryWidget, BinaryWidgetView};
class ContainerWidget extends TestWidget {
static serializers = {
...widgets.WidgetModel.serializers,
children: { deserialize: widgets.unpack_models },
};
defaults() {
return {
...super.defaults(),
_model_name: 'ContainerWidget',
_view_name: 'ContainerWidgetView',
// @ts-ignore
children: [],
};
}
}

class ContainerWidgetView extends TestWidgetView {
render(): void {
this._rendered += 1;
}
_rendered = 0;
}

let testWidgets = {TestWidget, TestWidgetView, BinaryWidget, BinaryWidgetView, ContainerWidget, ContainerWidgetView};
62 changes: 62 additions & 0 deletions packages/base/test/src/widget_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,68 @@ describe('unpack_models', function() {
});
});

describe('serialize/deserialize', function () {
before(async function () {
this.manager = new DummyManager();
this.widgetChild = await this.manager.new_widget({
model_name: 'WidgetModel',
model_module: '@jupyter-widgets/base',
model_module_version: '1.2.0',
view_name: 'WidgetView',
view_module: '@jupyter-widgets/base',
view_module_version: '1.2.0',
model_id: 'widgetChild',
});

this.widgetChild2 = await this.manager.new_widget({
model_name: 'WidgetModel',
model_module: '@jupyter-widgets/base',
model_module_version: '1.2.0',
view_name: 'WidgetView',
view_module: '@jupyter-widgets/base',
view_module_version: '1.2.0',
model_id: 'widgetChild2',
});

this.widgetContainer = await this.manager.new_widget(
{
model_name: 'ContainerWidget',
model_module: 'test-widgets',
model_module_version: '1.2.0',
view_name: 'ContainerWidgetView',
view_module: 'test-widgets',
view_module_version: '1.2.0',
model_id: 'widgetContainer',
},
{ children: [`IPY_MODEL_${this.widgetChild.model_id}`] }
);
});
it('serializes', function () {
const state = this.widgetContainer.get_state(false);
const serializedState = this.widgetContainer.serialize(state);
expect(serializedState).to.deep.equal({
_model_module: 'test-widgets',
_model_module_version: '1.0.0',
_model_name: 'ContainerWidget',
_view_count: null,
_view_module: 'test-widgets',
_view_module_version: '1.0.0',
_view_name: 'ContainerWidgetView',
children: ['IPY_MODEL_widgetChild'],
});
});
it('deserializes', async function () {
const serializedState = { children: ['IPY_MODEL_widgetChild2'] };
const state = await (
this.widgetContainer.constructor as typeof WidgetModel
)._deserialize_state(serializedState, this.manager);
await this.widgetContainer.set_state(state);
expect(this.widgetContainer.get('children')).to.deep.equal([
this.widgetChild2,
]);
});
});

describe('WidgetModel', function() {
before(async function() {
this.setup = async function() {
Expand Down

0 comments on commit 2f1d55a

Please sign in to comment.