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

Feat: ErrorWidget as fallback when widgets models or views fail. #2960

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 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 @@ -90,7 +91,7 @@ export abstract class ManagerBase implements IWidgetManager {
const viewPromise = (model.state_change = model.state_change.then(
async () => {
try {
const ViewType = (await this.loadClass(
const ViewType = (await this.loadViewClass(
model.get('_view_name'),
model.get('_view_module'),
model.get('_view_module_version')
Expand Down Expand Up @@ -295,11 +296,11 @@ 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;
try {
ModelType = await model_promise;
Expand Down Expand Up @@ -476,6 +477,42 @@ 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to await the promise before returning it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to catch a possible exception

} catch (error) {
return widgets.createErrorWidget(error);
}
}

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) {
return widgets.createErrorWidgetView(error);
}
}
maartenbreddels marked this conversation as resolved.
Show resolved Hide resolved

/**
* Create a comm which can be used for communication for a widget.
*
Expand Down
50 changes: 50 additions & 0 deletions packages/base/src/errorwidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
WidgetModel,
DOMWidgetModel,
DOMWidgetView,
WidgetView
} from './widget';
import { JUPYTER_WIDGETS_VERSION } from './version';

// create a Widget Model that captures an error object
export function createErrorWidget(error: Error): typeof WidgetModel {
class ErrorWidget extends DOMWidgetModel {
constructor(attributes: any, options: any) {
attributes = {
...attributes,
_view_name: 'ErrorWidgetView',
_view_module: '@jupyter-widgets/base',
_model_module_version: JUPYTER_WIDGETS_VERSION,
_view_module_version: JUPYTER_WIDGETS_VERSION,
failed_module: attributes._model_module,
failed_model_name: attributes._model_name,
error: error
};
super(attributes, options);
}
}
return ErrorWidget;
}

export function createErrorWidgetView(error?: Error): typeof WidgetView {
martinRenou marked this conversation as resolved.
Show resolved Hide resolved
return class ErrorWidgetView extends DOMWidgetView {
render() {
const module = this.model.get('_model_module');
const name = this.model.get('_model_name');
const viewModule = this.model.get('_view_module');
const viewName = this.model.get('_view_name');
const errorMessage = String(error || this.model.get('error').stack);
this.el.innerHTML = `Failed to create WidgetView '${viewName}' from module '${viewModule}' for WidgetModel '${name}' from module '${module}', error:<pre>${errorMessage}</pre>`;
}
};
}

export class ErrorWidgetView extends DOMWidgetView {
render() {
console.log('render');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct

const module = this.model.get('failed_module');
const name = this.model.get('failed_model_name');
const errorMessage = String(this.model.get('error').stack);
this.el.innerHTML = `Failed to load widget '${name}' from module '${module}', error:<pre>${errorMessage}</pre>`;
}
}
1 change: 1 addition & 0 deletions packages/base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './viewlist';
export * from './version';
export * from './utils';
export * from './registry';
export * from './errorwidget';
3 changes: 2 additions & 1 deletion packages/jupyterlab-manager/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ function activateWidgetExtension(
LayoutModel: base.LayoutModel,
LayoutView: base.LayoutView,
StyleModel: base.StyleModel,
StyleView: base.StyleView
StyleView: base.StyleView,
ErrorWidgetView: base.ErrorWidgetView
}
});

Expand Down
19 changes: 13 additions & 6 deletions widgetsnbextension/src/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,22 +193,29 @@ export class WidgetManager extends ManagerBase {
}

loadClass(className, moduleName, moduleVersion) {
const failure = () => {
throw new Error(
'Class ' + className + ' not found in module ' + moduleName
);
};
if (moduleName === '@jupyter-widgets/controls') {
return Promise.resolve(widgets[className]);
return widgets[className]
? Promise.resolve(widgets[className])
: failure();
} else if (moduleName === '@jupyter-widgets/base') {
return Promise.resolve(base[className]);
return base[className] ? Promise.resolve(base[className]) : failure();
} else if (moduleName == '@jupyter-widgets/output') {
return Promise.resolve(outputWidgets[className]);
return outputWidgets[className]
? Promise.resolve(outputWidgets[className])
: failure();
} else {
return new Promise(function(resolve, reject) {
window.require([moduleName], resolve, reject);
}).then(function(mod) {
if (mod[className]) {
return mod[className];
} else {
return Promise.reject(
'Class ' + className + ' not found in module ' + moduleName
);
return failure();
}
});
}
Expand Down