Skip to content

Commit 940c10d

Browse files
authored
Merge pull request #3389 from jasongrout/forwardport
Port #3337 and #3377 to master
2 parents 6518816 + 2c595ce commit 940c10d

File tree

11 files changed

+166
-142
lines changed

11 files changed

+166
-142
lines changed

docs/source/changelog.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ See also the
8484
- Fetch the full widget state via a control Comm ([#3021](https://github.com/jupyter-widgets/ipywidgets/pull/3021))
8585
- Export LabWidgetManager and KernelWidgetManager ([#3166](https://github.com/jupyter-widgets/ipywidgets/pull/3166))
8686
- More helpful semver range message ([#3185](https://github.com/jupyter-widgets/ipywidgets/pull/3185))
87-
87+
- 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))
8888

8989
### Documentation improvements
9090
- Documentation overhaul ([#3104](https://github.com/jupyter-widgets/ipywidgets/pull/3104),

examples/web3/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ document.addEventListener('DOMContentLoaded', async function (event) {
5555
const widgetData: any =
5656
msg.content.data['application/vnd.jupyter.widget-view+json'];
5757
if (widgetData !== undefined && widgetData.version_major === 2) {
58-
const model = await manager.get_model(widgetData.model_id);
59-
if (model !== undefined) {
58+
if (manager.has_model(widgetData.model_id)) {
59+
const model = await manager.get_model(widgetData.model_id)!;
6060
manager.display_view(manager.create_view(model), widgetarea);
6161
}
6262
}

packages/base-manager/src/manager-base.ts

+99-91
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,26 @@ export abstract class ManagerBase implements IWidgetManager {
210210
* Get a promise for a model by model id.
211211
*
212212
* #### Notes
213-
* If a model is not found, undefined is returned (NOT a promise). However,
214-
* the calling code should also deal with the case where a rejected promise
215-
* is returned, and should treat that also as a model not found.
213+
* If the model is not found, the returned Promise object is rejected.
214+
*
215+
* If you would like to synchronously test if a model exists, use .has_model().
216+
*/
217+
async get_model(model_id: string): Promise<WidgetModel> {
218+
const modelPromise = this._models[model_id];
219+
if (modelPromise === undefined) {
220+
throw new Error('widget model not found');
221+
}
222+
return modelPromise;
223+
}
224+
225+
/**
226+
* Returns true if the given model is registered, otherwise false.
227+
*
228+
* #### Notes
229+
* This is a synchronous way to check if a model is registered.
216230
*/
217-
get_model(model_id: string): Promise<WidgetModel> | undefined {
218-
// TODO: Perhaps we should return a Promise.reject if the model is not
219-
// found. Right now this isn't a true async function because it doesn't
220-
// always return a promise.
221-
return this._models[model_id];
231+
has_model(model_id: string): boolean {
232+
return this._models[model_id] !== undefined;
222233
}
223234

224235
/**
@@ -364,7 +375,7 @@ export abstract class ManagerBase implements IWidgetManager {
364375
/**
365376
* Fetch all widgets states from the kernel using the control comm channel
366377
* If this fails (control comm handler not implemented kernel side),
367-
* it will fallback to `_loadFromKernelSlow`.
378+
* it will fall back to `_loadFromKernelModels`.
368379
*
369380
* This is a utility function that can be used in subclasses.
370381
*/
@@ -417,51 +428,67 @@ export abstract class ManagerBase implements IWidgetManager {
417428
initComm.close();
418429
} catch (error) {
419430
console.warn(
420-
'Failed to fetch widgets through the "jupyter.widget.control" comm channel, fallback to slow fetching of widgets. Reason:',
431+
'Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel, fallback to fetching individual model state. Reason:',
421432
error
422433
);
423-
// Fallback to the old implementation for old ipywidgets backend versions (<=7.6)
424-
return this._loadFromKernelSlow();
434+
// Fall back to the old implementation for old ipywidgets backend versions (ipywidgets<=7.6)
435+
return this._loadFromKernelModels();
425436
}
426437

427438
const states: any = data.states;
428-
429-
// Extract buffer paths
430439
const bufferPaths: any = {};
431-
for (const bufferPath of data.buffer_paths) {
432-
if (!bufferPaths[bufferPath[0]]) {
433-
bufferPaths[bufferPath[0]] = [];
440+
const bufferGroups: any = {};
441+
442+
// Group buffers and buffer paths by widget id
443+
for (let i = 0; i < data.buffer_paths.length; i++) {
444+
const [widget_id, ...path] = data.buffer_paths[i];
445+
const b = buffers[i];
446+
if (!bufferPaths[widget_id]) {
447+
bufferPaths[widget_id] = [];
448+
bufferGroups[widget_id] = [];
434449
}
435-
bufferPaths[bufferPath[0]].push(bufferPath.slice(1));
450+
bufferPaths[widget_id].push(path);
451+
bufferGroups[widget_id].push(b);
436452
}
437453

438-
// Start creating all widgets
439-
await Promise.all(
454+
// Create comms for all new widgets.
455+
const widget_comms = await Promise.all(
440456
Object.keys(states).map(async (widget_id) => {
457+
const comm = this.has_model(widget_id)
458+
? undefined
459+
: await this._create_comm('jupyter.widget', widget_id);
460+
return { widget_id, comm };
461+
})
462+
);
463+
464+
await Promise.all(
465+
widget_comms.map(async ({ widget_id, comm }) => {
466+
const state = states[widget_id];
467+
// Put binary buffers
468+
if (widget_id in bufferPaths) {
469+
put_buffers(state, bufferPaths[widget_id], bufferGroups[widget_id]);
470+
}
441471
try {
442-
const state = states[widget_id];
443-
const comm = await this._create_comm('jupyter.widget', widget_id);
444-
445-
// Put binary buffers
446-
if (widget_id in bufferPaths) {
447-
const nBuffers = bufferPaths[widget_id].length;
448-
put_buffers(
449-
state,
450-
bufferPaths[widget_id],
451-
buffers.splice(0, nBuffers)
472+
if (comm) {
473+
// This must be the first await in the code path that
474+
// reaches here so that registering the model promise in
475+
// new_model can register the widget promise before it may
476+
// be required by other widgets.
477+
await this.new_model(
478+
{
479+
model_name: state.model_name,
480+
model_module: state.model_module,
481+
model_module_version: state.model_module_version,
482+
model_id: widget_id,
483+
comm: comm,
484+
},
485+
state.state
452486
);
487+
} else {
488+
// model already exists here
489+
const model = await this.get_model(widget_id);
490+
model!.set_state(state.state);
453491
}
454-
455-
await this.new_model(
456-
{
457-
model_name: state.model_name,
458-
model_module: state.model_module,
459-
model_module_version: state.model_module_version,
460-
model_id: widget_id,
461-
comm: comm,
462-
},
463-
state.state
464-
);
465492
} catch (error) {
466493
// Failed to create a widget model, we continue creating other models so that
467494
// other widgets can render
@@ -472,63 +499,46 @@ export abstract class ManagerBase implements IWidgetManager {
472499
}
473500

474501
/**
475-
* Old implementation of fetching widgets one by one using
502+
* Old implementation of fetching widget models one by one using
476503
* the request_state message on each comm.
477504
*
478505
* This is a utility function that can be used in subclasses.
479506
*/
480-
protected async _loadFromKernelSlow(): Promise<void> {
507+
protected async _loadFromKernelModels(): Promise<void> {
481508
const comm_ids = await this._get_comm_info();
482509

483510
// For each comm id that we do not know about, create the comm, and request the state.
484511
const widgets_info = await Promise.all(
485512
Object.keys(comm_ids).map(async (comm_id) => {
486-
try {
487-
const model = this.get_model(comm_id);
488-
// TODO Have the same this.get_model implementation for
489-
// the widgetsnbextension and labextension, the one that
490-
// throws an error if the model is not found instead of
491-
// returning undefined
492-
if (model === undefined) {
493-
throw new Error('widget model not found');
494-
}
495-
await model;
496-
// If we successfully get the model, do no more.
513+
if (this.has_model(comm_id)) {
497514
return;
498-
} catch (e) {
499-
// If we have the widget model not found error, then we can create the
500-
// widget. Otherwise, rethrow the error. We have to check the error
501-
// message text explicitly because the get_model function in this
502-
// class throws a generic error with this specific text.
503-
if (e.message !== 'widget model not found') {
504-
throw e;
515+
}
516+
517+
const comm = await this._create_comm(this.comm_target_name, comm_id);
518+
519+
let msg_id = '';
520+
const info = new PromiseDelegate<Private.ICommUpdateData>();
521+
comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => {
522+
if (
523+
(msg.parent_header as any).msg_id === msg_id &&
524+
msg.header.msg_type === 'comm_msg' &&
525+
msg.content.data.method === 'update'
526+
) {
527+
const data = msg.content.data as any;
528+
const buffer_paths = data.buffer_paths || [];
529+
const buffers = msg.buffers || [];
530+
put_buffers(data.state, buffer_paths, buffers);
531+
info.resolve({ comm, msg });
505532
}
506-
const comm = await this._create_comm(this.comm_target_name, comm_id);
507-
508-
let msg_id = '';
509-
const info = new PromiseDelegate<Private.ICommUpdateData>();
510-
comm.on_msg((msg: services.KernelMessage.ICommMsgMsg) => {
511-
if (
512-
(msg.parent_header as any).msg_id === msg_id &&
513-
msg.header.msg_type === 'comm_msg' &&
514-
msg.content.data.method === 'update'
515-
) {
516-
const data = msg.content.data as any;
517-
const buffer_paths = data.buffer_paths || [];
518-
const buffers = msg.buffers || [];
519-
put_buffers(data.state, buffer_paths, buffers);
520-
info.resolve({ comm, msg });
521-
}
522-
});
523-
msg_id = comm.send(
524-
{
525-
method: 'request_state',
526-
},
527-
this.callbacks(undefined)
528-
);
533+
});
534+
msg_id = comm.send(
535+
{
536+
method: 'request_state',
537+
},
538+
this.callbacks(undefined)
539+
);
529540

530-
return info.promise;
531-
}
541+
return info.promise;
532542
})
533543
);
534544

@@ -689,8 +699,8 @@ export abstract class ManagerBase implements IWidgetManager {
689699

690700
// If the model has already been created, set its state and then
691701
// return it.
692-
if (this._models[model_id] !== undefined) {
693-
return this._models[model_id].then((model) => {
702+
if (this.has_model(model_id)) {
703+
return this.get_model(model_id)!.then((model) => {
694704
// deserialize state
695705
return (model.constructor as typeof WidgetModel)
696706
._deserialize_state(modelState || {}, this)
@@ -841,9 +851,7 @@ export abstract class ManagerBase implements IWidgetManager {
841851
protected filterExistingModelState(serialized_state: any): any {
842852
let models = serialized_state.state;
843853
models = Object.keys(models)
844-
.filter((model_id) => {
845-
return !this._models[model_id];
846-
})
854+
.filter((model_id) => !this.has_model(model_id))
847855
.reduce<IManagerStateMap>((res, model_id) => {
848856
res[model_id] = models[model_id];
849857
return res;

packages/base-manager/test/src/manager_test.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,20 @@ describe('ManagerBase', function () {
147147
expect(await manager.get_model(model.model_id)).to.be.equal(model);
148148
});
149149

150-
it('returns undefined when model is not registered', function () {
151-
expect(this.managerBase.get_model('not-defined')).to.be.undefined;
150+
it('returns rejected promise when model is not registered', function () {
151+
expect(this.managerBase.get_model('not-defined')).to.be.rejected;
152+
});
153+
});
154+
155+
describe('has_model', function () {
156+
it('returns true when the model is registered', async function () {
157+
const manager = this.managerBase;
158+
const model = await manager.new_model(this.modelOptions);
159+
expect(manager.has_model(model.model_id)).to.be.true;
160+
});
161+
162+
it('returns false when the model is not registered', function () {
163+
expect(this.managerBase.has_model('not-defined')).to.be.false;
152164
});
153165
});
154166

@@ -422,7 +434,7 @@ describe('ManagerBase', function () {
422434
const manager = this.managerBase;
423435
const model = await manager.new_model(spec);
424436
comm.close();
425-
expect(manager.get_model(model.model_id)).to.be.undefined;
437+
expect(manager.get_model(model.model_id)).to.be.rejected;
426438
});
427439
});
428440

@@ -445,8 +457,8 @@ describe('ManagerBase', function () {
445457
expect(await manager.get_model(model1.model_id)).to.be.equal(model1);
446458
expect(await manager.get_model(model2.model_id)).to.be.equal(model2);
447459
await manager.clear_state();
448-
expect(manager.get_model(model1.model_id)).to.be.undefined;
449-
expect(manager.get_model(model2.model_id)).to.be.undefined;
460+
expect(manager.get_model(model1.model_id)).to.be.rejected;
461+
expect(manager.get_model(model2.model_id)).to.be.rejected;
450462
expect((comm1.close as any).calledOnce).to.be.true;
451463
expect((comm2.close as any).calledOnce).to.be.true;
452464
expect(model1.comm).to.be.undefined;

packages/base/src/manager.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,15 @@ export interface IWidgetManager {
123123
* the calling code should also deal with the case where a rejected promise
124124
* is returned, and should treat that also as a model not found.
125125
*/
126-
get_model(model_id: string): Promise<WidgetModel> | undefined;
126+
get_model(model_id: string): Promise<WidgetModel>;
127+
128+
/**
129+
* Returns true if the given model is registered, otherwise false.
130+
*
131+
* #### Notes
132+
* This is a synchronous way to check if a model is registered.
133+
*/
134+
has_model(model_id: string): boolean;
127135

128136
/**
129137
* Register a model instance promise with the manager.

packages/base/src/widget.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ export function unpack_models(
4949
});
5050
return utils.resolvePromisesDict(unpacked);
5151
} else if (typeof value === 'string' && value.slice(0, 10) === 'IPY_MODEL_') {
52-
// get_model returns a promise already (except when it returns undefined!)
53-
return Promise.resolve(manager.get_model(value.slice(10, value.length)));
52+
// get_model returns a promise already
53+
return manager.get_model(value.slice(10, value.length));
5454
} else {
5555
return Promise.resolve(value);
5656
}

packages/base/test/src/dummy-manager.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,26 @@ export class DummyManager implements widgets.IWidgetManager {
195195
* Get a promise for a model by model id.
196196
*
197197
* #### Notes
198-
* If a model is not found, undefined is returned (NOT a promise). However,
199-
* the calling code should also deal with the case where a rejected promise
200-
* is returned, and should treat that also as a model not found.
198+
* If the model is not found, the returned Promise object is rejected.
199+
*
200+
* If you would like to synchronously test if a model exists, use .has_model().
201+
*/
202+
async get_model(model_id: string): Promise<widgets.WidgetModel> {
203+
const modelPromise = this._models[model_id];
204+
if (modelPromise === undefined) {
205+
throw new Error('widget model not found');
206+
}
207+
return modelPromise;
208+
}
209+
210+
/**
211+
* Returns true if the given model is registered, otherwise false.
212+
*
213+
* #### Notes
214+
* This is a synchronous way to check if a model is registered.
201215
*/
202-
get_model(model_id: string): Promise<widgets.WidgetModel> | undefined {
203-
// TODO: Perhaps we should return a Promise.reject if the model is not
204-
// found. Right now this isn't a true async function because it doesn't
205-
// always return a promise.
206-
return this._models[model_id];
216+
has_model(model_id: string): boolean {
217+
return this._models[model_id] !== undefined;
207218
}
208219

209220
/**

packages/html-manager/src/libembed.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// Distributed under the terms of the Modified BSD License.
33

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

910
import '@fortawesome/fontawesome-free/css/all.min.css';
1011
import '@fortawesome/fontawesome-free/css/v4-shims.min.css';

0 commit comments

Comments
 (0)