diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 098ff97a95..2313b68e35 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -214,7 +214,11 @@ export abstract class ManagerBase implements IWidgetManager { * * If you would like to synchronously test if a model exists, use .has_model(). */ - async get_model(model_id: string): Promise { + async get_model(model_id: string, timeout = 1000): Promise { + let count = 0; + while (!this._models[model_id] && count++ < timeout / 10) { + await sleep(10); + } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { throw new Error(`widget model '${model_id}' not found`); @@ -919,6 +923,8 @@ export function serialize_state( return { version_major: 2, version_minor: 0, state: state }; } +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + namespace Private { /** * Data promised when a comm info request resolves. diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index fec2cb3e4e..c4870099d0 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -2,20 +2,20 @@ // Distributed under the terms of the Modified BSD License. import { - shims, + ExportData, + ExportMap, + ICallbacks, IClassicComm, IWidgetRegistryData, - ExportMap, - ExportData, WidgetModel, WidgetView, - ICallbacks, + shims, } from '@jupyter-widgets/base'; import { + IStateOptions, ManagerBase, serialize_state, - IStateOptions, } from '@jupyter-widgets/base-manager'; import { IDisposable } from '@lumino/disposable'; @@ -26,7 +26,18 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { Kernel, KernelMessage, Session } from '@jupyterlab/services'; +import { ObservableList, ObservableMap } from '@jupyterlab/observables'; + +import * as nbformat from '@jupyterlab/nbformat'; + +import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; + +import { + Kernel, + KernelConnection, + KernelMessage, + Session, +} from '@jupyterlab/services'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -36,6 +47,11 @@ import { valid } from 'semver'; import { SemVerCache } from './semvercache'; +import Backbone from 'backbone'; + +import * as base from '@jupyter-widgets/base'; +import { WidgetRenderer } from './renderer'; + /** * The mime type for a widget view. */ @@ -330,23 +346,39 @@ export abstract class LabWidgetManager this, KernelMessage.IIOPubMessage >(this); + static WIDGET_REGISTRY = new ObservableList(); } /** - * A widget manager that returns Lumino widgets. + * A singleton widget manager per kernel for the lifecycle of the kernel. */ export class KernelWidgetManager extends LabWidgetManager { constructor( kernel: Kernel.IKernelConnection, rendermime: IRenderMimeRegistry ) { + const instance = Private.kernelWidgetManagers.get(kernel.id); + if (instance) { + instance.attachToRendermime(rendermime); + return instance; + } super(rendermime); - this._kernel = kernel; - - kernel.statusChanged.connect((sender, args) => { + this.attachToRendermime(rendermime); + Private.kernelWidgetManagers.set(kernel.id, this); + this._kernel = new KernelConnection({ model: kernel.model }); + this.loadCustomWidgetDefinitions(); + LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => + this.loadCustomWidgetDefinitions() + ); + this._kernel.registerCommTarget( + this.comm_target_name, + this._handleCommOpen + ); + + this._kernel.statusChanged.connect((sender, args) => { this._handleKernelStatusChange(args); }); - kernel.connectionStatusChanged.connect((sender, args) => { + this._kernel.connectionStatusChanged.connect((sender, args) => { this._handleKernelConnectionStatusChange(args); }); @@ -405,24 +437,50 @@ export class KernelWidgetManager extends LabWidgetManager { return this._kernel; } + loadCustomWidgetDefinitions() { + for (const data of LabWidgetManager.WIDGET_REGISTRY) { + this.register(data); + } + } + + filterModelState(serialized_state: any): any { + return this.filterExistingModelState(serialized_state); + } + + attachToRendermime(rendermime: IRenderMimeRegistry) { + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options) => new WidgetRenderer(options, this), + }, + -10 + ); + } + private _kernel: Kernel.IKernelConnection; + protected _kernelRestoreInProgress = false; } /** - * A widget manager that returns phosphor widgets. + * Monitor kernel of the Context swapping the kernel manager on demand. + * A better name would be `NotebookManagerSwitcher'. */ -export class WidgetManager extends LabWidgetManager { +export class WidgetManager extends Backbone.Model implements IDisposable { constructor( context: DocumentRegistry.IContext, rendermime: IRenderMimeRegistry, settings: WidgetManager.Settings ) { - super(rendermime); + super(); + this._rendermime = rendermime; this._context = context; + this._settings = settings; - context.sessionContext.kernelChanged.connect((sender, args) => { - this._handleKernelChanged(args); - }); + context.sessionContext.kernelChanged.connect((sender, args) => + this.updateWidgetManager() + ); context.sessionContext.statusChanged.connect((sender, args) => { this._handleKernelStatusChange(args); @@ -432,17 +490,11 @@ export class WidgetManager extends LabWidgetManager { this._handleKernelConnectionStatusChange(args); }); - if (context.sessionContext.session?.kernel) { - this._handleKernelChanged({ - name: 'kernel', - oldValue: null, - newValue: context.sessionContext.session?.kernel, - }); - } + this.updateWidgetManager(); + this.setDirty(); this.restoreWidgets(this._context!.model); - this._settings = settings; context.saveState.connect((sender, saveState) => { if (saveState === 'started' && settings.saveState) { this._saveState(); @@ -454,7 +506,7 @@ export class WidgetManager extends LabWidgetManager { * Save the widget state to the context model. */ private _saveState(): void { - const state = this.get_state_sync({ drop_defaults: true }); + const state = this.widgetManager.get_state_sync({ drop_defaults: true }); if (this._context.model.setMetadata) { this._context.model.setMetadata('widgets', { 'application/vnd.jupyter.widget-state+json': state, @@ -468,6 +520,44 @@ export class WidgetManager extends LabWidgetManager { } } + updateWidgetManager() { + if (this._widgetManager) { + this.widgetManager.onUnhandledIOPubMessage.disconnect( + this.onUnhandledIOPubMessage, + this + ); + } + if (this.kernel) { + this._widgetManager = getWidgetManager(this.kernel, this.rendermime); + this._widgetManager.onUnhandledIOPubMessage.connect( + this.onUnhandledIOPubMessage, + this + ); + } + } + + onUnhandledIOPubMessage( + sender: LabWidgetManager, + msg: KernelMessage.IIOPubMessage + ) { + if (WidgetManager.loggerRegistry) { + const logger = WidgetManager.loggerRegistry.getLogger(this.context.path); + let level: LogLevel = 'warning'; + if ( + KernelMessage.isErrorMsg(msg) || + (KernelMessage.isStreamMsg(msg) && msg.content.name === 'stderr') + ) { + level = 'error'; + } + const data: nbformat.IOutput = { + ...msg.content, + output_type: msg.header.msg_type, + }; + // logger.rendermime = this.content.rendermime; + logger.log({ type: 'output', data, level }); + } + } + _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { if (status === 'connected') { // Only restore if we aren't currently trying to restore from the kernel @@ -483,9 +573,41 @@ export class WidgetManager extends LabWidgetManager { } _handleKernelStatusChange(status: Kernel.Status): void { - if (status === 'restarting') { - this.disconnect(); + this.setDirty(); + } + + get widgetManager(): KernelWidgetManager { + return this._widgetManager; + } + + /** + * A signal emitted when state is restored to the widget manager. + * + * #### Notes + * This indicates that previously-unavailable widget models might be available now. + */ + get restored(): ISignal { + return this._restored; + } + + /** + * Whether the state has been restored yet or not. + */ + get restoredStatus(): boolean { + return this._restoredStatus; + } + + /** + * + * @param renderers + */ + updateWidgetRenderers(renderers: IterableIterator) { + if (this.kernel) { + for (const r of renderers) { + r.manager = this.widgetManager; + } } + // Do we need to handle for if there isn't a kernel? } /** @@ -500,7 +622,6 @@ export class WidgetManager extends LabWidgetManager { if (loadKernel) { try { this._kernelRestoreInProgress = true; - await this._loadFromKernel(); } finally { this._kernelRestoreInProgress = false; } @@ -529,11 +650,21 @@ export class WidgetManager extends LabWidgetManager { // Restore any widgets from saved state that are not live if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) { let state = widget_md[WIDGET_STATE_MIMETYPE]; - state = this.filterExistingModelState(state); - await this.set_state(state); + state = this.widgetManager.filterModelState(state); + await this.widgetManager.set_state(state); } } + /** + * Get whether the manager is disposed. + * + * #### Notes + * This is a read-only property. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + /** * Dispose the resources held by the manager. */ @@ -543,7 +674,6 @@ export class WidgetManager extends LabWidgetManager { } this._context = null!; - super.dispose(); } /** @@ -562,11 +692,15 @@ export class WidgetManager extends LabWidgetManager { return this._context.sessionContext?.session?.kernel ?? null; } + get rendermime(): IRenderMimeRegistry { + return this._rendermime; + } + /** * Register a widget model. */ register_model(model_id: string, modelPromise: Promise): void { - super.register_model(model_id, modelPromise); + this.widgetManager.register_model(model_id, modelPromise); this.setDirty(); } @@ -575,7 +709,7 @@ export class WidgetManager extends LabWidgetManager { * @return Promise that resolves when the widget state is cleared. */ async clear_state(): Promise { - await super.clear_state(); + // await this.widgetManager.clear_state(); this.setDirty(); } @@ -589,9 +723,15 @@ export class WidgetManager extends LabWidgetManager { this._context!.model.dirty = true; } } - + static loggerRegistry: ILoggerRegistry | null; + protected _restored = new Signal(this); + protected _restoredStatus = false; + private _isDisposed = false; private _context: DocumentRegistry.IContext; + private _rendermime: IRenderMimeRegistry; private _settings: WidgetManager.Settings; + private _widgetManager: KernelWidgetManager; + protected _kernelRestoreInProgress = false; } export namespace WidgetManager { @@ -599,3 +739,49 @@ export namespace WidgetManager { saveState: boolean; }; } + +/** + * Get the widget manager for the kernel. Calling this will ensure + * widgets to work in a kernel. + * With the widgetManager use the method `widgetManager.attachToRendermime` + * against any rendermime. + * @param kernel A kernel connection to which the widget manager is associated. + * @returns LabWidgetManager + */ +export function getWidgetManager( + kernel: Kernel.IKernelConnection, + rendermime: IRenderMimeRegistry +): KernelWidgetManager { + if (!Private.kernelWidgetManagers.has(kernel.id)) { + new KernelWidgetManager(kernel, rendermime); + } + const wManager = Private.kernelWidgetManagers.get(kernel.id); + if (!wManager) { + throw new Error('Failed to create LabWidgetManager'); + } + if (wManager.rendermime !== rendermime) { + wManager.attachToRendermime(rendermime); + } + return wManager; +} + +/** + * Get the widgetManager that owns the model id=model_id. + * @param model_id An existing model_id + * @returns KernelWidgetManager + */ +export function findWidgetManager(model_id: string): KernelWidgetManager { + for (const wManager of Private.kernelWidgetManagers.values()) { + if (wManager.has_model(model_id)) { + return wManager; + } + } + throw new Error(`A widget manager was not found for model_id ${model_id}'`); +} + +/** + * A namespace for private data + */ +namespace Private { + export const kernelWidgetManagers = new ObservableMap(); +} diff --git a/python/jupyterlab_widgets/src/output.ts b/python/jupyterlab_widgets/src/output.ts index 37793bf193..ee559437da 100644 --- a/python/jupyterlab_widgets/src/output.ts +++ b/python/jupyterlab_widgets/src/output.ts @@ -7,13 +7,15 @@ import { JupyterLuminoPanelWidget } from '@jupyter-widgets/base'; import { Panel } from '@lumino/widgets'; -import { LabWidgetManager, WidgetManager } from './manager'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; + +import { LabWidgetManager } from './manager'; import { OutputAreaModel, OutputArea } from '@jupyterlab/outputarea'; import * as nbformat from '@jupyterlab/nbformat'; -import { KernelMessage, Session } from '@jupyterlab/services'; +import { KernelMessage } from '@jupyterlab/services'; import $ from 'jquery'; @@ -33,32 +35,11 @@ export class OutputModel extends outputBase.OutputModel { return false; }; - // if the context is available, react on kernel changes - if (this.widget_manager instanceof WidgetManager) { - this.widget_manager.context.sessionContext.kernelChanged.connect( - (sender, args) => { - this._handleKernelChanged(args); - } - ); - } this.listenTo(this, 'change:msg_id', this.reset_msg_id); this.listenTo(this, 'change:outputs', this.setOutputs); this.setOutputs(); } - /** - * Register a new kernel - */ - _handleKernelChanged({ - oldValue, - }: Session.ISessionConnection.IKernelChangedArgs): void { - const msgId = this.get('msg_id'); - if (msgId && oldValue) { - oldValue.removeMessageHook(msgId, this._msgHook); - this.set('msg_id', null); - } - } - /** * Reset the message id. */ @@ -121,6 +102,7 @@ export class OutputModel extends outputBase.OutputModel { private _msgHook: (msg: KernelMessage.IIOPubMessage) => boolean; private _outputs: OutputAreaModel; + static rendermime: IRenderMimeRegistry; } export class OutputView extends outputBase.OutputView { @@ -145,10 +127,11 @@ export class OutputView extends outputBase.OutputView { render(): void { super.render(); this._outputView = new OutputArea({ - rendermime: this.model.widget_manager.rendermime, + rendermime: OutputModel.rendermime, contentFactory: OutputArea.defaultContentFactory, model: this.model.outputs, }); + // TODO: why is this a readonly property now? // this._outputView.model = this.model.outputs; // TODO: why is this on the model now? diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index f0a16f05b9..68af793c33 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -2,7 +2,6 @@ // Distributed under the terms of the Modified BSD License. import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import * as nbformat from '@jupyterlab/nbformat'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -10,19 +9,18 @@ import { INotebookModel, INotebookTracker, Notebook, - NotebookPanel, } from '@jupyterlab/notebook'; import { - JupyterFrontEndPlugin, JupyterFrontEnd, + JupyterFrontEndPlugin, } from '@jupyterlab/application'; import { IMainMenu } from '@jupyterlab/mainmenu'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; +import { ILoggerRegistry } from '@jupyterlab/logconsole'; import { CodeCell } from '@jupyterlab/cells'; @@ -34,9 +32,13 @@ import { AttachedProperty } from '@lumino/properties'; import { WidgetRenderer } from './renderer'; -import { WidgetManager, WIDGET_VIEW_MIMETYPE } from './manager'; +import { + LabWidgetManager, + WidgetManager, + WIDGET_VIEW_MIMETYPE, +} from './manager'; -import { OutputModel, OutputView, OUTPUT_WIDGET_VERSION } from './output'; +import { OUTPUT_WIDGET_VERSION, OutputModel, OutputView } from './output'; import * as base from '@jupyter-widgets/base'; @@ -46,11 +48,8 @@ import { JUPYTER_CONTROLS_VERSION } from '@jupyter-widgets/controls/lib/version' import '@jupyter-widgets/base/css/index.css'; import '@jupyter-widgets/controls/css/widgets-base.css'; -import { KernelMessage } from '@jupyterlab/services'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; -const WIDGET_REGISTRY: base.IWidgetRegistryData[] = []; - /** * The cached settings. */ @@ -117,26 +116,11 @@ export function registerWidgetManager( let wManager = Private.widgetManagerProperty.get(context); if (!wManager) { wManager = new WidgetManager(context, rendermime, SETTINGS); - WIDGET_REGISTRY.forEach((data) => wManager!.register(data)); Private.widgetManagerProperty.set(context, wManager); } - - for (const r of renderers) { - r.manager = wManager; + if (wManager.kernel) { + wManager.updateWidgetRenderers(renderers); } - - // Replace the placeholder widget renderer with one bound to this widget - // manager. - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options) => new WidgetRenderer(options, wManager), - }, - -10 - ); - return new DisposableDelegate(() => { if (rendermime) { rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); @@ -183,33 +167,6 @@ function activateWidgetExtension( const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab_widgets'); - const bindUnhandledIOPubMessageSignal = (nb: NotebookPanel): void => { - if (!loggerRegistry) { - return; - } - - const wManager = Private.widgetManagerProperty.get(nb.context); - if (wManager) { - wManager.onUnhandledIOPubMessage.connect( - (sender: WidgetManager, msg: KernelMessage.IIOPubMessage) => { - const logger = loggerRegistry.getLogger(nb.context.path); - let level: LogLevel = 'warning'; - if ( - KernelMessage.isErrorMsg(msg) || - (KernelMessage.isStreamMsg(msg) && msg.content.name === 'stderr') - ) { - level = 'error'; - } - const data: nbformat.IOutput = { - ...msg.content, - output_type: msg.header.msg_type, - }; - logger.rendermime = nb.content.rendermime; - logger.log({ type: 'output', data, level }); - } - ); - } - }; if (settingRegistry !== null) { settingRegistry .load(managerPlugin.id) @@ -221,7 +178,7 @@ function activateWidgetExtension( console.error(reason.message); }); } - + WidgetManager.loggerRegistry = loggerRegistry; // Add a placeholder widget renderer. rendermime.addFactory( { @@ -242,8 +199,6 @@ function activateWidgetExtension( outputViews(app, panel.context.path) ) ); - - bindUnhandledIOPubMessageSignal(panel); }); tracker.widgetAdded.connect((sender, panel) => { registerWidgetManager( @@ -254,8 +209,6 @@ function activateWidgetExtension( outputViews(app, panel.context.path) ) ); - - bindUnhandledIOPubMessageSignal(panel); }); } @@ -284,7 +237,7 @@ function activateWidgetExtension( return { registerWidget(data: base.IWidgetRegistryData): void { - WIDGET_REGISTRY.push(data); + LabWidgetManager.WIDGET_REGISTRY.push(data); }, }; } @@ -356,17 +309,19 @@ export const controlWidgetsPlugin: JupyterFrontEndPlugin = { */ export const outputWidgetPlugin: JupyterFrontEndPlugin = { id: `@jupyter-widgets/jupyterlab-manager:output-${OUTPUT_WIDGET_VERSION}`, - requires: [base.IJupyterWidgetRegistry], + requires: [base.IJupyterWidgetRegistry, IRenderMimeRegistry], autoStart: true, activate: ( app: JupyterFrontEnd, - registry: base.IJupyterWidgetRegistry + registry: base.IJupyterWidgetRegistry, + rendermime: IRenderMimeRegistry ): void => { - registry.registerWidget({ - name: '@jupyter-widgets/output', - version: OUTPUT_WIDGET_VERSION, - exports: { OutputModel, OutputView }, - }); + (OutputModel.rendermime = rendermime), + registry.registerWidget({ + name: '@jupyter-widgets/output', + version: OUTPUT_WIDGET_VERSION, + exports: { OutputModel, OutputView }, + }); }, }; diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 1e0aa34f37..8b55cce9a2 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -5,13 +5,14 @@ import { PromiseDelegate } from '@lumino/coreutils'; import { IDisposable } from '@lumino/disposable'; -import { Panel, Widget as LuminoWidget } from '@lumino/widgets'; +import { Widget as LuminoWidget, Panel } from '@lumino/widgets'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; -import { LabWidgetManager } from './manager'; import { DOMWidgetModel } from '@jupyter-widgets/base'; +import { LabWidgetManager, findWidgetManager } from './manager'; + /** * A renderer for widgets. */ @@ -36,14 +37,22 @@ export class WidgetRenderer set manager(value: LabWidgetManager) { value.restored.connect(this._rerender, this); this._manager.resolve(value); + this._manager_set = true; } async renderModel(model: IRenderMime.IMimeModel): Promise { const source: any = model.data[this.mimeType]; - // Let's be optimistic, and hope the widget state will come later. this.node.textContent = 'Loading widget...'; - + if (!this._manager_set) { + try { + this.manager = findWidgetManager(source.model_id); + } catch (err) { + this.node.textContent = `widget model not found for ${model.data['text/plain']}`; + console.error(err); + return Promise.resolve(); + } + } const manager = await this._manager.promise; // If there is no model id, the view was removed, so hide the node. if (source.model_id === '') { @@ -61,12 +70,11 @@ export class WidgetRenderer this.node.textContent = 'Error displaying widget: model not found'; this.addClass('jupyter-widgets'); console.error(err); - return; } // Store the model for a possible rerender this._rerenderMimeModel = model; - return; + return Promise.resolve(); } // Successful getting the model, so we don't need to try to rerender. @@ -121,5 +129,6 @@ export class WidgetRenderer */ readonly mimeType: string; private _manager = new PromiseDelegate(); + private _manager_set = false; private _rerenderMimeModel: IRenderMime.IMimeModel | null = null; }