From 880a82c2b6935de0e073c535d4aebeda28c1d1a1 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 27 May 2024 21:37:01 +1000 Subject: [PATCH] Per-kernel widget manager. --- python/jupyterlab_widgets/src/manager.ts | 320 +++++++++++++++++---- python/jupyterlab_widgets/src/output.ts | 31 +- python/jupyterlab_widgets/src/plugin.ts | 326 +++++----------------- python/jupyterlab_widgets/src/renderer.ts | 21 +- 4 files changed, 347 insertions(+), 351 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index fec2cb3e4e..4451a131e6 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,6 +26,12 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; +import { ObservableList, ObservableMap } from '@jupyterlab/observables'; + +import * as nbformat from '@jupyterlab/nbformat'; + +import { ILoggerRegistry, LogLevel } from '@jupyterlab/logconsole'; + import { Kernel, KernelMessage, Session } from '@jupyterlab/services'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -36,6 +42,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,25 +341,40 @@ 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.attachToRendermime(rendermime); + Private.kernelWidgetManagers.set(kernel.id, this); this._kernel = kernel; - - kernel.statusChanged.connect((sender, args) => { - this._handleKernelStatusChange(args); - }); - kernel.connectionStatusChanged.connect((sender, args) => { - this._handleKernelConnectionStatusChange(args); - }); + this.loadCustomWidgetDefinitions(); + LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => + this.loadCustomWidgetDefinitions() + ); + this._kernel.registerCommTarget( + this.comm_target_name, + this._handleCommOpen + ); + + this._kernel.statusChanged.connect(this._handleKernelStatusChange, this); + this._kernel.connectionStatusChanged.connect( + this._handleKernelConnectionStatusChange, + this + ); this._handleKernelChanged({ name: 'kernel', @@ -358,18 +384,29 @@ export class KernelWidgetManager extends LabWidgetManager { this.restoreWidgets(); } - _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { - if (status === 'connected') { - // Only restore if we aren't currently trying to restore from the kernel - // (for example, in our initial restore from the constructor). - if (!this._kernelRestoreInProgress) { - this.restoreWidgets(); - } + _handleKernelConnectionStatusChange( + sender: Kernel.IKernelConnection, + status: Kernel.ConnectionStatus + ): void { + switch (status) { + case 'connected': + // Only restore if we aren't currently trying to restore from the kernel + // (for example, in our initial restore from the constructor). + if (!this._kernelRestoreInProgress) { + this.restoreWidgets(); + } + break; + case 'disconnected': + this.dispose(); } } - _handleKernelStatusChange(status: Kernel.Status): void { + _handleKernelStatusChange( + sender: Kernel.IKernelConnection, + status: Kernel.Status + ): void { if (status === 'restarting') { + this.clear_state(); this.disconnect(); } } @@ -405,56 +442,80 @@ 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( + this._handleKernelChange, + this + ); - context.sessionContext.statusChanged.connect((sender, args) => { - this._handleKernelStatusChange(args); - }); + context.sessionContext.statusChanged.connect( + this._handleStatusChange, + this + ); - context.sessionContext.connectionStatusChanged.connect((sender, args) => { - this._handleKernelConnectionStatusChange(args); - }); + context.sessionContext.connectionStatusChanged.connect( + this._handleConnectionStatusChange, + this + ); - 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(); - } - }); + if (context?.saveState) { + context.saveState.connect((sender, saveState) => { + if (saveState === 'started' && settings.saveState) { + this._saveState(); + } + }); + } } /** * 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,7 +529,48 @@ export class WidgetManager extends LabWidgetManager { } } - _handleKernelConnectionStatusChange(status: Kernel.ConnectionStatus): void { + 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 }); + } + } + + _handleConnectionStatusChange( + sender: any, + status: Kernel.ConnectionStatus + ): void { if (status === 'connected') { // Only restore if we aren't currently trying to restore from the kernel // (for example, in our initial restore from the constructor). @@ -482,10 +584,46 @@ export class WidgetManager extends LabWidgetManager { } } - _handleKernelStatusChange(status: Kernel.Status): void { - if (status === 'restarting') { - this.disconnect(); + _handleKernelChange(sender: any, kernel: any): void { + this.updateWidgetManager(); + this.setDirty(); + } + _handleStatusChange(sender: any, status: Kernel.Status): void { + 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 +638,6 @@ export class WidgetManager extends LabWidgetManager { if (loadKernel) { try { this._kernelRestoreInProgress = true; - await this._loadFromKernel(); } finally { this._kernelRestoreInProgress = false; } @@ -529,11 +666,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 +690,6 @@ export class WidgetManager extends LabWidgetManager { } this._context = null!; - super.dispose(); } /** @@ -562,11 +708,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 +725,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 +739,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 +755,49 @@ export namespace WidgetManager { saveState: boolean; }; } + +/** + * Get the widget manager for the kernel. Calling this will ensure + * widgets work in a kernel (providing the kerenel provides comms). + * 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 KernelWidgetManager'); + } + 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 8dd7d6c5d4..6a044a5fc0 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -5,12 +5,10 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import * as nbformat from '@jupyterlab/nbformat'; - import { - IConsoleTracker, CodeConsole, ConsolePanel, + IConsoleTracker, } from '@jupyterlab/console'; import { @@ -21,15 +19,15 @@ import { } 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'; @@ -37,15 +35,17 @@ import { filter } from '@lumino/algorithm'; import { DisposableDelegate } from '@lumino/disposable'; +import { AttachedProperty } from '@lumino/properties'; + import { WidgetRenderer } from './renderer'; import { - WidgetManager, + LabWidgetManager, WIDGET_VIEW_MIMETYPE, - KernelWidgetManager, + WidgetManager, } 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'; @@ -55,11 +55,7 @@ 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'; -import { ISessionContext } from '@jupyterlab/apputils'; - -const WIDGET_REGISTRY: base.IWidgetRegistryData[] = []; /** * The cached settings. @@ -138,135 +134,20 @@ function* chain( } } -/** - * Get the kernel id of current notebook or console panel, this value - * is used as key for `Private.widgetManagerProperty` to store the widget - * manager of current notebook or console panel. - * - * @param {ISessionContext} sessionContext The session context of notebook or - * console panel. - */ -async function getWidgetManagerOwner( - sessionContext: ISessionContext -): Promise { - await sessionContext.ready; - return sessionContext.session!.kernel!.id; -} - -/** - * Common handler for registering both notebook and console - * `WidgetManager` - * - * @param {(Notebook | CodeConsole)} content Context of panel. - * @param {ISessionContext} sessionContext Session context of panel. - * @param {IRenderMimeRegistry} rendermime Rendermime of panel. - * @param {IterableIterator} renderers Iterator of - * `WidgetRenderer` inside panel - * @param {(() => WidgetManager | KernelWidgetManager)} widgetManagerFactory - * function to create widget manager. - */ -async function registerWidgetHandler( - content: Notebook | CodeConsole, - sessionContext: ISessionContext, - rendermime: IRenderMimeRegistry, - renderers: IterableIterator, - widgetManagerFactory: () => WidgetManager | KernelWidgetManager -): Promise { - const wManagerOwner = await getWidgetManagerOwner(sessionContext); - let wManager = Private.widgetManagerProperty.get(wManagerOwner); - let currentOwner: string; - - if (!wManager) { - wManager = widgetManagerFactory(); - WIDGET_REGISTRY.forEach((data) => wManager!.register(data)); - Private.widgetManagerProperty.set(wManagerOwner, wManager); - currentOwner = wManagerOwner; - content.disposed.connect((_) => { - const currentwManager = Private.widgetManagerProperty.get(currentOwner); - if (currentwManager) { - Private.widgetManagerProperty.delete(currentOwner); - } - }); - - sessionContext.kernelChanged.connect((_, args) => { - const { newValue } = args; - if (newValue) { - const newKernelId = newValue.id; - const oldwManager = Private.widgetManagerProperty.get(currentOwner); - - if (oldwManager) { - Private.widgetManagerProperty.delete(currentOwner); - Private.widgetManagerProperty.set(newKernelId, oldwManager); - } - currentOwner = newKernelId; - } - }); - } - - for (const r of renderers) { - r.manager = wManager; - } - - // 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); - } - wManager!.dispose(); - }); -} - -// Kept for backward compat ipywidgets<=8, but not used here anymore export function registerWidgetManager( context: DocumentRegistry.IContext, rendermime: IRenderMimeRegistry, renderers: IterableIterator ): DisposableDelegate { - let wManager: WidgetManager; - const managerReady = getWidgetManagerOwner(context.sessionContext).then( - (wManagerOwner) => { - const currentManager = Private.widgetManagerProperty.get( - wManagerOwner - ) as WidgetManager; - if (!currentManager) { - wManager = new WidgetManager(context, rendermime, SETTINGS); - WIDGET_REGISTRY.forEach((data) => wManager!.register(data)); - Private.widgetManagerProperty.set(wManagerOwner, wManager); - } else { - wManager = currentManager; - } - - for (const r of renderers) { - r.manager = wManager; - } - - // 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(async () => { - await managerReady; + let wManager = Private.widgetManagerProperty.get(context); + if (!wManager) { + wManager = new WidgetManager(context, rendermime, SETTINGS); + Private.widgetManagerProperty.set(context, wManager); + } + if (wManager.kernel) { + wManager.updateWidgetRenderers(renderers); + } + return new DisposableDelegate(() => { if (rendermime) { rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); } @@ -274,43 +155,27 @@ export function registerWidgetManager( }); } -export async function registerNotebookWidgetManager( - panel: NotebookPanel, - renderers: IterableIterator -): Promise { - const content = panel.content; - const context = panel.context; - const sessionContext = context.sessionContext; - const rendermime = content.rendermime; - const widgetManagerFactory = () => - new WidgetManager(context, rendermime, SETTINGS); - - return registerWidgetHandler( - content, - sessionContext, - rendermime, - renderers, - widgetManagerFactory - ); -} - -export async function registerConsoleWidgetManager( - panel: ConsolePanel, - renderers: IterableIterator -): Promise { - const content = panel.console; - const sessionContext = content.sessionContext; - const rendermime = content.rendermime; - const widgetManagerFactory = () => - new KernelWidgetManager(sessionContext.session!.kernel!, rendermime); - - return registerWidgetHandler( - content, - sessionContext, - rendermime, - renderers, - widgetManagerFactory - ); +function attachWidgetManagerToPanel( + panel: NotebookPanel | ConsolePanel, + app: JupyterFrontEnd +) { + if (panel instanceof NotebookPanel) { + registerWidgetManager( + panel.context, + panel.content.rendermime, + chain( + notebookWidgetRenderers(panel.content), + outputViews(app, panel.context.path) + ) + ); + } else if (panel instanceof ConsolePanel) { + // A bit of a hack to make this a 'context' + registerWidgetManager( + panel.console as any, + panel.console.rendermime, + chain(consoleWidgetRenderers(panel.console)) + ); + } } /** @@ -343,7 +208,7 @@ function updateSettings(settings: ISettingRegistry.ISettings): void { function activateWidgetExtension( app: JupyterFrontEnd, rendermime: IRenderMimeRegistry, - tracker: INotebookTracker | null, + widgetTracker: INotebookTracker | null, consoleTracker: IConsoleTracker | null, settingRegistry: ISettingRegistry | null, menu: IMainMenu | null, @@ -353,41 +218,6 @@ function activateWidgetExtension( const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab_widgets'); - const bindUnhandledIOPubMessageSignal = async ( - nb: NotebookPanel - ): Promise => { - if (!loggerRegistry) { - return; - } - const wManagerOwner = await getWidgetManagerOwner( - nb.context.sessionContext - ); - const wManager = Private.widgetManagerProperty.get(wManagerOwner); - - if (wManager) { - wManager.onUnhandledIOPubMessage.connect( - ( - sender: WidgetManager | KernelWidgetManager, - 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) @@ -399,7 +229,7 @@ function activateWidgetExtension( console.error(reason.message); }); } - + WidgetManager.loggerRegistry = loggerRegistry; // Add a placeholder widget renderer. rendermime.addFactory( { @@ -409,33 +239,13 @@ function activateWidgetExtension( }, -10 ); - - if (tracker !== null) { - const rendererIterator = (panel: NotebookPanel) => - chain( - notebookWidgetRenderers(panel.content), - outputViews(app, panel.context.path) + for (const tracker of [widgetTracker, consoleTracker]) { + if (tracker !== null) { + tracker.forEach((panel) => attachWidgetManagerToPanel(panel, app)); + tracker.widgetAdded.connect((sender, panel) => + attachWidgetManagerToPanel(panel, app) ); - tracker.forEach(async (panel) => { - await registerNotebookWidgetManager(panel, rendererIterator(panel)); - bindUnhandledIOPubMessageSignal(panel); - }); - tracker.widgetAdded.connect(async (sender, panel) => { - await registerNotebookWidgetManager(panel, rendererIterator(panel)); - bindUnhandledIOPubMessageSignal(panel); - }); - } - - if (consoleTracker !== null) { - const rendererIterator = (panel: ConsolePanel) => - chain(consoleWidgetRenderers(panel.console)); - - consoleTracker.forEach(async (panel) => { - await registerConsoleWidgetManager(panel, rendererIterator(panel)); - }); - consoleTracker.widgetAdded.connect(async (sender, panel) => { - await registerConsoleWidgetManager(panel, rendererIterator(panel)); - }); + } } if (settingRegistry !== null) { // Add a command for automatically saving (jupyter-)widget state. @@ -462,7 +272,7 @@ function activateWidgetExtension( return { registerWidget(data: base.IWidgetRegistryData): void { - WIDGET_REGISTRY.push(data); + LabWidgetManager.WIDGET_REGISTRY.push(data); }, }; } @@ -534,17 +344,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 }, + }); }, }; @@ -556,23 +368,13 @@ export default [ ]; namespace Private { /** - * A type alias for keys of `widgetManagerProperty` . - */ - export type IWidgetManagerOwner = string; - - /** - * A type alias for values of `widgetManagerProperty` . - */ - export type IWidgetManagerValue = - | WidgetManager - | KernelWidgetManager - | undefined; - - /** - * A private map for a widget manager. + * A private attached property for a widget manager. */ - export const widgetManagerProperty = new Map< - IWidgetManagerOwner, - IWidgetManagerValue - >(); + export const widgetManagerProperty = new AttachedProperty< + DocumentRegistry.Context, + WidgetManager | undefined + >({ + name: 'widgetManager', + create: (owner: DocumentRegistry.Context): undefined => undefined, + }); } 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; }