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

ErrorWidget as fallback when widgets models or views fail - Following up #3304

Merged
merged 14 commits into from
Nov 23, 2021
108 changes: 89 additions & 19 deletions packages/base-manager/src/manager-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Distributed under the terms of the Modified BSD License.

import * as services from '@jupyterlab/services';
import * as widgets from '@jupyter-widgets/base';

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

Expand Down Expand Up @@ -128,10 +129,12 @@ export abstract class ManagerBase implements IWidgetManager {
const id = uuid();
const viewPromise = (model.state_change = model.state_change.then(
async () => {
const _view_name = model.get('_view_name');
const _view_module = model.get('_view_module');
try {
const ViewType = (await this.loadClass(
model.get('_view_name'),
model.get('_view_module'),
const ViewType = (await this.loadViewClass(
_view_name,
_view_module,
model.get('_view_module_version')
)) as typeof WidgetView;
const view = new ViewType({
Expand All @@ -153,7 +156,16 @@ export abstract class ManagerBase implements IWidgetManager {
console.error(
`Could not create a view for model id ${model.model_id}`
);
throw e;
const msg = `Failed to create view for '${_view_name}' from module '${_view_module}' with model '${model.name}' from module '${model.module}'`;
const ModelCls = widgets.createErrorWidgetModel(e, msg);
const errorModel = new ModelCls();
const view = new widgets.ErrorWidgetView({
model: errorModel,
options: this.setViewOptions(options),
});
await view.render();

return view;
}
}
));
Expand Down Expand Up @@ -330,35 +342,53 @@ export abstract class ManagerBase implements IWidgetManager {
serialized_state: any = {}
): Promise<WidgetModel> {
const model_id = options.model_id;
const model_promise = this.loadClass(
const model_promise = this.loadModelClass(
options.model_name,
options.model_module,
options.model_module_version
) as Promise<typeof WidgetModel>;
);
let ModelType: typeof WidgetModel;

const makeErrorModel = (error: any, msg: string) => {
const Cls = widgets.createErrorWidgetModel(error, msg);
const widget_model = new Cls();
return widget_model;
};

try {
ModelType = await model_promise;
} catch (error) {
console.error('Could not instantiate widget');
throw error;
const msg = 'Could not instantiate widget';
console.error(msg);
return makeErrorModel(error, msg);
}

if (!ModelType) {
throw new Error(
const msg = 'Could not instantiate widget';
console.error(msg);
const error = new Error(
`Cannot find model module ${options.model_module}@${options.model_module_version}, ${options.model_name}`
);
return makeErrorModel(error, msg);
}
let widget_model: WidgetModel;
try {
const attributes = await ModelType._deserialize_state(
serialized_state,
this
);
const modelOptions: IBackboneModelOptions = {
widget_manager: this,
model_id: model_id,
comm: options.comm,
};

const attributes = await ModelType._deserialize_state(
serialized_state,
this
);
const modelOptions: IBackboneModelOptions = {
widget_manager: this,
model_id: model_id,
comm: options.comm,
};
const widget_model = new ModelType(attributes, modelOptions);
widget_model = new ModelType(attributes, modelOptions);
} catch (error) {
console.error(error);
const msg = `Model class '${options.model_name}' from module '${options.model_module}' is loaded but can not be instantiated`;
widget_model = makeErrorModel(error, msg);
}
widget_model.name = options.model_name;
widget_model.module = options.model_module;
return widget_model;
Expand Down Expand Up @@ -520,6 +550,46 @@ export abstract class ManagerBase implements IWidgetManager {
moduleVersion: string
): Promise<typeof WidgetModel | typeof WidgetView>;

protected async loadModelClass(
className: string,
moduleName: string,
moduleVersion: string
): Promise<typeof WidgetModel> {
try {
const promise: Promise<typeof WidgetModel> = this.loadClass(
className,
moduleName,
moduleVersion
) as Promise<typeof WidgetModel>;
await promise;
return promise;
} catch (error) {
console.error(error);
const msg = `Failed to load model class '${className}' from module '${moduleName}'`;
martinRenou marked this conversation as resolved.
Show resolved Hide resolved
return widgets.createErrorWidgetModel(error, msg);
}
}

protected async loadViewClass(
className: string,
moduleName: string,
moduleVersion: string
): Promise<typeof WidgetView> {
try {
const promise: Promise<typeof WidgetView> = this.loadClass(
className,
moduleName,
moduleVersion
) as Promise<typeof WidgetView>;
await promise;
return promise;
} catch (error) {
console.error(error);
const msg = `Failed to load view class '${className}' from module '${moduleName}'`;
return widgets.createErrorWidgetView(error, msg);
}
}

/**
* Create a comm which can be used for communication for a widget.
*
Expand Down
49 changes: 49 additions & 0 deletions packages/base-manager/test/src/dummy-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,51 @@ class TestWidget extends widgets.WidgetModel {
};
}
}
class ModelErrorWidget extends widgets.WidgetModel {
defaults(): Backbone.ObjectHash {
return {
...super.defaults(),
_model_module: 'test-widgets',
_model_name: 'ModelErrorWidget',
_model_module_version: '1.0.0',
};
}
initialize(attributes: Backbone.ObjectHash, options: any) {
throw new Error('Model error');
}
}
class ModelWithMissingView extends widgets.WidgetModel {
defaults(): Backbone.ObjectHash {
return {
...super.defaults(),
_model_module: 'test-widgets',
_model_name: 'ModelWithViewError',
_model_module_version: '1.0.0',
_view_module: 'test-widgets',
_view_name: 'MissingView',
_view_module_version: '1.0.0',
};
}
}
class ModelWithViewError extends widgets.WidgetModel {
defaults(): Backbone.ObjectHash {
return {
...super.defaults(),
_model_module: 'test-widgets',
_model_name: 'ModelWithViewError',
_model_module_version: '1.0.0',
_view_module: 'test-widgets',
_view_name: 'ViewErrorWidget',
_view_module_version: '1.0.0',
};
}
}

class ViewErrorWidget extends widgets.WidgetView {
render(): void {
throw new Error('Render error');
}
}

class TestWidgetView extends widgets.WidgetView {
render(): void {
Expand Down Expand Up @@ -136,6 +181,10 @@ const testWidgets = {
TestWidgetView,
BinaryWidget,
BinaryWidgetView,
ModelErrorWidget,
ModelWithViewError,
ViewErrorWidget,
ModelWithMissingView,
};

export class DummyManager extends ManagerBase {
Expand Down
58 changes: 57 additions & 1 deletion packages/base-manager/test/src/manager_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,36 @@ describe('ManagerBase', function () {
expect(view._rendered).to.equal(1);
});

it('return ErrorWidget if view class can not be loaded', async function () {
const spec = {
model_name: 'ModelWithMissingView',
model_module: 'test-widgets',
model_module_version: '1.0.0',
model_id: 'id',
};
const manager = this.managerBase;
const model = await manager.new_model(spec);
const view = await manager.create_view(model);
expect(view.generateErrorMessage()['msg']).to.be.equal(
"Failed to load view class 'MissingView' from module 'test-widgets'"
);
});

it('return ErrorWidget if view class can not be created', async function () {
const spec = {
model_name: 'ModelWithViewError',
model_module: 'test-widgets',
model_module_version: '1.0.0',
model_id: 'id',
};
const manager = this.managerBase;
const model = await manager.new_model(spec);
const view = await manager.create_view(model);
expect(view.generateErrorMessage()['msg']).to.be.equal(
"Failed to create view for 'ViewErrorWidget' from module 'test-widgets' with model 'ModelWithViewError' from module 'test-widgets'"
);
});

it('removes the view on model destroy', async function () {
const manager = this.managerBase;
const model = await manager.new_model(this.modelOptions);
Expand Down Expand Up @@ -313,7 +343,33 @@ describe('ManagerBase', function () {
);
});

it('throws an error if there is an error loading the class');
it('return ErrorWidget if model class can not be loaded', async function () {
const spec = {
model_name: 'Foo',
model_module: 'bar',
model_module_version: '1.0.0',
model_id: 'id',
};
const manager = this.managerBase;
const model = await manager.new_model(spec);
expect(model.get('msg')).to.be.equal(
"Failed to load model class 'Foo' from module 'bar'"
);
});

it('return ErrorWidget if model can not be created', async function () {
const spec = {
model_name: 'ModelErrorWidget',
model_module: 'test-widgets',
model_module_version: '1.0.0',
model_id: 'id',
};
const manager = this.managerBase;
const model = await manager.new_model(spec);
expect(model.get('msg')).to.be.equal(
"Model class 'ModelErrorWidget' from module 'test-widgets' is loaded but can not be instantiated"
);
});

it('does not sync on creation', async function () {
const comm = new MockComm();
Expand Down
25 changes: 25 additions & 0 deletions packages/base/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,28 @@
padding: 3px;
align-self: flex-start;
}

.jupyter-widgets-error-widget {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
border: solid 1px red;
margin: 0 auto;
}

.jupyter-widgets-error-widget.icon-error {
min-width: var(--jp-widgets-inline-width-short);
}
.jupyter-widgets-error-widget.text-error {
min-width: calc(2 * var(--jp-widgets-inline-width));
min-height: calc(3 * var(--jp-widgets-inline-height));
}

.jupyter-widgets-error-widget p {
text-align: center;
}

.jupyter-widgets-error-widget.text-error pre::first-line {
font-weight: bold;
}
Loading