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

Port #3337 and #3377 to master #3389

Merged
merged 9 commits into from
Feb 18, 2022
2 changes: 1 addition & 1 deletion docs/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ See also the
- Fetch the full widget state via a control Comm ([#3021](https://github.com/jupyter-widgets/ipywidgets/pull/3021))
- Export LabWidgetManager and KernelWidgetManager ([#3166](https://github.com/jupyter-widgets/ipywidgets/pull/3166))
- More helpful semver range message ([#3185](https://github.com/jupyter-widgets/ipywidgets/pull/3185))

- Make the base widget manager `.get_model()` method always return a Promise, which is rejected if the requested model is not registered. To test if a model is registered, use the new `.has_model()` method ([#3389](https://github.com/jupyter-widgets/ipywidgets/pull/3389))

### Documentation improvements
- Documentation overhaul ([#3104](https://github.com/jupyter-widgets/ipywidgets/pull/3104),
Expand Down
4 changes: 2 additions & 2 deletions examples/web3/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ document.addEventListener('DOMContentLoaded', async function (event) {
const widgetData: any =
msg.content.data['application/vnd.jupyter.widget-view+json'];
if (widgetData !== undefined && widgetData.version_major === 2) {
const model = await manager.get_model(widgetData.model_id);
if (model !== undefined) {
if (manager.has_model(widgetData.model_id)) {
const model = await manager.get_model(widgetData.model_id)!;
manager.display_view(manager.create_view(model), widgetarea);
}
}
Expand Down
190 changes: 99 additions & 91 deletions packages/base-manager/src/manager-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,26 @@ export abstract class ManagerBase implements IWidgetManager {
* Get a promise for a model by model id.
*
* #### Notes
* If a model is not found, undefined is returned (NOT a promise). However,
* the calling code should also deal with the case where a rejected promise
* is returned, and should treat that also as a model not found.
* If the model is not found, the returned Promise object is rejected.
*
* If you would like to synchronously test if a model exists, use .has_model().
*/
async get_model(model_id: string): Promise<WidgetModel> {
const modelPromise = this._models[model_id];
if (modelPromise === undefined) {
throw new Error('widget model not found');
}
return modelPromise;
}

/**
* Returns true if the given model is registered, otherwise false.
*
* #### Notes
* This is a synchronous way to check if a model is registered.
*/
get_model(model_id: string): Promise<WidgetModel> | undefined {
// TODO: Perhaps we should return a Promise.reject if the model is not
// found. Right now this isn't a true async function because it doesn't
// always return a promise.
return this._models[model_id];
has_model(model_id: string): boolean {
return this._models[model_id] !== undefined;
}

/**
Expand Down Expand Up @@ -364,7 +375,7 @@ export abstract class ManagerBase implements IWidgetManager {
/**
* Fetch all widgets states from the kernel using the control comm channel
* If this fails (control comm handler not implemented kernel side),
* it will fallback to `_loadFromKernelSlow`.
* it will fall back to `_loadFromKernelModels`.
*
* This is a utility function that can be used in subclasses.
*/
Expand Down Expand Up @@ -417,51 +428,67 @@ export abstract class ManagerBase implements IWidgetManager {
initComm.close();
} catch (error) {
console.warn(
'Failed to fetch widgets through the "jupyter.widget.control" comm channel, fallback to slow fetching of widgets. Reason:',
'Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel, fallback to fetching individual model state. Reason:',
error
);
// Fallback to the old implementation for old ipywidgets backend versions (<=7.6)
return this._loadFromKernelSlow();
// Fall back to the old implementation for old ipywidgets backend versions (ipywidgets<=7.6)
return this._loadFromKernelModels();
}

const states: any = data.states;

// Extract buffer paths
const bufferPaths: any = {};
for (const bufferPath of data.buffer_paths) {
if (!bufferPaths[bufferPath[0]]) {
bufferPaths[bufferPath[0]] = [];
const bufferGroups: any = {};

// Group buffers and buffer paths by widget id
for (let i = 0; i < data.buffer_paths.length; i++) {
const [widget_id, ...path] = data.buffer_paths[i];
const b = buffers[i];
if (!bufferPaths[widget_id]) {
bufferPaths[widget_id] = [];
bufferGroups[widget_id] = [];
}
bufferPaths[bufferPath[0]].push(bufferPath.slice(1));
bufferPaths[widget_id].push(path);
bufferGroups[widget_id].push(b);
}

// Start creating all widgets
await Promise.all(
// Create comms for all new widgets.
const widget_comms = await Promise.all(
Object.keys(states).map(async (widget_id) => {
const comm = this.has_model(widget_id)
? undefined
: await this._create_comm('jupyter.widget', widget_id);
return { widget_id, comm };
})
);

await Promise.all(
widget_comms.map(async ({ widget_id, comm }) => {
const state = states[widget_id];
// Put binary buffers
if (widget_id in bufferPaths) {
put_buffers(state, bufferPaths[widget_id], bufferGroups[widget_id]);
}
try {
const state = states[widget_id];
const comm = await this._create_comm('jupyter.widget', widget_id);

// Put binary buffers
if (widget_id in bufferPaths) {
const nBuffers = bufferPaths[widget_id].length;
put_buffers(
state,
bufferPaths[widget_id],
buffers.splice(0, nBuffers)
if (comm) {
// This must be the first await in the code path that
// reaches here so that registering the model promise in
// new_model can register the widget promise before it may
// be required by other widgets.
await this.new_model(
{
model_name: state.model_name,
model_module: state.model_module,
model_module_version: state.model_module_version,
model_id: widget_id,
comm: comm,
},
state.state
);
} else {
// model already exists here
const model = await this.get_model(widget_id);
model!.set_state(state.state);
}

await this.new_model(
{
model_name: state.model_name,
model_module: state.model_module,
model_module_version: state.model_module_version,
model_id: widget_id,
comm: comm,
},
state.state
);
} catch (error) {
// Failed to create a widget model, we continue creating other models so that
// other widgets can render
Expand All @@ -472,63 +499,46 @@ export abstract class ManagerBase implements IWidgetManager {
}

/**
* Old implementation of fetching widgets one by one using
* Old implementation of fetching widget models one by one using
* the request_state message on each comm.
*
* This is a utility function that can be used in subclasses.
*/
protected async _loadFromKernelSlow(): Promise<void> {
protected async _loadFromKernelModels(): Promise<void> {
const comm_ids = await this._get_comm_info();

// For each comm id that we do not know about, create the comm, and request the state.
const widgets_info = await Promise.all(
Object.keys(comm_ids).map(async (comm_id) => {
try {
const model = this.get_model(comm_id);
// TODO Have the same this.get_model implementation for
// the widgetsnbextension and labextension, the one that
// throws an error if the model is not found instead of
// returning undefined
if (model === undefined) {
throw new Error('widget model not found');
}
await model;
// If we successfully get the model, do no more.
if (this.has_model(comm_id)) {
return;
} catch (e) {
// If we have the widget model not found error, then we can create the
// widget. Otherwise, rethrow the error. We have to check the error
// message text explicitly because the get_model function in this
// class throws a generic error with this specific text.
if (e.message !== 'widget model not found') {
throw e;
}

const comm = await this._create_comm(this.comm_target_name, comm_id);

let msg_id = '';
const info = new PromiseDelegate<Private.ICommUpdateData>();
comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => {
if (
(msg.parent_header as any).msg_id === msg_id &&
msg.header.msg_type === 'comm_msg' &&
msg.content.data.method === 'update'
) {
const data = msg.content.data as any;
const buffer_paths = data.buffer_paths || [];
const buffers = msg.buffers || [];
put_buffers(data.state, buffer_paths, buffers);
info.resolve({ comm, msg });
}
const comm = await this._create_comm(this.comm_target_name, comm_id);

let msg_id = '';
const info = new PromiseDelegate<Private.ICommUpdateData>();
comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => {
if (
(msg.parent_header as any).msg_id === msg_id &&
msg.header.msg_type === 'comm_msg' &&
msg.content.data.method === 'update'
) {
const data = msg.content.data as any;
const buffer_paths = data.buffer_paths || [];
const buffers = msg.buffers || [];
put_buffers(data.state, buffer_paths, buffers);
info.resolve({ comm, msg });
}
});
msg_id = comm.send(
{
method: 'request_state',
},
this.callbacks(undefined)
);
});
msg_id = comm.send(
{
method: 'request_state',
},
this.callbacks(undefined)
);

return info.promise;
}
return info.promise;
})
);

Expand Down Expand Up @@ -689,8 +699,8 @@ export abstract class ManagerBase implements IWidgetManager {

// If the model has already been created, set its state and then
// return it.
if (this._models[model_id] !== undefined) {
return this._models[model_id].then((model) => {
if (this.has_model(model_id)) {
return this.get_model(model_id)!.then((model) => {
// deserialize state
return (model.constructor as typeof WidgetModel)
._deserialize_state(modelState || {}, this)
Expand Down Expand Up @@ -841,9 +851,7 @@ export abstract class ManagerBase implements IWidgetManager {
protected filterExistingModelState(serialized_state: any): any {
let models = serialized_state.state;
models = Object.keys(models)
.filter((model_id) => {
return !this._models[model_id];
})
.filter((model_id) => !this.has_model(model_id))
.reduce<IManagerStateMap>((res, model_id) => {
res[model_id] = models[model_id];
return res;
Expand Down
22 changes: 17 additions & 5 deletions packages/base-manager/test/src/manager_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,20 @@ describe('ManagerBase', function () {
expect(await manager.get_model(model.model_id)).to.be.equal(model);
});

it('returns undefined when model is not registered', function () {
expect(this.managerBase.get_model('not-defined')).to.be.undefined;
it('returns rejected promise when model is not registered', function () {
expect(this.managerBase.get_model('not-defined')).to.be.rejected;
});
});

describe('has_model', function () {
it('returns true when the model is registered', async function () {
const manager = this.managerBase;
const model = await manager.new_model(this.modelOptions);
expect(manager.has_model(model.model_id)).to.be.true;
});

it('returns false when the model is not registered', function () {
expect(this.managerBase.has_model('not-defined')).to.be.false;
});
});

Expand Down Expand Up @@ -422,7 +434,7 @@ describe('ManagerBase', function () {
const manager = this.managerBase;
const model = await manager.new_model(spec);
comm.close();
expect(manager.get_model(model.model_id)).to.be.undefined;
expect(manager.get_model(model.model_id)).to.be.rejected;
});
});

Expand All @@ -445,8 +457,8 @@ describe('ManagerBase', function () {
expect(await manager.get_model(model1.model_id)).to.be.equal(model1);
expect(await manager.get_model(model2.model_id)).to.be.equal(model2);
await manager.clear_state();
expect(manager.get_model(model1.model_id)).to.be.undefined;
expect(manager.get_model(model2.model_id)).to.be.undefined;
expect(manager.get_model(model1.model_id)).to.be.rejected;
expect(manager.get_model(model2.model_id)).to.be.rejected;
expect((comm1.close as any).calledOnce).to.be.true;
expect((comm2.close as any).calledOnce).to.be.true;
expect(model1.comm).to.be.undefined;
Expand Down
10 changes: 9 additions & 1 deletion packages/base/src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,15 @@ export interface IWidgetManager {
* the calling code should also deal with the case where a rejected promise
* is returned, and should treat that also as a model not found.
*/
get_model(model_id: string): Promise<WidgetModel> | undefined;
get_model(model_id: string): Promise<WidgetModel>;

/**
* Returns true if the given model is registered, otherwise false.
*
* #### Notes
* This is a synchronous way to check if a model is registered.
*/
has_model(model_id: string): boolean;

/**
* Register a model instance promise with the manager.
Expand Down
4 changes: 2 additions & 2 deletions packages/base/src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export function unpack_models(
});
return utils.resolvePromisesDict(unpacked);
} else if (typeof value === 'string' && value.slice(0, 10) === 'IPY_MODEL_') {
// get_model returns a promise already (except when it returns undefined!)
return Promise.resolve(manager.get_model(value.slice(10, value.length)));
// get_model returns a promise already
return manager.get_model(value.slice(10, value.length));
} else {
return Promise.resolve(value);
}
Expand Down
27 changes: 19 additions & 8 deletions packages/base/test/src/dummy-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,26 @@ export class DummyManager implements widgets.IWidgetManager {
* Get a promise for a model by model id.
*
* #### Notes
* If a model is not found, undefined is returned (NOT a promise). However,
* the calling code should also deal with the case where a rejected promise
* is returned, and should treat that also as a model not found.
* If the model is not found, the returned Promise object is rejected.
*
* If you would like to synchronously test if a model exists, use .has_model().
*/
async get_model(model_id: string): Promise<widgets.WidgetModel> {
const modelPromise = this._models[model_id];
if (modelPromise === undefined) {
throw new Error('widget model not found');
}
return modelPromise;
}

/**
* Returns true if the given model is registered, otherwise false.
*
* #### Notes
* This is a synchronous way to check if a model is registered.
*/
get_model(model_id: string): Promise<widgets.WidgetModel> | undefined {
// TODO: Perhaps we should return a Promise.reject if the model is not
// found. Right now this isn't a true async function because it doesn't
// always return a promise.
return this._models[model_id];
has_model(model_id: string): boolean {
return this._models[model_id] !== undefined;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/html-manager/src/libembed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// Distributed under the terms of the Modified BSD License.

declare let __webpack_public_path__: string;
// eslint-disable-next-line prefer-const
/* eslint-disable prefer-const, @typescript-eslint/no-unused-vars */
__webpack_public_path__ =
(window as any).__jupyter_widgets_assets_path__ || __webpack_public_path__;
/* eslint-enable prefer-const, @typescript-eslint/no-unused-vars */

import '@fortawesome/fontawesome-free/css/all.min.css';
import '@fortawesome/fontawesome-free/css/v4-shims.min.css';
Expand Down
Loading