From 07ce11b631142f81ea19bb93b82a80220bef0331 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 27 May 2024 21:37:01 +1000 Subject: [PATCH 01/22] 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; } From 5bb3c5260c3f458297ac5a09db41c7d416ff494c Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Wed, 29 May 2024 19:37:49 +1000 Subject: [PATCH 02/22] Add attachToRendermime function. --- python/jupyterlab_widgets/src/manager.ts | 83 ++++++++++++------------ python/jupyterlab_widgets/src/plugin.ts | 4 +- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 4451a131e6..a84c98c3b6 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -326,6 +326,8 @@ export abstract class LabWidgetManager await this.handle_comm_open(oldComm, msg); }; + static globalRendermime: IRenderMimeRegistry; + protected _restored = new Signal(this); protected _restoredStatus = false; protected _kernelRestoreInProgress = false; @@ -346,20 +348,24 @@ export abstract class LabWidgetManager /** * A singleton widget manager per kernel for the lifecycle of the kernel. + * If a rendermime isn't provided the global singleton will be used. */ export class KernelWidgetManager extends LabWidgetManager { constructor( kernel: Kernel.IKernelConnection, - rendermime: IRenderMimeRegistry + rendermime: IRenderMimeRegistry | null ) { + if (!rendermime) { + rendermime = LabWidgetManager.globalRendermime; + } const instance = Private.kernelWidgetManagers.get(kernel.id); if (instance) { - instance.attachToRendermime(rendermime); + attachToRendermime(rendermime, instance); return instance; } super(rendermime); - this.attachToRendermime(rendermime); Private.kernelWidgetManagers.set(kernel.id, this); + attachToRendermime(rendermime, this); this._kernel = kernel; this.loadCustomWidgetDefinitions(); LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => @@ -433,7 +439,8 @@ export class KernelWidgetManager extends LabWidgetManager { if (this.isDisposed) { return; } - + attachToRendermime(this.rendermime); + Private.kernelWidgetManagers.delete(this.kernel.id); this._kernel = null!; super.dispose(); } @@ -452,18 +459,6 @@ export class KernelWidgetManager extends LabWidgetManager { 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; } @@ -537,7 +532,10 @@ export class WidgetManager extends Backbone.Model implements IDisposable { ); } if (this.kernel) { - this._widgetManager = getWidgetManager(this.kernel, this.rendermime); + this._widgetManager = new KernelWidgetManager( + this.kernel, + this.rendermime + ); this._widgetManager.onUnhandledIOPubMessage.connect( this.onUnhandledIOPubMessage, this @@ -756,31 +754,6 @@ export namespace WidgetManager { }; } -/** - * 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 @@ -795,6 +768,32 @@ export function findWidgetManager(model_id: string): KernelWidgetManager { throw new Error(`A widget manager was not found for model_id ${model_id}'`); } +/** + * Will define wManager as a renderer for rendermime if rendermime + * is not the global rendermime or there is only one wManager. + * If wManager is not provided, it will make the rendermine more general. + */ +function attachToRendermime( + rendermime: IRenderMimeRegistry, + wManager?: KernelWidgetManager +) { + const wManager_ = + rendermime === LabWidgetManager.globalRendermime && + Private.kernelWidgetManagers.size > 1 + ? undefined + : wManager; + + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options) => new WidgetRenderer(options, wManager_), + }, + -10 + ); +} + /** * A namespace for private data */ diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index 6a044a5fc0..ddffb3c498 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -148,9 +148,6 @@ export function registerWidgetManager( wManager.updateWidgetRenderers(renderers); } return new DisposableDelegate(() => { - if (rendermime) { - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - } wManager!.dispose(); }); } @@ -230,6 +227,7 @@ function activateWidgetExtension( }); } WidgetManager.loggerRegistry = loggerRegistry; + LabWidgetManager.globalRendermime = rendermime; // Add a placeholder widget renderer. rendermime.addFactory( { From 9452054cf7123da7e1044d2bc831f16c5775440c Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Wed, 29 May 2024 21:48:11 +1000 Subject: [PATCH 03/22] Update lock file. --- yarn.lock | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3cf819b206..dce4d8e913 100644 --- a/yarn.lock +++ b/yarn.lock @@ -732,11 +732,11 @@ __metadata: languageName: node linkType: hard -"@jupyter-widgets/base-manager@^1.0.8, @jupyter-widgets/base-manager@workspace:packages/base-manager": +"@jupyter-widgets/base-manager@^1.0.9, @jupyter-widgets/base-manager@workspace:packages/base-manager": version: 0.0.0-use.local resolution: "@jupyter-widgets/base-manager@workspace:packages/base-manager" dependencies: - "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base": ^6.0.8 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/coreutils": ^1.11.1 || ^2 "@types/base64-js": ^1.2.5 @@ -771,7 +771,7 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/base@^6.0.7, @jupyter-widgets/base@workspace:packages/base": +"@jupyter-widgets/base@^6.0.8, @jupyter-widgets/base@workspace:packages/base": version: 0.0.0-use.local resolution: "@jupyter-widgets/base@workspace:packages/base" dependencies: @@ -814,11 +814,11 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/controls@^5.0.8, @jupyter-widgets/controls@workspace:packages/controls": +"@jupyter-widgets/controls@^5.0.9, @jupyter-widgets/controls@workspace:packages/controls": version: 0.0.0-use.local resolution: "@jupyter-widgets/controls@workspace:packages/controls" dependencies: - "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base": ^6.0.8 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/algorithm": ^1.9.1 || ^2.1 "@lumino/domutils": ^1.8.1 || ^2.1 @@ -869,9 +869,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web1@workspace:examples/web1" dependencies: - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/base-manager": ^1.0.8 - "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/base-manager": ^1.0.9 + "@jupyter-widgets/controls": ^5.0.9 chai: ^4.0.0 css-loader: ^6.5.1 karma: ^6.3.3 @@ -890,9 +890,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web2@workspace:examples/web2" dependencies: - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/base-manager": ^1.0.8 - "@jupyter-widgets/controls": ^5.0.8 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/base-manager": ^1.0.9 + "@jupyter-widgets/controls": ^5.0.9 codemirror: ^5.48.0 css-loader: ^6.5.1 font-awesome: ^4.7.0 @@ -905,9 +905,9 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web3@workspace:examples/web3" dependencies: - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/controls": ^5.0.8 - "@jupyter-widgets/html-manager": ^1.0.10 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/controls": ^5.0.9 + "@jupyter-widgets/html-manager": ^1.0.11 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@types/codemirror": ^5.60.0 "@types/node": ^17.0.2 @@ -927,7 +927,7 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/example-web4@workspace:examples/web4" dependencies: - "@jupyter-widgets/html-manager": ^1.0.10 + "@jupyter-widgets/html-manager": ^1.0.11 css-loader: ^6.5.1 font-awesome: ^4.7.0 style-loader: ^3.3.1 @@ -935,16 +935,16 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/html-manager@^1.0.10, @jupyter-widgets/html-manager@workspace:packages/html-manager": +"@jupyter-widgets/html-manager@^1.0.11, @jupyter-widgets/html-manager@workspace:packages/html-manager": version: 0.0.0-use.local resolution: "@jupyter-widgets/html-manager@workspace:packages/html-manager" dependencies: "@fortawesome/fontawesome-free": ^5.12.0 - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/base-manager": ^1.0.8 - "@jupyter-widgets/controls": ^5.0.8 - "@jupyter-widgets/output": ^6.0.7 - "@jupyter-widgets/schema": ^0.5.4 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/base-manager": ^1.0.9 + "@jupyter-widgets/controls": ^5.0.9 + "@jupyter-widgets/output": ^6.0.8 + "@jupyter-widgets/schema": ^0.5.5 "@jupyterlab/outputarea": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime-interfaces": ^3.0.0 || ^4.0.0 @@ -981,10 +981,10 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/jupyterlab-manager@workspace:python/jupyterlab_widgets" dependencies: - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/base-manager": ^1.0.8 - "@jupyter-widgets/controls": ^5.0.8 - "@jupyter-widgets/output": ^6.0.7 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/base-manager": ^1.0.9 + "@jupyter-widgets/controls": ^5.0.9 + "@jupyter-widgets/output": ^6.0.8 "@jupyterlab/application": ^3.0.0 || ^4.0.0 "@jupyterlab/apputils": ^3.0.0 || ^4.0.0 "@jupyterlab/builder": ^3.0.0 || ^4.0.0 @@ -1028,11 +1028,11 @@ __metadata: version: 0.0.0-use.local resolution: "@jupyter-widgets/notebook-manager@workspace:python/widgetsnbextension" dependencies: - "@jupyter-widgets/base": ^6.0.7 - "@jupyter-widgets/base-manager": ^1.0.8 - "@jupyter-widgets/controls": ^5.0.8 - "@jupyter-widgets/html-manager": ^1.0.10 - "@jupyter-widgets/output": ^6.0.7 + "@jupyter-widgets/base": ^6.0.8 + "@jupyter-widgets/base-manager": ^1.0.9 + "@jupyter-widgets/controls": ^5.0.9 + "@jupyter-widgets/html-manager": ^1.0.11 + "@jupyter-widgets/output": ^6.0.8 "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@lumino/messaging": ^1.10.1 || ^2.1 "@lumino/widgets": ^1.30.0 || ^2.1 @@ -1046,17 +1046,17 @@ __metadata: languageName: unknown linkType: soft -"@jupyter-widgets/output@^6.0.7, @jupyter-widgets/output@workspace:packages/output": +"@jupyter-widgets/output@^6.0.8, @jupyter-widgets/output@workspace:packages/output": version: 0.0.0-use.local resolution: "@jupyter-widgets/output@workspace:packages/output" dependencies: - "@jupyter-widgets/base": ^6.0.7 + "@jupyter-widgets/base": ^6.0.8 rimraf: ^3.0.2 typescript: ~4.9.4 languageName: unknown linkType: soft -"@jupyter-widgets/schema@^0.5.4, @jupyter-widgets/schema@workspace:packages/schema": +"@jupyter-widgets/schema@^0.5.5, @jupyter-widgets/schema@workspace:packages/schema": version: 0.0.0-use.local resolution: "@jupyter-widgets/schema@workspace:packages/schema" languageName: unknown From 90e010f494aa97a522200621db6cd3523d086d6b Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 31 May 2024 18:33:00 +1000 Subject: [PATCH 04/22] WidgetModel: set comm_live to false when comm closed and remove redundant comm_closed argument. --- packages/base/src/widget.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index aea060fc8d..936becf6a4 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -214,17 +214,15 @@ export class WidgetModel extends Backbone.Model { /** * Close model * - * @param comm_closed - true if the comm is already being closed. If false, the comm will be closed. - * * @returns - a promise that is fulfilled when all the associated views have been removed. */ - close(comm_closed = false): Promise { + close(): Promise { // can only be closed once. if (this._closed) { return Promise.resolve(); } this._closed = true; - if (this.comm && !comm_closed) { + if (this.comm && this.comm_live) { this.comm.close(); } this.stopListening(); @@ -249,8 +247,11 @@ export class WidgetModel extends Backbone.Model { * Handle when a widget comm is closed. */ _handle_comm_closed(msg: KernelMessage.ICommCloseMsg): void { + this.comm_live = false; + if (this._closed) { + this.close(); + } this.trigger('comm:close'); - this.close(true); } /** From 7715db955d5fe02bfafd8be29e93a5f2f8f68ae4 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 31 May 2024 19:15:37 +1000 Subject: [PATCH 05/22] Improve ManagerBase_loadFromKernel --- packages/base-manager/src/manager-base.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 4953767894..fe569a5213 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -383,6 +383,7 @@ export abstract class ManagerBase implements IWidgetManager { // Try fetching all widget states through the control comm let data: any; let buffers: any; + let timeoutID: number | undefined; try { const initComm = await this._create_comm( CONTROL_COMM_TARGET, @@ -390,8 +391,8 @@ export abstract class ManagerBase implements IWidgetManager { {}, { version: CONTROL_COMM_PROTOCOL_VERSION } ); - await new Promise((resolve, reject) => { + let succeeded = false; initComm.on_msg((msg: any) => { data = msg['content']['data']; @@ -409,28 +410,31 @@ export abstract class ManagerBase implements IWidgetManager { return new DataView(b instanceof ArrayBuffer ? b : b.buffer); } }); - + succeeded = true; + clearTimeout(timeoutID); resolve(null); }); - initComm.on_close(() => reject('Control comm was closed too early')); + initComm.on_close(() => { + if (!succeeded) reject('Control comm was closed too early'); + }); // Send a states request msg initComm.send({ method: 'request_states' }, {}); // Reject if we didn't get a response in time - setTimeout( + timeoutID = window.setTimeout( () => reject('Control comm did not respond in time'), CONTROL_COMM_TIMEOUT ); }); - initComm.close(); } catch (error) { console.warn( 'Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel, fallback to fetching individual model state. Reason:', error ); + clearTimeout(timeoutID); // Fall back to the old implementation for old ipywidgets backend versions (ipywidgets<=7.6) return this._loadFromKernelModels(); } From f7c2dcf72d896d5ad92692409b9706067c9c7da1 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 31 May 2024 19:17:15 +1000 Subject: [PATCH 06/22] Update jupyterlab_widgets package.json --- python/jupyterlab_widgets/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/jupyterlab_widgets/package.json b/python/jupyterlab_widgets/package.json index 71fafd5410..1711edbb5f 100644 --- a/python/jupyterlab_widgets/package.json +++ b/python/jupyterlab_widgets/package.json @@ -51,13 +51,13 @@ "@jupyter-widgets/controls": "^5.0.9", "@jupyter-widgets/output": "^6.0.8", "@jupyterlab/application": "^3.0.0 || ^4.0.0", - "@jupyterlab/apputils": "^3.0.0 || ^4.0.0", "@jupyterlab/console": "^3.0.0 || ^4.0.0", "@jupyterlab/docregistry": "^3.0.0 || ^4.0.0", "@jupyterlab/logconsole": "^3.0.0 || ^4.0.0", "@jupyterlab/mainmenu": "^3.0.0 || ^4.0.0", "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0", "@jupyterlab/notebook": "^3.0.0 || ^4.0.0", + "@jupyterlab/observables": "^5.2.1", "@jupyterlab/outputarea": "^3.0.0 || ^4.0.0", "@jupyterlab/rendermime": "^3.0.0 || ^4.0.0", "@jupyterlab/rendermime-interfaces": "^3.0.0 || ^4.0.0", @@ -67,6 +67,7 @@ "@lumino/algorithm": "^1.11.1 || ^2.0.0", "@lumino/coreutils": "^1.11.1 || ^2.1", "@lumino/disposable": "^1.10.1 || ^2.1", + "@lumino/properties": "^2.0.1", "@lumino/signaling": "^1.10.1 || ^2.1", "@lumino/widgets": "^1.30.0 || ^2.1", "@types/backbone": "1.4.14", From 9cd1b47623c3ffc4f33496a2da2e6a46a1338366 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Fri, 31 May 2024 23:19:11 +1000 Subject: [PATCH 07/22] Improved restoring widgets when opening and closing of notebooks. --- python/jupyterlab_widgets/src/manager.ts | 289 ++++++++++++---------- python/jupyterlab_widgets/src/plugin.ts | 5 +- python/jupyterlab_widgets/src/renderer.ts | 49 ++-- yarn.lock | 5 +- 4 files changed, 197 insertions(+), 151 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index a84c98c3b6..ddcbc46a2f 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -26,7 +26,7 @@ import { INotebookModel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { ObservableList, ObservableMap } from '@jupyterlab/observables'; +import { ObservableList } from '@jupyterlab/observables'; import * as nbformat from '@jupyterlab/nbformat'; @@ -116,7 +116,6 @@ export abstract class LabWidgetManager // A "load" for a kernel that does not handle comms does nothing. return; } - return super._loadFromKernel(); } @@ -177,6 +176,7 @@ export abstract class LabWidgetManager return; } this._isDisposed = true; + this._rendermime = null!; if (this._commRegistration) { this._commRegistration.dispose(); @@ -348,7 +348,8 @@ export abstract class LabWidgetManager /** * A singleton widget manager per kernel for the lifecycle of the kernel. - * If a rendermime isn't provided the global singleton will be used. + * The factory of the rendermime will be update to use the widgetManager + * directly if it isn't the globalRendermime. */ export class KernelWidgetManager extends LabWidgetManager { constructor( @@ -360,34 +361,84 @@ export class KernelWidgetManager extends LabWidgetManager { } const instance = Private.kernelWidgetManagers.get(kernel.id); if (instance) { - attachToRendermime(rendermime, instance); + KernelWidgetManager.updateManagerKernel(instance, kernel); + KernelWidgetManager.attachToRendermime(rendermime, instance); return instance; } - super(rendermime); + if (!kernel.handleComms) { + throw new Error('Kernel does not have handleComms enabled'); + } + super(LabWidgetManager.globalRendermime); Private.kernelWidgetManagers.set(kernel.id, this); - attachToRendermime(rendermime, this); - this._kernel = kernel; 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 - ); + KernelWidgetManager.updateManagerKernel(this, kernel); + KernelWidgetManager.attachToRendermime(rendermime, this); + } - this._handleKernelChanged({ + static updateManagerKernel( + manager: KernelWidgetManager, + kernel: Kernel.IKernelConnection + ) { + if (!kernel.handleComms) { + return; + } + manager._handleKernelChanged({ name: 'kernel', - oldValue: null, + oldValue: manager._kernel, newValue: kernel, }); - this.restoreWidgets(); + if (manager._kernel) { + manager._kernel.statusChanged.disconnect( + manager._handleKernelStatusChange, + manager + ); + manager._kernel.connectionStatusChanged.disconnect( + manager._handleKernelConnectionStatusChange, + manager + ); + } + manager._kernel = kernel; + manager._kernel.statusChanged.connect( + manager._handleKernelStatusChange, + manager + ); + manager._kernel.connectionStatusChanged.connect( + manager._handleKernelConnectionStatusChange, + manager + ); + manager._restoredStatus = false; + manager._kernelRestoreInProgress = true; + manager.clear_state().then(() => manager.restoreWidgets()); + } + + /** + * Will define wManager as a renderer for rendermime if rendermime + * is not the global rendermime or there is only one wManager. + * If wManager is not provided, it will make the rendermine more general. + */ + static attachToRendermime( + rendermime: IRenderMimeRegistry, + wManager?: KernelWidgetManager + ) { + const wManager_ = + rendermime === LabWidgetManager.globalRendermime && + Private.kernelWidgetManagers.size > 1 + ? undefined + : wManager; + const pendingManagerMessage = wManager ? 'Loading widget ...' : ''; + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options) => + new WidgetRenderer(options, wManager_, pendingManagerMessage), + }, + -10 + ); } _handleKernelConnectionStatusChange( @@ -403,7 +454,8 @@ export class KernelWidgetManager extends LabWidgetManager { } break; case 'disconnected': - this.dispose(); + this.disconnect(); + break; } } @@ -411,25 +463,24 @@ export class KernelWidgetManager extends LabWidgetManager { sender: Kernel.IKernelConnection, status: Kernel.Status ): void { - if (status === 'restarting') { - this.clear_state(); - this.disconnect(); + switch (status) { + case 'restarting': + case 'dead': + this.disconnect(); + break; } } - /** - * Restore widgets from kernel and saved state. + * Restore widgets from kernel. */ async restoreWidgets(): Promise { try { - this._kernelRestoreInProgress = true; await this._loadFromKernel(); - this._restoredStatus = true; - this._restored.emit(); } catch (err) { // Do nothing } - this._kernelRestoreInProgress = false; + this._restoredStatus = true; + this._restored.emit(); } /** @@ -439,10 +490,16 @@ export class KernelWidgetManager extends LabWidgetManager { if (this.isDisposed) { return; } - attachToRendermime(this.rendermime); + super.dispose(); + KernelWidgetManager.attachToRendermime(this.rendermime); Private.kernelWidgetManagers.delete(this.kernel.id); + this._handleKernelChanged({ + name: 'kernel', + oldValue: this._kernel, + newValue: null, + }); this._kernel = null!; - super.dispose(); + this.clear_state(); } get kernel(): Kernel.IKernelConnection { @@ -471,12 +528,14 @@ export class WidgetManager extends Backbone.Model implements IDisposable { constructor( context: DocumentRegistry.IContext, rendermime: IRenderMimeRegistry, - settings: WidgetManager.Settings + settings: WidgetManager.Settings, + renderers?: IterableIterator ) { super(); this._rendermime = rendermime; this._context = context; this._settings = settings; + this._renderers = renderers; context.sessionContext.kernelChanged.connect( this._handleKernelChange, @@ -492,11 +551,6 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this._handleConnectionStatusChange, this ); - - this.updateWidgetManager(); - this.setDirty(); - - this.restoreWidgets(this._context!.model); if (context?.saveState) { context.saveState.connect((sender, saveState) => { if (saveState === 'started' && settings.saveState) { @@ -504,12 +558,30 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } }); } + if (rendermime !== LabWidgetManager.globalRendermime) { + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options) => + new WidgetRenderer(options, undefined, 'Waiting for kernel'), + }, + -10 + ); + } + if (this.kernel) { + this.updateWidgetManager(); + } } /** * Save the widget state to the context model. */ private _saveState(): void { + if (!this.widgetManager) { + return; + } const state = this.widgetManager.get_state_sync({ drop_defaults: true }); if (this._context.model.setMetadata) { this._context.model.setMetadata('widgets', { @@ -524,22 +596,37 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } } - updateWidgetManager() { + async updateWidgetManager() { if (this._widgetManager) { - this.widgetManager.onUnhandledIOPubMessage.disconnect( + this._widgetManager.onUnhandledIOPubMessage.disconnect( this.onUnhandledIOPubMessage, this ); } if (this.kernel) { - this._widgetManager = new KernelWidgetManager( - this.kernel, - this.rendermime - ); + await this.context.sessionContext.ready; + this._widgetManager = new KernelWidgetManager(this.kernel, null); this._widgetManager.onUnhandledIOPubMessage.connect( this.onUnhandledIOPubMessage, this ); + if (!this._widgetManager.restoredStatus) { + await new Promise((resolve) => { + this._widgetManager?.restored.connect(resolve); + }); + if (!this.restored) { + this.restoreWidgets(this._context!.model); + } + } + KernelWidgetManager.attachToRendermime( + this.rendermime, + this._widgetManager + ); + if (this._renderers) { + for (const r of this._renderers) { + r.manager = this._widgetManager; + } + } } } @@ -569,17 +656,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { 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). - if (!this._kernelRestoreInProgress) { - // We only want to restore widgets from the kernel, not ones saved in the notebook. - this.restoreWidgets(this._context!.model, { - loadKernel: true, - loadNotebook: false, - }); - } - } + null; } _handleKernelChange(sender: any, kernel: any): void { @@ -590,7 +667,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this.setDirty(); } - get widgetManager(): KernelWidgetManager { + get widgetManager(): KernelWidgetManager | null { return this._widgetManager; } @@ -612,34 +689,13 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } /** - * - * @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? - } - - /** - * Restore widgets from kernel and saved state. + * Restore widgets from notebook and saved state. */ async restoreWidgets( notebook: INotebookModel, - { loadKernel, loadNotebook } = { loadKernel: true, loadNotebook: true } + { loadNotebook } = { loadNotebook: true } ): Promise { try { - await this.context.sessionContext.ready; - if (loadKernel) { - try { - this._kernelRestoreInProgress = true; - } finally { - this._kernelRestoreInProgress = false; - } - } if (loadNotebook) { await this._loadFromNotebook(notebook); } @@ -656,15 +712,32 @@ export class WidgetManager extends Backbone.Model implements IDisposable { * Load widget state from notebook metadata */ async _loadFromNotebook(notebook: INotebookModel): Promise { + if (!this.widgetManager) { + return; + } const widget_md = notebook.getMetadata ? (notebook.getMetadata('widgets') as any) : // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore JupyterLab 3 support notebook.metadata.get('widgets'); - // 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.widgetManager.filterModelState(state); + + // Ensure the kernel has been restored. + let timeoutID; + const restored = this.widgetManager.restored; + if (!this.widgetManager.restoredStatus) { + await new Promise((resolve, reject) => { + restored.connect(resolve); + timeoutID = window.setTimeout( + () => reject('Timeout waiting for '), + 4000 + ); + }); + } + clearTimeout(timeoutID); + // Restore any widgets from saved state that are not live await this.widgetManager.set_state(state); } } @@ -686,8 +759,11 @@ export class WidgetManager extends Backbone.Model implements IDisposable { if (this.isDisposed) { return; } - this._context = null!; + this._renderers = undefined; + this._context = null!; + this._rendermime = null!; + this._settings = null!; } /** @@ -710,23 +786,6 @@ export class WidgetManager extends Backbone.Model implements IDisposable { return this._rendermime; } - /** - * Register a widget model. - */ - register_model(model_id: string, modelPromise: Promise): void { - this.widgetManager.register_model(model_id, modelPromise); - this.setDirty(); - } - - /** - * Close all widgets and empty the widget state. - * @return Promise that resolves when the widget state is cleared. - */ - async clear_state(): Promise { - // await this.widgetManager.clear_state(); - this.setDirty(); - } - /** * Set the dirty state of the notebook model if applicable. * @@ -744,8 +803,8 @@ export class WidgetManager extends Backbone.Model implements IDisposable { private _context: DocumentRegistry.IContext; private _rendermime: IRenderMimeRegistry; private _settings: WidgetManager.Settings; - private _widgetManager: KernelWidgetManager; - protected _kernelRestoreInProgress = false; + private _widgetManager: KernelWidgetManager | null; + private _renderers: IterableIterator | undefined; } export namespace WidgetManager { @@ -765,38 +824,12 @@ export function findWidgetManager(model_id: string): KernelWidgetManager { return wManager; } } - throw new Error(`A widget manager was not found for model_id ${model_id}'`); -} - -/** - * Will define wManager as a renderer for rendermime if rendermime - * is not the global rendermime or there is only one wManager. - * If wManager is not provided, it will make the rendermine more general. - */ -function attachToRendermime( - rendermime: IRenderMimeRegistry, - wManager?: KernelWidgetManager -) { - const wManager_ = - rendermime === LabWidgetManager.globalRendermime && - Private.kernelWidgetManagers.size > 1 - ? undefined - : wManager; - - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options) => new WidgetRenderer(options, wManager_), - }, - -10 - ); + 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(); + export const kernelWidgetManagers = new Map(); } diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index ddffb3c498..8a6e9ffc6e 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -141,12 +141,9 @@ export function registerWidgetManager( ): DisposableDelegate { let wManager = Private.widgetManagerProperty.get(context); if (!wManager) { - wManager = new WidgetManager(context, rendermime, SETTINGS); + wManager = new WidgetManager(context, rendermime, SETTINGS, renderers); Private.widgetManagerProperty.set(context, wManager); } - if (wManager.kernel) { - wManager.updateWidgetRenderers(renderers); - } return new DisposableDelegate(() => { wManager!.dispose(); }); diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 8b55cce9a2..7549067a4a 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -15,6 +15,19 @@ import { LabWidgetManager, findWidgetManager } from './manager'; /** * A renderer for widgets. + * + * Default behavior is to search for the manager, unless the manager + * has already been set. + * + * If `pendingManagerMessage` is a non-empty string, no attempt will + * be made to search for the wiget manager, and the manager must be + * waiting for a manager to be set. + * + * pendingManagerMessage: A message to post when rendering whilst + * awaiting the when manager. + * + * Omitting a message means a manager will be searched for. + * */ export class WidgetRenderer extends Panel @@ -22,10 +35,12 @@ export class WidgetRenderer { constructor( options: IRenderMime.IRendererOptions, - manager?: LabWidgetManager + manager?: LabWidgetManager, + pendingManagerMessage = '' ) { super(); this.mimeType = options.mimeType; + this._pendingManagerMessage = pendingManagerMessage; if (manager) { this.manager = manager; } @@ -37,35 +52,35 @@ export class WidgetRenderer set manager(value: LabWidgetManager) { value.restored.connect(this._rerender, this); this._manager.resolve(value); - this._manager_set = true; + this._managerIsSet = true; } async renderModel(model: IRenderMime.IMimeModel): Promise { const source: any = model.data[this.mimeType]; - 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 === '') { this.hide(); return Promise.resolve(); } - + let manager; + if (!this._pendingManagerMessage && !this._managerIsSet) { + manager = findWidgetManager(source.model_id); + } + this.node.textContent = `${ + this._pendingManagerMessage || model.data['text/plain'] + }`; + if (!manager) { + manager = await this._manager.promise; + } let wModel: DOMWidgetModel; try { // Presume we have a DOMWidgetModel. Should we check for sure? wModel = (await manager.get_model(source.model_id)) as DOMWidgetModel; } catch (err) { - if (manager.restoredStatus) { + if (!manager.restoredStatus) { + this._rerenderMimeModel = model; + } else { // The manager has been restored, so this error won't be going away. this.node.textContent = 'Error displaying widget: model not found'; this.addClass('jupyter-widgets'); @@ -73,7 +88,6 @@ export class WidgetRenderer } // Store the model for a possible rerender - this._rerenderMimeModel = model; return Promise.resolve(); } @@ -129,6 +143,7 @@ export class WidgetRenderer */ readonly mimeType: string; private _manager = new PromiseDelegate(); - private _manager_set = false; + private _managerIsSet = false; + private _pendingManagerMessage: string; private _rerenderMimeModel: IRenderMime.IMimeModel | null = null; } diff --git a/yarn.lock b/yarn.lock index dce4d8e913..0558c955e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -986,7 +986,6 @@ __metadata: "@jupyter-widgets/controls": ^5.0.9 "@jupyter-widgets/output": ^6.0.8 "@jupyterlab/application": ^3.0.0 || ^4.0.0 - "@jupyterlab/apputils": ^3.0.0 || ^4.0.0 "@jupyterlab/builder": ^3.0.0 || ^4.0.0 "@jupyterlab/cells": ^3.0.0 || ^4.0.0 "@jupyterlab/console": ^3.0.0 || ^4.0.0 @@ -995,6 +994,7 @@ __metadata: "@jupyterlab/mainmenu": ^3.0.0 || ^4.0.0 "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0 "@jupyterlab/notebook": ^3.0.0 || ^4.0.0 + "@jupyterlab/observables": ^5.2.1 "@jupyterlab/outputarea": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime": ^3.0.0 || ^4.0.0 "@jupyterlab/rendermime-interfaces": ^3.0.0 || ^4.0.0 @@ -1004,6 +1004,7 @@ __metadata: "@lumino/algorithm": ^1.11.1 || ^2.0.0 "@lumino/coreutils": ^1.11.1 || ^2.1 "@lumino/disposable": ^1.10.1 || ^2.1 + "@lumino/properties": ^2.0.1 "@lumino/signaling": ^1.10.1 || ^2.1 "@lumino/widgets": ^1.30.0 || ^2.1 "@types/backbone": 1.4.14 @@ -1127,7 +1128,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/apputils@npm:^3.0.0 || ^4.0.0, @jupyterlab/apputils@npm:^4.3.1": +"@jupyterlab/apputils@npm:^4.3.1": version: 4.3.1 resolution: "@jupyterlab/apputils@npm:4.3.1" dependencies: From fc3967ce5a0bb31a4b57b854cdc7bb9ec3023422 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 1 Jun 2024 14:07:12 +1000 Subject: [PATCH 08/22] manager and render code refactoring. --- python/jupyterlab_widgets/src/manager.ts | 127 ++++++++++++---------- python/jupyterlab_widgets/src/plugin.ts | 2 +- python/jupyterlab_widgets/src/renderer.ts | 9 +- 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index ddcbc46a2f..f753a2e77d 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -20,6 +20,8 @@ import { import { IDisposable } from '@lumino/disposable'; +import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; + import { ReadonlyPartialJSONValue } from '@lumino/coreutils'; import { INotebookModel } from '@jupyterlab/notebook'; @@ -44,9 +46,10 @@ import { SemVerCache } from './semvercache'; import Backbone from 'backbone'; -import * as base from '@jupyter-widgets/base'; import { WidgetRenderer } from './renderer'; +import * as base from '@jupyter-widgets/base'; + /** * The mime type for a widget view. */ @@ -354,15 +357,20 @@ export abstract class LabWidgetManager export class KernelWidgetManager extends LabWidgetManager { constructor( kernel: Kernel.IKernelConnection, - rendermime: IRenderMimeRegistry | null + rendermime: IRenderMimeRegistry | null, + pendingManagerMessage = 'Loading widget ...' ) { if (!rendermime) { rendermime = LabWidgetManager.globalRendermime; } const instance = Private.kernelWidgetManagers.get(kernel.id); if (instance) { - KernelWidgetManager.updateManagerKernel(instance, kernel); - KernelWidgetManager.attachToRendermime(rendermime, instance); + instance._useKernel(kernel); + KernelWidgetManager.configureRendermime( + rendermime, + instance, + pendingManagerMessage + ); return instance; } if (!kernel.handleComms) { @@ -374,68 +382,72 @@ export class KernelWidgetManager extends LabWidgetManager { LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => this.loadCustomWidgetDefinitions() ); - KernelWidgetManager.updateManagerKernel(this, kernel); - KernelWidgetManager.attachToRendermime(rendermime, this); + this._useKernel(kernel); + KernelWidgetManager.configureRendermime( + rendermime, + this, + pendingManagerMessage + ); } - static updateManagerKernel( - manager: KernelWidgetManager, - kernel: Kernel.IKernelConnection - ) { + _useKernel(this: KernelWidgetManager, kernel: Kernel.IKernelConnection) { if (!kernel.handleComms) { return; } - manager._handleKernelChanged({ + this._handleKernelChanged({ name: 'kernel', - oldValue: manager._kernel, + oldValue: this._kernel, newValue: kernel, }); - if (manager._kernel) { - manager._kernel.statusChanged.disconnect( - manager._handleKernelStatusChange, - manager + if (this._kernel) { + this._kernel.statusChanged.disconnect( + this._handleKernelStatusChange, + this ); - manager._kernel.connectionStatusChanged.disconnect( - manager._handleKernelConnectionStatusChange, - manager + this._kernel.connectionStatusChanged.disconnect( + this._handleKernelConnectionStatusChange, + this ); } - manager._kernel = kernel; - manager._kernel.statusChanged.connect( - manager._handleKernelStatusChange, - manager - ); - manager._kernel.connectionStatusChanged.connect( - manager._handleKernelConnectionStatusChange, - manager + this._kernel = kernel; + this._kernel.statusChanged.connect(this._handleKernelStatusChange, this); + this._kernel.connectionStatusChanged.connect( + this._handleKernelConnectionStatusChange, + this ); - manager._restoredStatus = false; - manager._kernelRestoreInProgress = true; - manager.clear_state().then(() => manager.restoreWidgets()); + this._restoredStatus = false; + this._kernelRestoreInProgress = true; + this.clear_state().then(() => this.restoreWidgets()); } /** - * Will define wManager as a renderer for rendermime if rendermime - * is not the global rendermime or there is only one wManager. - * If wManager is not provided, it will make the rendermine more general. + * Configure a non-global rendermime. Passing the global rendermine will do + * nothing. + * + * @param rendermime + * @param manager The manager to use with WidgetRenderer. + * @param pendingManagerMessage A message that is displayed while the manager + * has not been provided. If manager is not provided here a non-empty string + * assumes the manager will be provided at some time in the future. + * + * The default will search for a manager once. + * @returns */ - static attachToRendermime( + static configureRendermime( rendermime: IRenderMimeRegistry, - wManager?: KernelWidgetManager + manager?: KernelWidgetManager, + pendingManagerMessage = '' ) { - const wManager_ = - rendermime === LabWidgetManager.globalRendermime && - Private.kernelWidgetManagers.size > 1 - ? undefined - : wManager; - const pendingManagerMessage = wManager ? 'Loading widget ...' : ''; + if (rendermime === LabWidgetManager.globalRendermime) { + return; + } rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); rendermime.addFactory( { safe: false, mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options) => - new WidgetRenderer(options, wManager_, pendingManagerMessage), + createRenderer: (options: IRenderMime.IRendererOptions) => + new WidgetRenderer(options, manager, pendingManagerMessage), }, -10 ); @@ -491,7 +503,7 @@ export class KernelWidgetManager extends LabWidgetManager { return; } super.dispose(); - KernelWidgetManager.attachToRendermime(this.rendermime); + KernelWidgetManager.configureRendermime(this.rendermime); Private.kernelWidgetManagers.delete(this.kernel.id); this._handleKernelChanged({ name: 'kernel', @@ -559,15 +571,11 @@ export class WidgetManager extends Backbone.Model implements IDisposable { }); } if (rendermime !== LabWidgetManager.globalRendermime) { - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options) => - new WidgetRenderer(options, undefined, 'Waiting for kernel'), - }, - -10 + // Instruct the renderer to wait for the widgetManager. + KernelWidgetManager.configureRendermime( + rendermime, + undefined, + 'Waiting for kernel' ); } if (this.kernel) { @@ -618,9 +626,10 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this.restoreWidgets(this._context!.model); } } - KernelWidgetManager.attachToRendermime( + KernelWidgetManager.configureRendermime( this.rendermime, - this._widgetManager + this._widgetManager, + 'Loading widget ...' ); if (this._renderers) { for (const r of this._renderers) { @@ -814,17 +823,15 @@ export namespace WidgetManager { } /** - * Get the widgetManager that owns the model id=model_id. - * @param model_id An existing model_id - * @returns KernelWidgetManager + * Get the widgetManager that owns the model. */ -export function findWidgetManager(model_id: string): KernelWidgetManager { +export function getWidgetManager(model_id: string): KernelWidgetManager | null { 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}'`); + return null; } /** diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index 8a6e9ffc6e..c475e07436 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -225,7 +225,7 @@ function activateWidgetExtension( } WidgetManager.loggerRegistry = loggerRegistry; LabWidgetManager.globalRendermime = rendermime; - // Add a placeholder widget renderer. + // Add a default widget renderer. rendermime.addFactory( { safe: false, diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 7549067a4a..da8e9787f5 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -11,7 +11,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { DOMWidgetModel } from '@jupyter-widgets/base'; -import { LabWidgetManager, findWidgetManager } from './manager'; +import { LabWidgetManager, getWidgetManager } from './manager'; /** * A renderer for widgets. @@ -65,7 +65,7 @@ export class WidgetRenderer } let manager; if (!this._pendingManagerMessage && !this._managerIsSet) { - manager = findWidgetManager(source.model_id); + manager = getWidgetManager(source.model_id); } this.node.textContent = `${ this._pendingManagerMessage || model.data['text/plain'] @@ -80,13 +80,16 @@ export class WidgetRenderer } catch (err) { if (!manager.restoredStatus) { this._rerenderMimeModel = model; + } else if (this._pendingManagerMessage === 'Waiting for kernel') { + this.node.textContent = `Widget not found in this kernel: ${ + model.data['text/plain'] || source.model_id + }`; } else { // The manager has been restored, so this error won't be going away. this.node.textContent = 'Error displaying widget: model not found'; this.addClass('jupyter-widgets'); console.error(err); } - // Store the model for a possible rerender return Promise.resolve(); } From 3819e7d17ea9ad870d1b7d6ee39aa00e258e02c0 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 1 Jun 2024 14:13:20 +1000 Subject: [PATCH 09/22] Avoid generating a warning "Failed to fetch ipywidgets through the "jupyter.widget.control" comm channel ..." when loading a kernel where ipywidgets hasn't been imported yet. --- packages/base-manager/src/manager-base.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index fe569a5213..0806fa2b74 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -384,6 +384,11 @@ export abstract class ManagerBase implements IWidgetManager { let data: any; let buffers: any; let timeoutID: number | undefined; + const comm_ids = await this._get_comm_info(); + if (Object.keys(comm_ids).length === 0) { + // There is nothing to load, either a new kernel or no widgets in the kernel. + return Promise.resolve(); + } try { const initComm = await this._create_comm( CONTROL_COMM_TARGET, From b873722034fef8e7c37e6648de0b0777affc71ac Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 2 Jun 2024 10:36:30 +1000 Subject: [PATCH 10/22] WidgetModel.close - catch any error when closing comm to ensure closing runs to completion. --- packages/base/src/widget.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index 936becf6a4..9da9a4c4bf 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -214,16 +214,22 @@ export class WidgetModel extends Backbone.Model { /** * Close model * + * @param comm_closed - true if the comm is already being closed. If false, the comm will be closed. + * * @returns - a promise that is fulfilled when all the associated views have been removed. */ - close(): Promise { + close(comm_closed = false): Promise { // can only be closed once. if (this._closed) { return Promise.resolve(); } this._closed = true; - if (this.comm && this.comm_live) { - this.comm.close(); + if (this.comm && !comm_closed && this.comm_live) { + try { + this.comm.close(); + } catch (err) { + // Do Nothing + } } this.stopListening(); this.trigger('destroy', this); @@ -248,10 +254,10 @@ export class WidgetModel extends Backbone.Model { */ _handle_comm_closed(msg: KernelMessage.ICommCloseMsg): void { this.comm_live = false; - if (this._closed) { - this.close(); - } this.trigger('comm:close'); + if (!this._closed) { + this.close(true); + } } /** From b830b76f92045d9f0d50402354e5ca55d87acf5a Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 2 Jun 2024 17:35:19 +1000 Subject: [PATCH 11/22] Improved manager logic and re-rendering when the kernel is re-connected. --- python/jupyterlab_widgets/src/manager.ts | 193 ++++++++++------------ python/jupyterlab_widgets/src/renderer.ts | 43 ++--- 2 files changed, 107 insertions(+), 129 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index f753a2e77d..0895122fb3 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -24,7 +24,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { ReadonlyPartialJSONValue } from '@lumino/coreutils'; -import { INotebookModel } from '@jupyterlab/notebook'; +import { INotebookModel, NotebookModel } from '@jupyterlab/notebook'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; @@ -353,24 +353,20 @@ export abstract class LabWidgetManager * A singleton widget manager per kernel for the lifecycle of the kernel. * The factory of the rendermime will be update to use the widgetManager * directly if it isn't the globalRendermime. + * + * Note: The rendermime of the instance is always the global rendermime. */ export class KernelWidgetManager extends LabWidgetManager { constructor( kernel: Kernel.IKernelConnection, - rendermime: IRenderMimeRegistry | null, + rendermime?: IRenderMimeRegistry, pendingManagerMessage = 'Loading widget ...' ) { - if (!rendermime) { - rendermime = LabWidgetManager.globalRendermime; - } const instance = Private.kernelWidgetManagers.get(kernel.id); if (instance) { instance._useKernel(kernel); - KernelWidgetManager.configureRendermime( - rendermime, - instance, - pendingManagerMessage - ); + configureRendermime(rendermime, instance, pendingManagerMessage); + instance._firstLoad = false; return instance; } if (!kernel.handleComms) { @@ -383,15 +379,11 @@ export class KernelWidgetManager extends LabWidgetManager { this.loadCustomWidgetDefinitions() ); this._useKernel(kernel); - KernelWidgetManager.configureRendermime( - rendermime, - this, - pendingManagerMessage - ); + configureRendermime(rendermime, this, pendingManagerMessage); } _useKernel(this: KernelWidgetManager, kernel: Kernel.IKernelConnection) { - if (!kernel.handleComms) { + if (!kernel.handleComms || this._kernel === kernel) { return; } this._handleKernelChanged({ @@ -420,39 +412,6 @@ export class KernelWidgetManager extends LabWidgetManager { this.clear_state().then(() => this.restoreWidgets()); } - /** - * Configure a non-global rendermime. Passing the global rendermine will do - * nothing. - * - * @param rendermime - * @param manager The manager to use with WidgetRenderer. - * @param pendingManagerMessage A message that is displayed while the manager - * has not been provided. If manager is not provided here a non-empty string - * assumes the manager will be provided at some time in the future. - * - * The default will search for a manager once. - * @returns - */ - static configureRendermime( - rendermime: IRenderMimeRegistry, - manager?: KernelWidgetManager, - pendingManagerMessage = '' - ) { - if (rendermime === LabWidgetManager.globalRendermime) { - return; - } - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options: IRenderMime.IRendererOptions) => - new WidgetRenderer(options, manager, pendingManagerMessage), - }, - -10 - ); - } - _handleKernelConnectionStatusChange( sender: Kernel.IKernelConnection, status: Kernel.ConnectionStatus @@ -482,6 +441,7 @@ export class KernelWidgetManager extends LabWidgetManager { break; } } + /** * Restore widgets from kernel. */ @@ -503,7 +463,7 @@ export class KernelWidgetManager extends LabWidgetManager { return; } super.dispose(); - KernelWidgetManager.configureRendermime(this.rendermime); + configureRendermime(this.rendermime); Private.kernelWidgetManagers.delete(this.kernel.id); this._handleKernelChanged({ name: 'kernel', @@ -518,6 +478,10 @@ export class KernelWidgetManager extends LabWidgetManager { return this._kernel; } + get firstLoad() { + return this._firstLoad; + } + loadCustomWidgetDefinitions() { for (const data of LabWidgetManager.WIDGET_REGISTRY) { this.register(data); @@ -527,7 +491,7 @@ export class KernelWidgetManager extends LabWidgetManager { filterModelState(serialized_state: any): any { return this.filterExistingModelState(serialized_state); } - + private _firstLoad = true; private _kernel: Kernel.IKernelConnection; protected _kernelRestoreInProgress = false; } @@ -570,17 +534,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } }); } - if (rendermime !== LabWidgetManager.globalRendermime) { - // Instruct the renderer to wait for the widgetManager. - KernelWidgetManager.configureRendermime( - rendermime, - undefined, - 'Waiting for kernel' - ); - } - if (this.kernel) { - this.updateWidgetManager(); - } + this.updateWidgetManager(); } /** @@ -605,38 +559,45 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } async updateWidgetManager() { + let wManager: KernelWidgetManager | undefined; + if (!this.kernel) { + //'No kernel' is matched in WidgetRenderer to supress 'model not found errors'. + configureRendermime(this.rendermime, undefined, 'No kernel'); + } else { + await this.context.sessionContext.ready; + wManager = new KernelWidgetManager(this.kernel); + } + if (wManager === this._widgetManager) { + return; + } if (this._widgetManager) { this._widgetManager.onUnhandledIOPubMessage.disconnect( this.onUnhandledIOPubMessage, this ); } - if (this.kernel) { - await this.context.sessionContext.ready; - this._widgetManager = new KernelWidgetManager(this.kernel, null); - this._widgetManager.onUnhandledIOPubMessage.connect( - this.onUnhandledIOPubMessage, - this - ); - if (!this._widgetManager.restoredStatus) { - await new Promise((resolve) => { - this._widgetManager?.restored.connect(resolve); - }); - if (!this.restored) { - this.restoreWidgets(this._context!.model); - } + this._widgetManager = wManager; + if (!wManager) { + return; + } + if (wManager.firstLoad) { + await new Promise((resolve) => { + this._widgetManager?.restored.connect(resolve); + }); + if (!this.restored) { + this.restoreWidgets(this._context!.model); } - KernelWidgetManager.configureRendermime( - this.rendermime, - this._widgetManager, - 'Loading widget ...' - ); - if (this._renderers) { - for (const r of this._renderers) { - r.manager = this._widgetManager; - } + } + if (this._renderers) { + for (const r of this._renderers) { + r.manager = wManager; } } + configureRendermime(this.rendermime, wManager, 'Loading widget ...'); + wManager.onUnhandledIOPubMessage.connect( + this.onUnhandledIOPubMessage, + this + ); } onUnhandledIOPubMessage( @@ -676,7 +637,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this.setDirty(); } - get widgetManager(): KernelWidgetManager | null { + get widgetManager(): KernelWidgetManager | undefined { return this._widgetManager; } @@ -698,17 +659,13 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } /** - * Restore widgets from notebook and saved state. + * Restore widgets from model. */ - async restoreWidgets( - notebook: INotebookModel, - { loadNotebook } = { loadNotebook: true } - ): Promise { + async restoreWidgets(model: INotebookModel): Promise { try { - if (loadNotebook) { - await this._loadFromNotebook(notebook); + if (model instanceof NotebookModel) { + await this._loadFromNotebook(model); } - // If the restore worked above, then update our state. this._restoredStatus = true; this._restored.emit(); @@ -733,19 +690,6 @@ export class WidgetManager extends Backbone.Model implements IDisposable { let state = widget_md[WIDGET_STATE_MIMETYPE]; state = this.widgetManager.filterModelState(state); - // Ensure the kernel has been restored. - let timeoutID; - const restored = this.widgetManager.restored; - if (!this.widgetManager.restoredStatus) { - await new Promise((resolve, reject) => { - restored.connect(resolve); - timeoutID = window.setTimeout( - () => reject('Timeout waiting for '), - 4000 - ); - }); - } - clearTimeout(timeoutID); // Restore any widgets from saved state that are not live await this.widgetManager.set_state(state); } @@ -812,7 +756,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { private _context: DocumentRegistry.IContext; private _rendermime: IRenderMimeRegistry; private _settings: WidgetManager.Settings; - private _widgetManager: KernelWidgetManager | null; + private _widgetManager: KernelWidgetManager | undefined; private _renderers: IterableIterator | undefined; } @@ -822,6 +766,39 @@ export namespace WidgetManager { }; } +/** + * Configure a non-global rendermime. Passing the global rendermine will do + * nothing. + * + * @param rendermime + * @param manager The manager to use with WidgetRenderer. + * @param pendingManagerMessage A message that is displayed while the manager + * has not been provided. If manager is not provided here a non-empty string + * assumes the manager will be provided at some time in the future. + * + * The default will search for a manager once prior to waiting for a manager. + * @returns + */ +function configureRendermime( + rendermime?: IRenderMimeRegistry, + manager?: KernelWidgetManager, + pendingManagerMessage = '' +) { + if (!rendermime || rendermime === LabWidgetManager.globalRendermime) { + return; + } + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options: IRenderMime.IRendererOptions) => + new WidgetRenderer(options, manager, pendingManagerMessage), + }, + -10 + ); +} + /** * Get the widgetManager that owns the model. */ diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index da8e9787f5..6654ac99ed 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -48,11 +48,20 @@ export class WidgetRenderer /** * The widget manager. + * + * Will accept the first non-null manager and ignore anything afterwards. */ - set manager(value: LabWidgetManager) { - value.restored.connect(this._rerender, this); - this._manager.resolve(value); - this._managerIsSet = true; + + set manager(value: LabWidgetManager | null) { + if (value && !this._managerIsSet) { + // Can only set the manager once + this._manager.resolve(value); + this._managerIsSet = true; + value.restored.connect(this._rerender, this); + this.disposed.connect(() => + value.restored.disconnect(this._rerender, this) + ); + } } async renderModel(model: IRenderMime.IMimeModel): Promise { @@ -63,28 +72,23 @@ export class WidgetRenderer this.hide(); return Promise.resolve(); } - let manager; if (!this._pendingManagerMessage && !this._managerIsSet) { - manager = getWidgetManager(source.model_id); + this.manager = getWidgetManager(source.model_id); } this.node.textContent = `${ this._pendingManagerMessage || model.data['text/plain'] }`; - if (!manager) { - manager = await this._manager.promise; - } + const manager: LabWidgetManager = await this._manager.promise; + this._rerenderMimeModel = model; + let wModel: DOMWidgetModel; try { // Presume we have a DOMWidgetModel. Should we check for sure? wModel = (await manager.get_model(source.model_id)) as DOMWidgetModel; } catch (err) { - if (!manager.restoredStatus) { - this._rerenderMimeModel = model; - } else if (this._pendingManagerMessage === 'Waiting for kernel') { - this.node.textContent = `Widget not found in this kernel: ${ - model.data['text/plain'] || source.model_id - }`; - } else { + if (this._pendingManagerMessage === 'No kernel') { + this.node.textContent = 'Model not found in new kernel'; + } else if (manager.restoredStatus) { // The manager has been restored, so this error won't be going away. this.node.textContent = 'Error displaying widget: model not found'; this.addClass('jupyter-widgets'); @@ -93,10 +97,6 @@ export class WidgetRenderer // Store the model for a possible rerender return Promise.resolve(); } - - // Successful getting the model, so we don't need to try to rerender. - this._rerenderMimeModel = null; - let widget: LuminoWidget; try { widget = (await manager.create_view(wModel)).luminoWidget; @@ -110,12 +110,12 @@ export class WidgetRenderer // Clear any previous loading message. this.node.textContent = ''; this.addWidget(widget); + this.show(); // When the widget is disposed, hide this container and make sure we // change the output model to reflect the view was closed. widget.disposed.connect(() => { this.hide(); - source.model_id = ''; }); } @@ -127,6 +127,7 @@ export class WidgetRenderer return; } this._manager = null!; + this._rerenderMimeModel = null; super.dispose(); } From a7a6d5fa20b2ed1158f5300201d483d63ffd1440 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 2 Jun 2024 21:18:15 +1000 Subject: [PATCH 12/22] Added delays for getWidgetManager and get_model if the model isn't immediately available. --- packages/base-manager/src/manager-base.ts | 7 ++++++- python/jupyterlab_widgets/src/manager.ts | 13 +++++++++---- python/jupyterlab_widgets/src/renderer.ts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 0806fa2b74..aa63ab24ed 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -215,9 +215,13 @@ 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 { + let i = 0; + while (!this._models[model_id] && i < this._sleepTimes.length) { + new Promise((r) => setTimeout(r, this._sleepTimes[i++])) + } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { - throw new Error('widget model not found'); + throw new Error(`widget model '${model_id}' not found`); } return modelPromise; } @@ -874,6 +878,7 @@ export abstract class ManagerBase implements IWidgetManager { /** * Dictionary of model ids and model instance promises */ + private _sleepTimes = [2, 50, 200, 800]; private _models: { [key: string]: Promise } = Object.create(null); } diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 0895122fb3..a1d9d73d97 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -802,10 +802,15 @@ function configureRendermime( /** * Get the widgetManager that owns the model. */ -export function getWidgetManager(model_id: string): KernelWidgetManager | null { - for (const wManager of Private.kernelWidgetManagers.values()) { - if (wManager.has_model(model_id)) { - return wManager; +export async function getWidgetManager( + model_id: string +): Promise { + for (const sleepTime of [0, 50, 1000]) { + await new Promise((r) => setTimeout(r, sleepTime)); + for (const wManager of Private.kernelWidgetManagers.values()) { + if (wManager.has_model(model_id)) { + return wManager; + } } } return null; diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 6654ac99ed..6f0efbb731 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -73,7 +73,7 @@ export class WidgetRenderer return Promise.resolve(); } if (!this._pendingManagerMessage && !this._managerIsSet) { - this.manager = getWidgetManager(source.model_id); + this.manager = await getWidgetManager(source.model_id); } this.node.textContent = `${ this._pendingManagerMessage || model.data['text/plain'] From ab0c544ad2ef7a9e8fa8c11739798d0383739d16 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Mon, 3 Jun 2024 22:01:20 +1000 Subject: [PATCH 13/22] Manager, WidgetManager and plugin refactoring (simplification) . Warning: some signature changes. --- packages/base-manager/src/manager-base.ts | 2 +- python/jupyterlab_widgets/package.json | 2 - python/jupyterlab_widgets/src/manager.ts | 283 ++++++++++++---------- python/jupyterlab_widgets/src/plugin.ts | 143 +---------- python/jupyterlab_widgets/src/renderer.ts | 17 +- yarn.lock | 18 +- 6 files changed, 184 insertions(+), 281 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index aa63ab24ed..7f578fd9ed 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -217,7 +217,7 @@ export abstract class ManagerBase implements IWidgetManager { async get_model(model_id: string): Promise { let i = 0; while (!this._models[model_id] && i < this._sleepTimes.length) { - new Promise((r) => setTimeout(r, this._sleepTimes[i++])) + new Promise((resolve) => setTimeout(resolve, this._sleepTimes[i++])); } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { diff --git a/python/jupyterlab_widgets/package.json b/python/jupyterlab_widgets/package.json index 1711edbb5f..c06acc14e3 100644 --- a/python/jupyterlab_widgets/package.json +++ b/python/jupyterlab_widgets/package.json @@ -64,7 +64,6 @@ "@jupyterlab/services": "^6.0.0 || ^7.0.0", "@jupyterlab/settingregistry": "^3.0.0 || ^4.0.0", "@jupyterlab/translation": "^3.0.0 || ^4.0.0", - "@lumino/algorithm": "^1.11.1 || ^2.0.0", "@lumino/coreutils": "^1.11.1 || ^2.1", "@lumino/disposable": "^1.10.1 || ^2.1", "@lumino/properties": "^2.0.1", @@ -76,7 +75,6 @@ }, "devDependencies": { "@jupyterlab/builder": "^3.0.0 || ^4.0.0", - "@jupyterlab/cells": "^3.0.0 || ^4.0.0", "@types/jquery": "^3.5.16", "@types/semver": "^7.3.6", "@typescript-eslint/eslint-plugin": "^5.8.0", diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index a1d9d73d97..9bd5ed42fb 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -20,6 +20,8 @@ import { import { IDisposable } from '@lumino/disposable'; +import { AttachedProperty } from '@lumino/properties'; + import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { ReadonlyPartialJSONValue } from '@lumino/coreutils'; @@ -351,7 +353,7 @@ export abstract class LabWidgetManager /** * A singleton widget manager per kernel for the lifecycle of the kernel. - * The factory of the rendermime will be update to use the widgetManager + * The factory of the rendermime will be updated to use the widgetManager * directly if it isn't the globalRendermime. * * Note: The rendermime of the instance is always the global rendermime. @@ -365,8 +367,11 @@ export class KernelWidgetManager extends LabWidgetManager { const instance = Private.kernelWidgetManagers.get(kernel.id); if (instance) { instance._useKernel(kernel); - configureRendermime(rendermime, instance, pendingManagerMessage); - instance._firstLoad = false; + KernelWidgetManager.configureRendermime( + rendermime, + instance, + pendingManagerMessage + ); return instance; } if (!kernel.handleComms) { @@ -379,7 +384,11 @@ export class KernelWidgetManager extends LabWidgetManager { this.loadCustomWidgetDefinitions() ); this._useKernel(kernel); - configureRendermime(rendermime, this, pendingManagerMessage); + KernelWidgetManager.configureRendermime( + rendermime, + this, + pendingManagerMessage + ); } _useKernel(this: KernelWidgetManager, kernel: Kernel.IKernelConnection) { @@ -412,6 +421,38 @@ export class KernelWidgetManager extends LabWidgetManager { this.clear_state().then(() => this.restoreWidgets()); } + /** + * Configure a non-global rendermime. Passing the global rendermine will do + * nothing. + * + * @param rendermime + * @param manager The manager to use with WidgetRenderer. + * @param pendingManagerMessage A message that is displayed while the manager + * has not been provided. If manager is not provided here a non-empty string + * assumes the manager will be provided at some time in the future. + * + * The default will search for a manager once prior to waiting for a manager. + * @returns + */ + static configureRendermime( + rendermime?: IRenderMimeRegistry, + manager?: KernelWidgetManager, + pendingManagerMessage = '' + ) { + if (!rendermime || rendermime === LabWidgetManager.globalRendermime) { + return; + } + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: (options: IRenderMime.IRendererOptions) => + new WidgetRenderer(options, manager, pendingManagerMessage), + }, + -10 + ); + } _handleKernelConnectionStatusChange( sender: Kernel.IKernelConnection, status: Kernel.ConnectionStatus @@ -451,10 +492,13 @@ export class KernelWidgetManager extends LabWidgetManager { } catch (err) { // Do nothing } + this.triggerRestored(); + } + + triggerRestored() { this._restoredStatus = true; this._restored.emit(); } - /** * Dispose the resources held by the manager. */ @@ -463,7 +507,7 @@ export class KernelWidgetManager extends LabWidgetManager { return; } super.dispose(); - configureRendermime(this.rendermime); + KernelWidgetManager.configureRendermime(this.rendermime); Private.kernelWidgetManagers.delete(this.kernel.id); this._handleKernelChanged({ name: 'kernel', @@ -478,10 +522,6 @@ export class KernelWidgetManager extends LabWidgetManager { return this._kernel; } - get firstLoad() { - return this._firstLoad; - } - loadCustomWidgetDefinitions() { for (const data of LabWidgetManager.WIDGET_REGISTRY) { this.register(data); @@ -491,27 +531,34 @@ export class KernelWidgetManager extends LabWidgetManager { filterModelState(serialized_state: any): any { return this.filterExistingModelState(serialized_state); } - private _firstLoad = true; + private _kernel: Kernel.IKernelConnection; protected _kernelRestoreInProgress = false; } /** - * Monitor kernel of the Context swapping the kernel manager on demand. - * A better name would be `NotebookManagerSwitcher'. + * A single 'WidgetManager' per context. + * It monitors the kernel of the context swapping the kernel manager when the + * kernel is changed. + * A better name would be `WidgetManagerChanger'. TODO: change name and context. */ export class WidgetManager extends Backbone.Model implements IDisposable { constructor( - context: DocumentRegistry.IContext, + context: DocumentRegistry.Context, rendermime: IRenderMimeRegistry, - settings: WidgetManager.Settings, - renderers?: IterableIterator + settings?: WidgetManager.Settings ) { + const instance = Private.widgetManagerProperty.get(context); + if (instance) { + WidgetManager._rendermimeSetFactory(rendermime, instance); + return instance; + } super(); - this._rendermime = rendermime; + Private.widgetManagerProperty.set(context, this); this._context = context; this._settings = settings; - this._renderers = renderers; + this._renderers = new Set(); + WidgetManager._rendermimeSetFactory(rendermime, this); context.sessionContext.kernelChanged.connect( this._handleKernelChange, @@ -529,7 +576,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { ); if (context?.saveState) { context.saveState.connect((sender, saveState) => { - if (saveState === 'started' && settings.saveState) { + if (saveState === 'started' && settings?.saveState) { this._saveState(); } }); @@ -545,26 +592,36 @@ export class WidgetManager extends Backbone.Model implements IDisposable { return; } 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, - }); - } else { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore JupyterLab 3 support - this._context.model.metadata.set('widgets', { + const model = this._context.model; + if (model instanceof NotebookModel) { + model.setMetadata('widgets', { 'application/vnd.jupyter.widget-state+json': state, }); } } + static _rendermimeSetFactory( + rendermime: IRenderMimeRegistry, + manager: WidgetManager + ) { + if (rendermime === LabWidgetManager.globalRendermime) { + throw new Error('Using global rendermime is not permitted!'); + } + rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); + rendermime.addFactory( + { + safe: false, + mimeTypes: [WIDGET_VIEW_MIMETYPE], + createRenderer: manager._newWidgetRenderer.bind(manager), + }, + -10 + ); + } + async updateWidgetManager() { let wManager: KernelWidgetManager | undefined; - if (!this.kernel) { - //'No kernel' is matched in WidgetRenderer to supress 'model not found errors'. - configureRendermime(this.rendermime, undefined, 'No kernel'); - } else { - await this.context.sessionContext.ready; + await this.context.sessionContext.ready; + if (this.kernel) { wManager = new KernelWidgetManager(this.kernel); } if (wManager === this._widgetManager) { @@ -580,28 +637,38 @@ export class WidgetManager extends Backbone.Model implements IDisposable { if (!wManager) { return; } - if (wManager.firstLoad) { + wManager.onUnhandledIOPubMessage.connect( + this.onUnhandledIOPubMessage, + this + ); + if (!wManager.restored) { await new Promise((resolve) => { this._widgetManager?.restored.connect(resolve); }); - if (!this.restored) { - this.restoreWidgets(this._context!.model); - } } - if (this._renderers) { - for (const r of this._renderers) { - r.manager = wManager; - } + this._renderers.forEach( + (renderer: WidgetRenderer) => (renderer.manager = wManager) + ); + if (await this._restoreWidgets(this._context!.model)) { + wManager.triggerRestored(); } - configureRendermime(this.rendermime, wManager, 'Loading widget ...'); - wManager.onUnhandledIOPubMessage.connect( - this.onUnhandledIOPubMessage, - this + } + + _newWidgetRenderer(options: IRenderMime.IRendererOptions) { + const renderer = new WidgetRenderer( + options, + this.widgetManager, + this.widgetManager ? 'Loading widget ...' : 'No kernel' + ); + this._renderers.add(renderer); + renderer.disposed.connect((renderer_: WidgetRenderer) => + this._renderers.delete(renderer_) ); + return renderer; } onUnhandledIOPubMessage( - sender: LabWidgetManager, + sender: KernelWidgetManager, msg: KernelMessage.IIOPubMessage ) { if (WidgetManager.loggerRegistry) { @@ -641,34 +708,16 @@ export class WidgetManager extends Backbone.Model implements IDisposable { 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; - } - /** * Restore widgets from model. */ - async restoreWidgets(model: INotebookModel): Promise { + async _restoreWidgets( + model: DocumentRegistry.IModel + ): Promise { try { if (model instanceof NotebookModel) { - await this._loadFromNotebook(model); + return await this._loadFromNotebook(model); } - // If the restore worked above, then update our state. - this._restoredStatus = true; - this._restored.emit(); } catch (err) { // Do nothing if the restore did not work. } @@ -677,22 +726,25 @@ export class WidgetManager extends Backbone.Model implements IDisposable { /** * Load widget state from notebook metadata */ - async _loadFromNotebook(notebook: INotebookModel): Promise { - if (!this.widgetManager) { - return; - } - const widget_md = notebook.getMetadata - ? (notebook.getMetadata('widgets') as any) - : // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore JupyterLab 3 support - notebook.metadata.get('widgets'); - if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) { - let state = widget_md[WIDGET_STATE_MIMETYPE]; - state = this.widgetManager.filterModelState(state); - - // Restore any widgets from saved state that are not live - await this.widgetManager.set_state(state); + async _loadFromNotebook(notebook: INotebookModel): Promise { + if (this.widgetManager) { + const widget_md = notebook.getMetadata + ? (notebook.getMetadata('widgets') as any) + : // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore JupyterLab 3 support + notebook.metadata.get('widgets'); + if (widget_md && widget_md[WIDGET_STATE_MIMETYPE]) { + let state = widget_md[WIDGET_STATE_MIMETYPE]; + state = this.widgetManager.filterModelState(state); + const n = Object.keys(state?.state || {}).length; + if (n) { + // Restore any widgets from saved state that are not live + await this.widgetManager.set_state(state); + } + return n; + } } + return 0; } /** @@ -712,8 +764,11 @@ export class WidgetManager extends Backbone.Model implements IDisposable { if (this.isDisposed) { return; } + // Remove the custom factory from the rendermime. TODO: de-register the rendermime factory for this object + KernelWidgetManager.configureRendermime(this.rendermime); + this._renderers.forEach((renderer) => renderer.dispose()); + this._renderers = null!; this._context = null!; - this._renderers = undefined; this._context = null!; this._rendermime = null!; this._settings = null!; @@ -727,7 +782,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { return this.context.urlResolver.getDownloadUrl(partial); } - get context(): DocumentRegistry.IContext { + get context(): DocumentRegistry.Context { return this._context; } @@ -745,19 +800,19 @@ export class WidgetManager extends Backbone.Model implements IDisposable { * TODO: perhaps should also set dirty when any model changes any data */ setDirty(): void { - if (this._settings.saveState) { - this._context!.model.dirty = true; + if (this._settings?.saveState && this._context?.model) { + this._context.model.dirty = true; } } static loggerRegistry: ILoggerRegistry | null; - protected _restored = new Signal(this); - protected _restoredStatus = false; + // protected _restored = new Signal(this); + // protected _restoredStatus = false; private _isDisposed = false; - private _context: DocumentRegistry.IContext; + private _context: DocumentRegistry.Context; private _rendermime: IRenderMimeRegistry; - private _settings: WidgetManager.Settings; + private _settings: WidgetManager.Settings | undefined; private _widgetManager: KernelWidgetManager | undefined; - private _renderers: IterableIterator | undefined; + _renderers: Set; } export namespace WidgetManager { @@ -766,54 +821,21 @@ export namespace WidgetManager { }; } -/** - * Configure a non-global rendermime. Passing the global rendermine will do - * nothing. - * - * @param rendermime - * @param manager The manager to use with WidgetRenderer. - * @param pendingManagerMessage A message that is displayed while the manager - * has not been provided. If manager is not provided here a non-empty string - * assumes the manager will be provided at some time in the future. - * - * The default will search for a manager once prior to waiting for a manager. - * @returns - */ -function configureRendermime( - rendermime?: IRenderMimeRegistry, - manager?: KernelWidgetManager, - pendingManagerMessage = '' -) { - if (!rendermime || rendermime === LabWidgetManager.globalRendermime) { - return; - } - rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); - rendermime.addFactory( - { - safe: false, - mimeTypes: [WIDGET_VIEW_MIMETYPE], - createRenderer: (options: IRenderMime.IRendererOptions) => - new WidgetRenderer(options, manager, pendingManagerMessage), - }, - -10 - ); -} - /** * Get the widgetManager that owns the model. */ export async function getWidgetManager( model_id: string -): Promise { +): Promise { for (const sleepTime of [0, 50, 1000]) { - await new Promise((r) => setTimeout(r, sleepTime)); + await new Promise((resolve) => setTimeout(resolve, sleepTime)); for (const wManager of Private.kernelWidgetManagers.values()) { if (wManager.has_model(model_id)) { return wManager; } } } - return null; + return undefined; } /** @@ -821,4 +843,11 @@ export async function getWidgetManager( */ namespace Private { export const kernelWidgetManagers = new Map(); + export const widgetManagerProperty = new AttachedProperty< + DocumentRegistry.Context, + WidgetManager | undefined + >({ + name: 'widgetManager', + create: (owner: DocumentRegistry.Context): undefined => undefined, + }); } diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index c475e07436..932d564338 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -5,18 +5,9 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { - CodeConsole, - ConsolePanel, - IConsoleTracker, -} from '@jupyterlab/console'; +import { ConsolePanel, IConsoleTracker } from '@jupyterlab/console'; -import { - INotebookModel, - INotebookTracker, - Notebook, - NotebookPanel, -} from '@jupyterlab/notebook'; +import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { JupyterFrontEnd, @@ -29,14 +20,8 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { ILoggerRegistry } from '@jupyterlab/logconsole'; -import { CodeCell } from '@jupyterlab/cells'; - -import { filter } from '@lumino/algorithm'; - import { DisposableDelegate } from '@lumino/disposable'; -import { AttachedProperty } from '@lumino/properties'; - import { WidgetRenderer } from './renderer'; import { @@ -62,113 +47,19 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; */ const SETTINGS: WidgetManager.Settings = { saveState: false }; -/** - * Iterate through all widget renderers in a notebook. - */ -function* notebookWidgetRenderers( - nb: Notebook -): Generator { - for (const cell of nb.widgets) { - if (cell.model.type === 'code') { - for (const codecell of (cell as CodeCell).outputArea.widgets) { - // We use Array.from instead of using Lumino 2 (JLab 4) iterator - // This is to support Lumino 1 (JLab 3) as well - for (const output of Array.from(codecell.children())) { - if (output instanceof WidgetRenderer) { - yield output; - } - } - } - } - } -} - -/** - * Iterate through all widget renderers in a console. - */ -function* consoleWidgetRenderers( - console: CodeConsole -): Generator { - for (const cell of Array.from(console.cells)) { - if (cell.model.type === 'code') { - for (const codecell of (cell as unknown as CodeCell).outputArea.widgets) { - for (const output of Array.from(codecell.children())) { - if (output instanceof WidgetRenderer) { - yield output; - } - } - } - } - } -} - -/** - * Iterate through all matching linked output views - */ -function* outputViews( - app: JupyterFrontEnd, - path: string -): Generator { - const linkedViews = filter( - app.shell.widgets(), - (w) => w.id.startsWith('LinkedOutputView-') && (w as any).path === path - ); - // We use Array.from instead of using Lumino 2 (JLab 4) iterator - // This is to support Lumino 1 (JLab 3) as well - for (const view of Array.from(linkedViews)) { - for (const outputs of Array.from(view.children())) { - for (const output of Array.from(outputs.children())) { - if (output instanceof WidgetRenderer) { - yield output; - } - } - } - } -} - -function* chain( - ...args: IterableIterator[] -): Generator { - for (const it of args) { - yield* it; - } -} - export function registerWidgetManager( - context: DocumentRegistry.IContext, - rendermime: IRenderMimeRegistry, - renderers: IterableIterator + context: DocumentRegistry.Context, + rendermime: IRenderMimeRegistry ): DisposableDelegate { - let wManager = Private.widgetManagerProperty.get(context); - if (!wManager) { - wManager = new WidgetManager(context, rendermime, SETTINGS, renderers); - Private.widgetManagerProperty.set(context, wManager); - } - return new DisposableDelegate(() => { - wManager!.dispose(); - }); + const wManager = new WidgetManager(context, rendermime, SETTINGS); + return new DisposableDelegate(() => wManager!.dispose()); } -function attachWidgetManagerToPanel( - panel: NotebookPanel | ConsolePanel, - app: JupyterFrontEnd -) { +function attachWidgetManager(panel: NotebookPanel | ConsolePanel) { if (panel instanceof NotebookPanel) { - registerWidgetManager( - panel.context, - panel.content.rendermime, - chain( - notebookWidgetRenderers(panel.content), - outputViews(app, panel.context.path) - ) - ); + new WidgetManager(panel.context, panel.content.rendermime, SETTINGS); } 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)) - ); + new WidgetManager(panel.console as any, panel.console.rendermime); } } @@ -236,9 +127,9 @@ function activateWidgetExtension( ); for (const tracker of [widgetTracker, consoleTracker]) { if (tracker !== null) { - tracker.forEach((panel) => attachWidgetManagerToPanel(panel, app)); + tracker.forEach((panel) => attachWidgetManager(panel)); tracker.widgetAdded.connect((sender, panel) => - attachWidgetManagerToPanel(panel, app) + attachWidgetManager(panel) ); } } @@ -361,15 +252,3 @@ export default [ controlWidgetsPlugin, outputWidgetPlugin, ]; -namespace Private { - /** - * A private attached property for a widget manager. - */ - 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 6f0efbb731..621ccca470 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -41,9 +41,7 @@ export class WidgetRenderer super(); this.mimeType = options.mimeType; this._pendingManagerMessage = pendingManagerMessage; - if (manager) { - this.manager = manager; - } + this.manager = manager; } /** @@ -52,14 +50,14 @@ export class WidgetRenderer * Will accept the first non-null manager and ignore anything afterwards. */ - set manager(value: LabWidgetManager | null) { + set manager(value: LabWidgetManager | undefined) { if (value && !this._managerIsSet) { // Can only set the manager once this._manager.resolve(value); this._managerIsSet = true; - value.restored.connect(this._rerender, this); + value.restored.connect(this.rerender, this); this.disposed.connect(() => - value.restored.disconnect(this._rerender, this) + value.restored.disconnect(this.rerender, this) ); } } @@ -87,7 +85,7 @@ export class WidgetRenderer wModel = (await manager.get_model(source.model_id)) as DOMWidgetModel; } catch (err) { if (this._pendingManagerMessage === 'No kernel') { - this.node.textContent = 'Model not found in new kernel'; + this.node.textContent = `Model not found for this kernel: ${model.data['text/plain']}`; } else if (manager.restoredStatus) { // The manager has been restored, so this error won't be going away. this.node.textContent = 'Error displaying widget: model not found'; @@ -131,8 +129,9 @@ export class WidgetRenderer super.dispose(); } - private _rerender(): void { - if (this._rerenderMimeModel) { + rerender(): void { + // TODO: Add conditions for when re-rendering should occur. + if (this._rerenderMimeModel && !this.children.length) { // Clear the error message this.node.textContent = ''; this.removeClass('jupyter-widgets'); diff --git a/yarn.lock b/yarn.lock index 0558c955e8..da9009d3b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -987,7 +987,6 @@ __metadata: "@jupyter-widgets/output": ^6.0.8 "@jupyterlab/application": ^3.0.0 || ^4.0.0 "@jupyterlab/builder": ^3.0.0 || ^4.0.0 - "@jupyterlab/cells": ^3.0.0 || ^4.0.0 "@jupyterlab/console": ^3.0.0 || ^4.0.0 "@jupyterlab/docregistry": ^3.0.0 || ^4.0.0 "@jupyterlab/logconsole": ^3.0.0 || ^4.0.0 @@ -1001,7 +1000,6 @@ __metadata: "@jupyterlab/services": ^6.0.0 || ^7.0.0 "@jupyterlab/settingregistry": ^3.0.0 || ^4.0.0 "@jupyterlab/translation": ^3.0.0 || ^4.0.0 - "@lumino/algorithm": ^1.11.1 || ^2.0.0 "@lumino/coreutils": ^1.11.1 || ^2.1 "@lumino/disposable": ^1.10.1 || ^2.1 "@lumino/properties": ^2.0.1 @@ -1245,7 +1243,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/cells@npm:^3.0.0 || ^4.0.0, @jupyterlab/cells@npm:^4.2.1": +"@jupyterlab/cells@npm:^4.2.1": version: 4.2.1 resolution: "@jupyterlab/cells@npm:4.2.1" dependencies: @@ -2756,13 +2754,6 @@ __metadata: languageName: node linkType: hard -"@lumino/algorithm@npm:^1.11.1 || ^2.0.0, @lumino/algorithm@npm:^2.0.1": - version: 2.0.1 - resolution: "@lumino/algorithm@npm:2.0.1" - checksum: cbf7fcf6ee6b785ea502cdfddc53d61f9d353dcb9659343511d5cd4b4030be2ff2ca4c08daec42f84417ab0318a3d9972a17319fa5231693e109ab112dcf8000 - languageName: node - linkType: hard - "@lumino/algorithm@npm:^1.9.1 || ^2.1, @lumino/algorithm@npm:^1.9.2": version: 1.9.2 resolution: "@lumino/algorithm@npm:1.9.2" @@ -2770,6 +2761,13 @@ __metadata: languageName: node linkType: hard +"@lumino/algorithm@npm:^2.0.1": + version: 2.0.1 + resolution: "@lumino/algorithm@npm:2.0.1" + checksum: cbf7fcf6ee6b785ea502cdfddc53d61f9d353dcb9659343511d5cd4b4030be2ff2ca4c08daec42f84417ab0318a3d9972a17319fa5231693e109ab112dcf8000 + languageName: node + linkType: hard + "@lumino/application@npm:^2.3.1": version: 2.3.1 resolution: "@lumino/application@npm:2.3.1" From 658f15193c0b38c86a09099a466464af78e68b2d Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Tue, 4 Jun 2024 18:52:56 +1000 Subject: [PATCH 14/22] Change packaging workflow to jupyterlab~4.0. --- .github/workflows/packaging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index b99fed35e7..d11a8c2ad9 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -134,7 +134,7 @@ jobs: - name: Check the JupyterLab extension is installed if: matrix.dist != 'widgetsnbextension*.tar.gz' run: | - python -m pip install jupyterlab~=3.0 + python -m pip install jupyterlab~=4.0 jupyter labextension list jupyter labextension list 2>&1 | grep -ie "@jupyter-widgets/jupyterlab-manager.*enabled.*ok" - From 70864f9e6c61a747d54a8e004aa4dd6adf2296bd Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 9 Jun 2024 14:22:13 +1000 Subject: [PATCH 15/22] WidgetManager - do nothing if rendermime is the global rendermime instead of raising an error. --- python/jupyterlab_widgets/src/manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 9bd5ed42fb..355f00ab02 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -605,7 +605,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { manager: WidgetManager ) { if (rendermime === LabWidgetManager.globalRendermime) { - throw new Error('Using global rendermime is not permitted!'); + return; } rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); rendermime.addFactory( From 23d9b2bbda6407fc49df3d314b5ee8e97434743d Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 9 Jun 2024 14:23:31 +1000 Subject: [PATCH 16/22] Fix not awaiting delay promise in get_model. --- packages/base-manager/src/manager-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 7f578fd9ed..fb7fbcd303 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -217,7 +217,7 @@ export abstract class ManagerBase implements IWidgetManager { async get_model(model_id: string): Promise { let i = 0; while (!this._models[model_id] && i < this._sleepTimes.length) { - new Promise((resolve) => setTimeout(resolve, this._sleepTimes[i++])); + await new Promise((resolve) => setTimeout(resolve, this._sleepTimes[i++])); } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { From 66a2d3358b39b75a4f2e343760c77d5abbd31d74 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 31 Aug 2024 11:04:49 +1000 Subject: [PATCH 17/22] Update yarn.lock --- yarn.lock | 265 ++++++------------------------------------------------ 1 file changed, 26 insertions(+), 239 deletions(-) diff --git a/yarn.lock b/yarn.lock index 00cf8660f7..df5822c5b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2885,7 +2885,7 @@ __metadata: languageName: node linkType: hard -"@lumino/algorithm@npm:^1.11.1 || ^2.0.0, @lumino/algorithm@npm:^2.0.1": +"@lumino/algorithm@npm:^2.0.1": version: 2.0.1 resolution: "@lumino/algorithm@npm:2.0.1" checksum: cbf7fcf6ee6b785ea502cdfddc53d61f9d353dcb9659343511d5cd4b4030be2ff2ca4c08daec42f84417ab0318a3d9972a17319fa5231693e109ab112dcf8000 @@ -3807,7 +3807,7 @@ __metadata: languageName: node linkType: hard -"@types/backbone@npm:1.4.14": +"@types/backbone@npm:1.4.14, @types/backbone@npm:^1.4.1": version: 1.4.14 resolution: "@types/backbone@npm:1.4.14" dependencies: @@ -3817,16 +3817,6 @@ __metadata: languageName: node linkType: hard -"@types/backbone@npm:^1.4.1": - version: 1.4.19 - resolution: "@types/backbone@npm:1.4.19" - dependencies: - "@types/jquery": "*" - "@types/underscore": "*" - checksum: fdd3452c9ccba44e54eeeeeab408421ae95e0303a971a45b908f33f6b1d33721fe559e38103a40103d3767c3334b10ce6f3a85659cd19cd30f0f4f30e5f5794f - languageName: node - linkType: hard - "@types/base64-js@npm:^1.2.5": version: 1.3.2 resolution: "@types/base64-js@npm:1.3.2" @@ -3998,14 +3988,7 @@ __metadata: languageName: node linkType: hard -"@types/minimatch@npm:*": - version: 5.1.2 - resolution: "@types/minimatch@npm:5.1.2" - checksum: 0391a282860c7cb6fe262c12b99564732401bdaa5e395bee9ca323c312c1a0f45efbf34dce974682036e857db59a5c9b1da522f3d6055aeead7097264c8705a8 - languageName: node - linkType: hard - -"@types/minimatch@npm:^3.0.3": +"@types/minimatch@npm:*, @types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" checksum: c41d136f67231c3131cf1d4ca0b06687f4a322918a3a5adddc87ce90ed9dbd175a3610adee36b106ae68c0b92c637c35e02b58c8a56c424f71d30993ea220b92 @@ -4026,16 +4009,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0": - version: 20.12.12 - resolution: "@types/node@npm:20.12.12" - dependencies: - undici-types: ~5.26.4 - checksum: 5373983874b9af7c216e7ca5d26b32a8d9829c703a69f1e66f2113598b5be8582c0e009ca97369f1ec9a6282b3f92812208d06eb1e9fc3bd9b939b022303d042 - languageName: node - linkType: hard - -"@types/node@npm:^17.0.2": +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^17.0.2": version: 17.0.45 resolution: "@types/node@npm:17.0.45" checksum: aa04366b9103b7d6cfd6b2ef64182e0eaa7d4462c3f817618486ea0422984c51fc69fd0d436eae6c9e696ddfdbec9ccaa27a917f7c2e8c75c5d57827fe3d95e8 @@ -4108,16 +4082,7 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:*": - version: 17.0.3 - resolution: "@types/sinon@npm:17.0.3" - dependencies: - "@types/sinonjs__fake-timers": "*" - checksum: c8e9956d9c90fe1ec1cc43085ae48897f93f9ea86e909ab47f255ea71f5229651faa070393950fb6923aef426c84e92b375503f9f8886ef44668b82a8ee49e9a - languageName: node - linkType: hard - -"@types/sinon@npm:^10.0.2": +"@types/sinon@npm:*, @types/sinon@npm:^10.0.2": version: 10.0.20 resolution: "@types/sinon@npm:10.0.20" dependencies: @@ -5048,7 +5013,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:8.12.0": +"ajv@npm:8.12.0, ajv@npm:^8.0.0, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.9.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -5084,32 +5049,13 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.12.0, ajv@npm:^8.6.0, ajv@npm:^8.9.0": - version: 8.13.0 - resolution: "ajv@npm:8.13.0" - dependencies: - fast-deep-equal: ^3.1.3 - json-schema-traverse: ^1.0.0 - require-from-string: ^2.0.2 - uri-js: ^4.4.1 - checksum: 6de82d0b2073e645ca3300561356ddda0234f39b35d2125a8700b650509b296f41c00ab69f53178bbe25ad688bd6ac3747ab44101f2f4bd245952e8fd6ccc3c1 - languageName: node - linkType: hard - -"ansi-colors@npm:4.1.1": +"ansi-colors@npm:4.1.1, ansi-colors@npm:^4.1.1": version: 4.1.1 resolution: "ansi-colors@npm:4.1.1" checksum: 138d04a51076cb085da0a7e2d000c5c0bb09f6e772ed5c65c53cb118d37f6c5f1637506d7155fb5f330f0abcf6f12fa2e489ac3f8cdab9da393bf1bb4f9a32b0 languageName: node linkType: hard -"ansi-colors@npm:^4.1.1": - version: 4.1.3 - resolution: "ansi-colors@npm:4.1.3" - checksum: a9c2ec842038a1fabc7db9ece7d3177e2fe1c5dc6f0c51ecfbf5f39911427b89c00b5dc6b8bd95f82a26e9b16aaae2e83d45f060e98070ce4d1333038edceb0e - languageName: node - linkType: hard - "ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -6047,7 +5993,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.5.3": +"chokidar@npm:3.5.3, chokidar@npm:^3.3.0, chokidar@npm:^3.5.1": version: 3.5.3 resolution: "chokidar@npm:3.5.3" dependencies: @@ -6066,25 +6012,6 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.3.0, chokidar@npm:^3.5.1": - version: 3.6.0 - resolution: "chokidar@npm:3.6.0" - dependencies: - anymatch: ~3.1.2 - braces: ~3.0.2 - fsevents: ~2.3.2 - glob-parent: ~5.1.2 - is-binary-path: ~2.1.0 - is-glob: ~4.0.1 - normalize-path: ~3.0.0 - readdirp: ~3.6.0 - dependenciesMeta: - fsevents: - optional: true - checksum: d2f29f499705dcd4f6f3bbed79a9ce2388cf530460122eed3b9c48efeab7a4e28739c6551fd15bec9245c6b9eeca7a32baa64694d64d9b6faeb74ddb8c4a413d - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -6129,20 +6056,13 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:2.6.1": +"cli-spinners@npm:2.6.1, cli-spinners@npm:^2.5.0": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" checksum: 423409baaa7a58e5104b46ca1745fbfc5888bbd0b0c5a626e052ae1387060839c8efd512fb127e25769b3dc9562db1dc1b5add6e0b93b7ef64f477feb6416a45 languageName: node linkType: hard -"cli-spinners@npm:^2.5.0": - version: 2.9.2 - resolution: "cli-spinners@npm:2.9.2" - checksum: 1bd588289b28432e4676cb5d40505cfe3e53f2e4e10fbe05c8a710a154d6fe0ce7836844b00d6858f740f2ffe67cdc36e0fce9c7b6a8430e80e6388d5aa4956c - languageName: node - linkType: hard - "cli-truncate@npm:^2.1.0": version: 2.1.0 resolution: "cli-truncate@npm:2.1.0" @@ -6711,20 +6631,13 @@ __metadata: languageName: node linkType: hard -"core-util-is@npm:1.0.2": +"core-util-is@npm:1.0.2, core-util-is@npm:~1.0.0": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" checksum: 7a4c925b497a2c91421e25bf76d6d8190f0b2359a9200dbeed136e63b2931d6294d3b1893eda378883ed363cd950f44a12a401384c609839ea616befb7927dab languageName: node linkType: hard -"core-util-is@npm:~1.0.0": - version: 1.0.3 - resolution: "core-util-is@npm:1.0.3" - checksum: 9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99 - languageName: node - linkType: hard - "cors@npm:2.8.5, cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" @@ -6845,20 +6758,13 @@ __metadata: languageName: node linkType: hard -"csstype@npm:3.0.10": +"csstype@npm:3.0.10, csstype@npm:^3.0.2": version: 3.0.10 resolution: "csstype@npm:3.0.10" checksum: 20a8fa324f2b33ddf94aa7507d1b6ab3daa6f3cc308888dc50126585d7952f2471de69b2dbe0635d1fdc31223fef8e070842691877e725caf456e2378685a631 languageName: node linkType: hard -"csstype@npm:^3.0.2": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 8db785cc92d259102725b3c694ec0c823f5619a84741b5c7991b8ad135dfaa66093038a1cc63e03361a6cd28d122be48f2106ae72334e067dd619a51f49eddf7 - languageName: node - linkType: hard - "custom-event@npm:~1.0.0": version: 1.0.1 resolution: "custom-event@npm:1.0.1" @@ -8044,20 +7950,13 @@ __metadata: languageName: node linkType: hard -"extsprintf@npm:1.3.0": +"extsprintf@npm:1.3.0, extsprintf@npm:^1.2.0": version: 1.3.0 resolution: "extsprintf@npm:1.3.0" checksum: cee7a4a1e34cffeeec18559109de92c27517e5641991ec6bab849aa64e3081022903dd53084f2080d0d2530803aa5ee84f1e9de642c365452f9e67be8f958ce2 languageName: node linkType: hard -"extsprintf@npm:^1.2.0": - version: 1.4.1 - resolution: "extsprintf@npm:1.4.1" - checksum: a2f29b241914a8d2bad64363de684821b6b1609d06ae68d5b539e4de6b28659715b5bea94a7265201603713b7027d35399d10b0548f09071c5513e65e8323d33 - languageName: node - linkType: hard - "fast-deep-equal@npm:^1.0.0": version: 1.1.0 resolution: "fast-deep-equal@npm:1.1.0" @@ -8725,7 +8624,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:7.2.0": +"glob@npm:7.2.0, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.7": version: 7.2.0 resolution: "glob@npm:7.2.0" dependencies: @@ -8767,20 +8666,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.7": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - "glob@npm:^8.0.1": version: 8.1.0 resolution: "glob@npm:8.1.0" @@ -10260,20 +10145,13 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:3.2.0": +"jsonc-parser@npm:3.2.0, jsonc-parser@npm:^3.2.0": version: 3.2.0 resolution: "jsonc-parser@npm:3.2.0" checksum: 946dd9a5f326b745aa326d48a7257e3f4a4b62c5e98ec8e49fa2bdd8d96cef7e6febf1399f5c7016114fd1f68a1c62c6138826d5d90bc650448e3cf0951c53c7 languageName: node linkType: hard -"jsonc-parser@npm:^3.2.0": - version: 3.2.1 - resolution: "jsonc-parser@npm:3.2.1" - checksum: 656d9027b91de98d8ab91b3aa0d0a4cab7dc798a6830845ca664f3e76c82d46b973675bbe9b500fae1de37fd3e81aceacbaa2a57884bf2f8f29192150d2d1ef7 - languageName: node - linkType: hard - "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -10667,20 +10545,13 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:2.0.5": +"lilconfig@npm:2.0.5, lilconfig@npm:^2.0.5": version: 2.0.5 resolution: "lilconfig@npm:2.0.5" checksum: f7bb9e42656f06930ad04e583026f087508ae408d3526b8b54895e934eb2a966b7aafae569656f2c79a29fe6d779b3ec44ba577e80814734c8655d6f71cdf2d1 languageName: node linkType: hard -"lilconfig@npm:^2.0.5": - version: 2.1.0 - resolution: "lilconfig@npm:2.1.0" - checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117 - languageName: node - linkType: hard - "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -11365,7 +11236,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:2 || 3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:2 || 3, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -11374,7 +11245,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.0.5": +"minimatch@npm:3.0.5, minimatch@npm:~3.0.4": version: 3.0.5 resolution: "minimatch@npm:3.0.5" dependencies: @@ -11419,15 +11290,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:~3.0.4": - version: 3.0.8 - resolution: "minimatch@npm:3.0.8" - dependencies: - brace-expansion: ^1.1.7 - checksum: 850cca179cad715133132693e6963b0db64ab0988c4d211415b087fc23a3e46321e2c5376a01bf5623d8782aba8bdf43c571e2e902e51fdce7175c7215c29f8b - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -13698,7 +13560,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.11.0": +"qs@npm:6.11.0, qs@npm:^6.4.0": version: 6.11.0 resolution: "qs@npm:6.11.0" dependencies: @@ -13707,15 +13569,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.4.0": - version: 6.12.1 - resolution: "qs@npm:6.12.1" - dependencies: - side-channel: ^1.0.6 - checksum: aa761d99e65b6936ba2dd2187f2d9976afbcda38deb3ff1b3fe331d09b0c578ed79ca2abdde1271164b5be619c521ec7db9b34c23f49a074e5921372d16242d5 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -14385,21 +14238,7 @@ __metadata: languageName: node linkType: hard -"sanitize-html@npm:^2.3": - version: 2.13.0 - resolution: "sanitize-html@npm:2.13.0" - dependencies: - deepmerge: ^4.2.2 - escape-string-regexp: ^4.0.0 - htmlparser2: ^8.0.0 - is-plain-object: ^5.0.0 - parse-srcset: ^1.0.2 - postcss: ^8.3.11 - checksum: d88602328306dbbddb9c5e2a5798783a3b38977a7ef40bf81dae31220d7fb583149c1046a33ec6817e9d96d172b1aaa9ea159776eb1ee08f6a0571150114c9bf - languageName: node - linkType: hard - -"sanitize-html@npm:~2.12.1": +"sanitize-html@npm:^2.3, sanitize-html@npm:~2.12.1": version: 2.12.1 resolution: "sanitize-html@npm:2.12.1" dependencies: @@ -14679,7 +14518,7 @@ __metadata: languageName: node linkType: hard -"side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": +"side-channel@npm:^1.0.4": version: 1.0.6 resolution: "side-channel@npm:1.0.6" dependencies: @@ -14858,7 +14697,7 @@ __metadata: languageName: node linkType: hard -"sonic-boom@npm:3.8.0": +"sonic-boom@npm:3.8.0, sonic-boom@npm:^3.7.0": version: 3.8.0 resolution: "sonic-boom@npm:3.8.0" dependencies: @@ -14876,15 +14715,6 @@ __metadata: languageName: node linkType: hard -"sonic-boom@npm:^3.7.0": - version: 3.8.1 - resolution: "sonic-boom@npm:3.8.1" - dependencies: - atomic-sleep: ^1.0.0 - checksum: 79c90d7a2f928489fd3d4b68d8f8d747a426ca6ccf83c3b102b36f899d4524463dd310982ab7ab6d6bcfd34b7c7c281ad25e495ad71fbff8fd6fa86d6273fc6b - languageName: node - linkType: hard - "sort-keys@npm:^2.0.0": version: 2.0.0 resolution: "sort-keys@npm:2.0.0" @@ -15953,27 +15783,13 @@ __metadata: languageName: node linkType: hard -"underscore@npm:>=1.7.0, underscore@npm:^1.8.3": +"underscore@npm:>=1.7.0, underscore@npm:>=1.8.3, underscore@npm:^1.8.3": version: 1.13.7 resolution: "underscore@npm:1.13.7" checksum: 174b011af29e4fbe2c70eb2baa8bfab0d0336cf2f5654f364484967bc6264a86224d0134b9176e4235c8cceae00d11839f0fd4824268de04b11c78aca1241684 languageName: node linkType: hard -"underscore@npm:>=1.8.3": - version: 1.13.6 - resolution: "underscore@npm:1.13.6" - checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 - languageName: node - linkType: hard - -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 - languageName: node - linkType: hard - "union@npm:~0.5.0": version: 0.5.0 resolution: "union@npm:0.5.0" @@ -16099,7 +15915,7 @@ __metadata: languageName: node linkType: hard -"uri-js@npm:^4.2.2, uri-js@npm:^4.4.1": +"uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" dependencies: @@ -16349,7 +16165,7 @@ __metadata: languageName: node linkType: hard -"vscode-jsonrpc@npm:8.2.0": +"vscode-jsonrpc@npm:8.2.0, vscode-jsonrpc@npm:^8.0.2": version: 8.2.0 resolution: "vscode-jsonrpc@npm:8.2.0" checksum: f302a01e59272adc1ae6494581fa31c15499f9278df76366e3b97b2236c7c53ebfc71efbace9041cfd2caa7f91675b9e56f2407871a1b3c7f760a2e2ee61484a @@ -16363,13 +16179,6 @@ __metadata: languageName: node linkType: hard -"vscode-jsonrpc@npm:^8.0.2": - version: 8.2.1 - resolution: "vscode-jsonrpc@npm:8.2.1" - checksum: 2af2c333d73f6587896a7077978b8d4b430e55c674d5dbb90597a84a6647057c1655a3bff398a9b08f1f8ba57dbd2deabf05164315829c297b0debba3b8bc19e - languageName: node - linkType: hard - "vscode-languageserver-protocol@npm:^3.17.0": version: 3.17.5 resolution: "vscode-languageserver-protocol@npm:3.17.5" @@ -16879,22 +16688,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0": - version: 8.17.0 - resolution: "ws@npm:8.17.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e - languageName: node - linkType: hard - -"ws@npm:~8.11.0": +"ws@npm:^8.11.0, ws@npm:~8.11.0": version: 8.11.0 resolution: "ws@npm:8.11.0" peerDependencies: @@ -16955,7 +16749,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:20.2.4": +"yargs-parser@npm:20.2.4, yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": version: 20.2.4 resolution: "yargs-parser@npm:20.2.4" checksum: d251998a374b2743a20271c2fd752b9fbef24eb881d53a3b99a7caa5e8227fcafd9abf1f345ac5de46435821be25ec12189a11030c12ee6481fef6863ed8b924 @@ -16969,13 +16763,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": - version: 20.2.9 - resolution: "yargs-parser@npm:20.2.9" - checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 - languageName: node - linkType: hard - "yargs-unparser@npm:2.0.0": version: 2.0.0 resolution: "yargs-unparser@npm:2.0.0" From 63e47d37abf96fff98564ffffab8f62de7e97e9f Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 8 Sep 2024 10:39:36 +1000 Subject: [PATCH 18/22] Add kernel monitoring. --- python/jupyterlab_widgets/src/manager.ts | 8 ++++++++ python/jupyterlab_widgets/src/plugin.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 355f00ab02..8c5fc9840f 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -112,6 +112,9 @@ export abstract class LabWidgetManager super.disconnect(); this._restoredStatus = false; } + get disconnected() { + return !this._restoredStatus; + } protected async _loadFromKernel(): Promise { if (!this.kernel) { @@ -471,6 +474,11 @@ export class KernelWidgetManager extends LabWidgetManager { } } + static existsWithActiveKenel(id: string) { + const widgetManager = Private.kernelWidgetManagers.get(id); + return !widgetManager?.disconnected; + } + _handleKernelStatusChange( sender: Kernel.IKernelConnection, status: Kernel.Status diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index 932d564338..e816c6f38b 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -25,6 +25,7 @@ import { DisposableDelegate } from '@lumino/disposable'; import { WidgetRenderer } from './renderer'; import { + KernelWidgetManager, LabWidgetManager, WIDGET_VIEW_MIMETYPE, WidgetManager, @@ -103,6 +104,22 @@ function activateWidgetExtension( const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab_widgets'); + app.serviceManager.kernels.runningChanged.connect((models) => { + for (const model of models.running()) { + if ( + model && + model.name === 'python3' && + model.execution_state !== 'starting' && + !KernelWidgetManager.existsWithActiveKenel(model.id) + ) { + const kernel = app.serviceManager.kernels.connectTo({ model: model }); + if (kernel.handleComms) { + new KernelWidgetManager(kernel); + } + } + } + }); + if (settingRegistry !== null) { settingRegistry .load(managerPlugin.id) From 29da2bdfac03e55c52501078db2dc038c0bbc3f0 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Wed, 25 Sep 2024 13:02:41 +1000 Subject: [PATCH 19/22] Changed getWidgetManager to be a static method of KernelWidgetManager and renamed to getManager. --- packages/base-manager/src/manager-base.ts | 9 ++--- python/jupyterlab_widgets/src/index.ts | 6 +-- python/jupyterlab_widgets/src/manager.ts | 48 ++++++++++++----------- python/jupyterlab_widgets/src/renderer.ts | 23 ++++++----- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index 0f9f416f30..a5a3c85fd4 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -214,12 +214,10 @@ 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, delays = [500]): Promise { let i = 0; - while (!this._models[model_id] && i < this._sleepTimes.length) { - await new Promise((resolve) => - setTimeout(resolve, this._sleepTimes[i++]) - ); + while (!this._models[model_id] && i < delays.length) { + await new Promise((resolve) => setTimeout(resolve, delays[i++])); } const modelPromise = this._models[model_id]; if (modelPromise === undefined) { @@ -875,7 +873,6 @@ export abstract class ManagerBase implements IWidgetManager { /** * Dictionary of model ids and model instance promises */ - private _sleepTimes = [2, 50, 200, 800]; private _models: { [key: string]: Promise } = Object.create(null); } diff --git a/python/jupyterlab_widgets/src/index.ts b/python/jupyterlab_widgets/src/index.ts index eb40b5522f..e8e25c3af0 100644 --- a/python/jupyterlab_widgets/src/index.ts +++ b/python/jupyterlab_widgets/src/index.ts @@ -8,11 +8,7 @@ export default WidgetManagerProvider; export { registerWidgetManager } from './plugin'; -export { - KernelWidgetManager, - LabWidgetManager, - WidgetManager, -} from './manager'; +export { KernelWidgetManager, WidgetManager } from './manager'; export { WidgetRenderer } from './renderer'; diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 8c5fc9840f..8b591a0ba1 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -367,7 +367,7 @@ export class KernelWidgetManager extends LabWidgetManager { rendermime?: IRenderMimeRegistry, pendingManagerMessage = 'Loading widget ...' ) { - const instance = Private.kernelWidgetManagers.get(kernel.id); + const instance = Private.managers.get(kernel.id); if (instance) { instance._useKernel(kernel); KernelWidgetManager.configureRendermime( @@ -381,7 +381,7 @@ export class KernelWidgetManager extends LabWidgetManager { throw new Error('Kernel does not have handleComms enabled'); } super(LabWidgetManager.globalRendermime); - Private.kernelWidgetManagers.set(kernel.id, this); + Private.managers.set(kernel.id, this); this.loadCustomWidgetDefinitions(); LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => this.loadCustomWidgetDefinitions() @@ -475,10 +475,30 @@ export class KernelWidgetManager extends LabWidgetManager { } static existsWithActiveKenel(id: string) { - const widgetManager = Private.kernelWidgetManagers.get(id); + const widgetManager = Private.managers.get(id); return !widgetManager?.disconnected; } + /** + * Get the KernelWidgetManager that owns the model. + */ + static async getManager( + model_id: string, + delays = [100, 1000] + ): Promise { + for (const sleepTime of delays) { + for (const wManager of Private.managers.values()) { + if (wManager.has_model(model_id)) { + return wManager; + } + } + await new Promise((resolve) => setTimeout(resolve, sleepTime)); + } + throw new Error( + `Failed to locate the KernelWidgetManager for model_id='${model_id}'` + ); + } + _handleKernelStatusChange( sender: Kernel.IKernelConnection, status: Kernel.Status @@ -516,7 +536,7 @@ export class KernelWidgetManager extends LabWidgetManager { } super.dispose(); KernelWidgetManager.configureRendermime(this.rendermime); - Private.kernelWidgetManagers.delete(this.kernel.id); + Private.managers.delete(this.kernel.id); this._handleKernelChanged({ name: 'kernel', oldValue: this._kernel, @@ -829,28 +849,12 @@ export namespace WidgetManager { }; } -/** - * Get the widgetManager that owns the model. - */ -export async function getWidgetManager( - model_id: string -): Promise { - for (const sleepTime of [0, 50, 1000]) { - await new Promise((resolve) => setTimeout(resolve, sleepTime)); - for (const wManager of Private.kernelWidgetManagers.values()) { - if (wManager.has_model(model_id)) { - return wManager; - } - } - } - return undefined; -} - /** * A namespace for private data */ namespace Private { - export const kernelWidgetManagers = new Map(); + export const managers = new Map(); + export const widgetManagerProperty = new AttachedProperty< DocumentRegistry.Context, WidgetManager | undefined diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index 0a2eb29f7b..b510ae9346 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -11,7 +11,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { DOMWidgetModel } from '@jupyter-widgets/base'; -import { LabWidgetManager, getWidgetManager } from './manager'; +import { KernelWidgetManager } from './manager'; /** * A renderer for widgets. @@ -35,7 +35,7 @@ export class WidgetRenderer { constructor( options: IRenderMime.IRendererOptions, - manager?: LabWidgetManager, + manager?: KernelWidgetManager, pendingManagerMessage = '' ) { super(); @@ -50,7 +50,7 @@ export class WidgetRenderer * Will accept the first non-null manager and ignore anything afterwards. */ - set manager(value: LabWidgetManager | undefined) { + set manager(value: KernelWidgetManager | undefined) { if (value && !this._managerIsSet) { // Can only set the manager once this._manager.resolve(value); @@ -68,15 +68,20 @@ export class WidgetRenderer // If there is no model id, the view was removed, so hide the node. if (source.model_id === '') { this.hide(); - return Promise.resolve(); + return; } if (!this._pendingManagerMessage && !this._managerIsSet) { - this.manager = await getWidgetManager(source.model_id); + try { + this.manager = await KernelWidgetManager.getManager(source.model_id); + } catch { + this.node.textContent = `KernelWidgetManager not found for model: ${model.data['text/plain']}`; + return; + } } this.node.textContent = `${ this._pendingManagerMessage || model.data['text/plain'] }`; - const manager: LabWidgetManager = await this._manager.promise; + const manager = await this._manager.promise; this._rerenderMimeModel = model; let wModel: DOMWidgetModel; @@ -85,7 +90,7 @@ export class WidgetRenderer wModel = (await manager.get_model(source.model_id)) as DOMWidgetModel; } catch (err) { if (this._pendingManagerMessage === 'No kernel') { - this.node.textContent = `Model not found for this kernel: ${model.data['text/plain']}`; + this.node.textContent = `Model not found: ${model.data['text/plain']}`; } else if (manager.restoredStatus) { // The manager has been restored, so this error won't be going away. this.node.textContent = 'Error displaying widget: model not found'; @@ -93,7 +98,7 @@ export class WidgetRenderer console.error(err); } // Store the model for a possible rerender - return Promise.resolve(); + return; } let widget: LuminoWidget; try { @@ -146,7 +151,7 @@ export class WidgetRenderer * The mimetype being rendered. */ readonly mimeType: string; - private _manager = new PromiseDelegate(); + private _manager = new PromiseDelegate(); private _managerIsSet = false; private _pendingManagerMessage: string; private _rerenderMimeModel: IRenderMime.IMimeModel | null = null; From 43dfd3f4b92f7cbc6e92d52b6f433c5eef490978 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 2 Nov 2024 17:35:31 +1100 Subject: [PATCH 20/22] Tweak restoration --- python/jupyterlab_widgets/src/manager.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 8b591a0ba1..4919a447d1 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -112,9 +112,6 @@ export abstract class LabWidgetManager super.disconnect(); this._restoredStatus = false; } - get disconnected() { - return !this._restoredStatus; - } protected async _loadFromKernel(): Promise { if (!this.kernel) { @@ -419,9 +416,7 @@ export class KernelWidgetManager extends LabWidgetManager { this._handleKernelConnectionStatusChange, this ); - this._restoredStatus = false; - this._kernelRestoreInProgress = true; - this.clear_state().then(() => this.restoreWidgets()); + this.restoreWidgets(); } /** @@ -476,7 +471,7 @@ export class KernelWidgetManager extends LabWidgetManager { static existsWithActiveKenel(id: string) { const widgetManager = Private.managers.get(id); - return !widgetManager?.disconnected; + return widgetManager?._restoredStatus; } /** @@ -515,7 +510,10 @@ export class KernelWidgetManager extends LabWidgetManager { * Restore widgets from kernel. */ async restoreWidgets(): Promise { + this._restoredStatus = false; + this._kernelRestoreInProgress = true; try { + await this.clear_state(); await this._loadFromKernel(); } catch (err) { // Do nothing From 86b5b0ea2ad8f4c0522e5f993f6cca9479880a82 Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sun, 3 Nov 2024 20:33:28 +1100 Subject: [PATCH 21/22] Improved KernelWidgetManager creation and kernel restoration. --- python/jupyterlab_widgets/src/manager.ts | 137 +++++++++++----------- python/jupyterlab_widgets/src/output.ts | 6 +- python/jupyterlab_widgets/src/plugin.ts | 23 +--- python/jupyterlab_widgets/src/renderer.ts | 2 +- 4 files changed, 72 insertions(+), 96 deletions(-) diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 4919a447d1..8708430f9b 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -66,15 +66,7 @@ export const WIDGET_STATE_MIMETYPE = /** * A widget manager that returns Lumino widgets. */ -export abstract class LabWidgetManager - extends ManagerBase - implements IDisposable -{ - constructor(rendermime: IRenderMimeRegistry) { - super(); - this._rendermime = rendermime; - } - +abstract class LabWidgetManager extends ManagerBase implements IDisposable { /** * Default callback handler to emit unhandled kernel messages. */ @@ -181,7 +173,6 @@ export abstract class LabWidgetManager return; } this._isDisposed = true; - this._rendermime = null!; if (this._commRegistration) { this._commRegistration.dispose(); @@ -245,10 +236,6 @@ export abstract class LabWidgetManager abstract get kernel(): Kernel.IKernelConnection | null; - get rendermime(): IRenderMimeRegistry { - return this._rendermime; - } - /** * A signal emitted when state is restored to the widget manager. * @@ -298,6 +285,7 @@ export abstract class LabWidgetManager * @return Promise that resolves when the widget state is cleared. */ async clear_state(): Promise { + this._restoredStatus = false; await super.clear_state(); this._modelsSync = new Map(); } @@ -331,15 +319,13 @@ export abstract class LabWidgetManager await this.handle_comm_open(oldComm, msg); }; - static globalRendermime: IRenderMimeRegistry; + static rendermime: IRenderMimeRegistry; protected _restored = new Signal(this); protected _restoredStatus = false; - protected _kernelRestoreInProgress = false; private _isDisposed = false; private _registry: SemVerCache = new SemVerCache(); - private _rendermime: IRenderMimeRegistry; private _commRegistration: IDisposable; @@ -352,46 +338,31 @@ export abstract class LabWidgetManager } /** - * A singleton widget manager per kernel for the lifecycle of the kernel. - * The factory of the rendermime will be updated to use the widgetManager - * directly if it isn't the globalRendermime. - * - * Note: The rendermime of the instance is always the global rendermime. + * KernelWidgetManager is singleton widget manager per kernel.id. + * This class should not be created directly or subclassed, instead use + * the class method `KernelWidgetManager.getManager(kernel)`. */ export class KernelWidgetManager extends LabWidgetManager { - constructor( - kernel: Kernel.IKernelConnection, - rendermime?: IRenderMimeRegistry, - pendingManagerMessage = 'Loading widget ...' - ) { - const instance = Private.managers.get(kernel.id); - if (instance) { - instance._useKernel(kernel); - KernelWidgetManager.configureRendermime( - rendermime, - instance, - pendingManagerMessage - ); - return instance; + constructor(kernel: Kernel.IKernelConnection) { + if (Private.managers.has(kernel.id)) { + throw new Error('A manager already exists!'); } if (!kernel.handleComms) { throw new Error('Kernel does not have handleComms enabled'); } - super(LabWidgetManager.globalRendermime); + super(); Private.managers.set(kernel.id, this); this.loadCustomWidgetDefinitions(); LabWidgetManager.WIDGET_REGISTRY.changed.connect(() => this.loadCustomWidgetDefinitions() ); - this._useKernel(kernel); - KernelWidgetManager.configureRendermime( - rendermime, - this, - pendingManagerMessage - ); + this._updateKernel(kernel); } - _useKernel(this: KernelWidgetManager, kernel: Kernel.IKernelConnection) { + private _updateKernel( + this: KernelWidgetManager, + kernel: Kernel.IKernelConnection + ) { if (!kernel.handleComms || this._kernel === kernel) { return; } @@ -409,13 +380,15 @@ export class KernelWidgetManager extends LabWidgetManager { this._handleKernelConnectionStatusChange, this ); + this._kernel.disposed.disconnect(this._onKernelDisposed, this); } this._kernel = kernel; - this._kernel.statusChanged.connect(this._handleKernelStatusChange, this); - this._kernel.connectionStatusChanged.connect( + kernel.statusChanged.connect(this._handleKernelStatusChange, this); + kernel.connectionStatusChanged.connect( this._handleKernelConnectionStatusChange, this ); + kernel.disposed.connect(this._onKernelDisposed, this); this.restoreWidgets(); } @@ -437,7 +410,7 @@ export class KernelWidgetManager extends LabWidgetManager { manager?: KernelWidgetManager, pendingManagerMessage = '' ) { - if (!rendermime || rendermime === LabWidgetManager.globalRendermime) { + if (!rendermime || rendermime === LabWidgetManager.rendermime) { return; } rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); @@ -457,11 +430,7 @@ export class KernelWidgetManager extends LabWidgetManager { ): 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(); - } + this.restoreWidgets(); break; case 'disconnected': this.disconnect(); @@ -469,15 +438,10 @@ export class KernelWidgetManager extends LabWidgetManager { } } - static existsWithActiveKenel(id: string) { - const widgetManager = Private.managers.get(id); - return widgetManager?._restoredStatus; - } - /** - * Get the KernelWidgetManager that owns the model. + * Find the KernelWidgetManager that owns the model. */ - static async getManager( + static async findManager( model_id: string, delays = [100, 1000] ): Promise { @@ -494,6 +458,28 @@ export class KernelWidgetManager extends LabWidgetManager { ); } + /** + * The correct way to get a KernelWidgetManager + * @param kernel IKernelConnection + * @returns + */ + static async getManager( + kernel: Kernel.IKernelConnection + ): Promise { + let manager = Private.managers.get(kernel.id); + if (!manager) { + manager = new KernelWidgetManager(kernel); + } + if (kernel.handleComms) { + manager._updateKernel(kernel); + if (!manager.restoredStatus) { + const restored = manager.restored; + await new Promise((resolve) => restored.connect(resolve)); + } + } + return manager; + } + _handleKernelStatusChange( sender: Kernel.IKernelConnection, status: Kernel.Status @@ -506,23 +492,36 @@ export class KernelWidgetManager extends LabWidgetManager { } } + async _onKernelDisposed() { + const model = await KernelWidgetManager.kernels.findById(this.kernel?.id); + if (model) { + const kernel = KernelWidgetManager.kernels.connectTo({ model }); + this._updateKernel(kernel); + } + } + /** * Restore widgets from kernel. */ async restoreWidgets(): Promise { + if (this._kernelRestoreInProgress) { + return; + } this._restoredStatus = false; this._kernelRestoreInProgress = true; try { await this.clear_state(); await this._loadFromKernel(); - } catch (err) { - // Do nothing + } catch { + /* empty */ + } finally { + this._restoredStatus = true; + this._kernelRestoreInProgress = false; + this.triggerRestored(); } - this.triggerRestored(); } triggerRestored() { - this._restoredStatus = true; this._restored.emit(); } /** @@ -533,7 +532,6 @@ export class KernelWidgetManager extends LabWidgetManager { return; } super.dispose(); - KernelWidgetManager.configureRendermime(this.rendermime); Private.managers.delete(this.kernel.id); this._handleKernelChanged({ name: 'kernel', @@ -557,9 +555,9 @@ export class KernelWidgetManager extends LabWidgetManager { filterModelState(serialized_state: any): any { return this.filterExistingModelState(serialized_state); } - + static kernels: Kernel.IManager; private _kernel: Kernel.IKernelConnection; - protected _kernelRestoreInProgress = false; + private _kernelRestoreInProgress = false; } /** @@ -630,7 +628,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { rendermime: IRenderMimeRegistry, manager: WidgetManager ) { - if (rendermime === LabWidgetManager.globalRendermime) { + if (rendermime === LabWidgetManager.rendermime) { return; } rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE); @@ -648,7 +646,7 @@ export class WidgetManager extends Backbone.Model implements IDisposable { let wManager: KernelWidgetManager | undefined; await this.context.sessionContext.ready; if (this.kernel) { - wManager = new KernelWidgetManager(this.kernel); + wManager = await KernelWidgetManager.getManager(this.kernel); } if (wManager === this._widgetManager) { return; @@ -796,7 +794,6 @@ export class WidgetManager extends Backbone.Model implements IDisposable { this._renderers = null!; this._context = null!; this._context = null!; - this._rendermime = null!; this._settings = null!; } @@ -831,8 +828,6 @@ export class WidgetManager extends Backbone.Model implements IDisposable { } } static loggerRegistry: ILoggerRegistry | null; - // protected _restored = new Signal(this); - // protected _restoredStatus = false; private _isDisposed = false; private _context: DocumentRegistry.Context; private _rendermime: IRenderMimeRegistry; diff --git a/python/jupyterlab_widgets/src/output.ts b/python/jupyterlab_widgets/src/output.ts index ee559437da..a71032defe 100644 --- a/python/jupyterlab_widgets/src/output.ts +++ b/python/jupyterlab_widgets/src/output.ts @@ -9,7 +9,7 @@ import { Panel } from '@lumino/widgets'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { LabWidgetManager } from './manager'; +import { KernelWidgetManager } from './manager'; import { OutputAreaModel, OutputArea } from '@jupyterlab/outputarea'; @@ -44,7 +44,7 @@ export class OutputModel extends outputBase.OutputModel { * Reset the message id. */ reset_msg_id(): void { - const kernel = this.widget_manager.kernel; + const kernel = (this.widget_manager as KernelWidgetManager).kernel; const msgId = this.get('msg_id'); const oldMsgId = this.previous('msg_id'); @@ -98,8 +98,6 @@ export class OutputModel extends outputBase.OutputModel { } } - widget_manager: LabWidgetManager; - private _msgHook: (msg: KernelMessage.IIOPubMessage) => boolean; private _outputs: OutputAreaModel; static rendermime: IRenderMimeRegistry; diff --git a/python/jupyterlab_widgets/src/plugin.ts b/python/jupyterlab_widgets/src/plugin.ts index e816c6f38b..f0805871fc 100644 --- a/python/jupyterlab_widgets/src/plugin.ts +++ b/python/jupyterlab_widgets/src/plugin.ts @@ -26,7 +26,6 @@ import { WidgetRenderer } from './renderer'; import { KernelWidgetManager, - LabWidgetManager, WIDGET_VIEW_MIMETYPE, WidgetManager, } from './manager'; @@ -103,23 +102,7 @@ function activateWidgetExtension( ): base.IJupyterWidgetRegistry { const { commands } = app; const trans = (translator ?? nullTranslator).load('jupyterlab_widgets'); - - app.serviceManager.kernels.runningChanged.connect((models) => { - for (const model of models.running()) { - if ( - model && - model.name === 'python3' && - model.execution_state !== 'starting' && - !KernelWidgetManager.existsWithActiveKenel(model.id) - ) { - const kernel = app.serviceManager.kernels.connectTo({ model: model }); - if (kernel.handleComms) { - new KernelWidgetManager(kernel); - } - } - } - }); - + KernelWidgetManager.kernels = app.serviceManager.kernels; if (settingRegistry !== null) { settingRegistry .load(managerPlugin.id) @@ -132,7 +115,7 @@ function activateWidgetExtension( }); } WidgetManager.loggerRegistry = loggerRegistry; - LabWidgetManager.globalRendermime = rendermime; + KernelWidgetManager.rendermime = rendermime; // Add a default widget renderer. rendermime.addFactory( { @@ -175,7 +158,7 @@ function activateWidgetExtension( return { registerWidget(data: base.IWidgetRegistryData): void { - LabWidgetManager.WIDGET_REGISTRY.push(data); + KernelWidgetManager.WIDGET_REGISTRY.push(data); }, }; } diff --git a/python/jupyterlab_widgets/src/renderer.ts b/python/jupyterlab_widgets/src/renderer.ts index b510ae9346..258b11b5ea 100644 --- a/python/jupyterlab_widgets/src/renderer.ts +++ b/python/jupyterlab_widgets/src/renderer.ts @@ -72,7 +72,7 @@ export class WidgetRenderer } if (!this._pendingManagerMessage && !this._managerIsSet) { try { - this.manager = await KernelWidgetManager.getManager(source.model_id); + this.manager = await KernelWidgetManager.findManager(source.model_id); } catch { this.node.textContent = `KernelWidgetManager not found for model: ${model.data['text/plain']}`; return; From fb8adbe92cada8b9aa27a69ed5cc54dbe687476d Mon Sep 17 00:00:00 2001 From: Alan Fleming <> Date: Sat, 9 Nov 2024 09:43:01 +1100 Subject: [PATCH 22/22] Create new comm for existing models when restoring from kernel. --- packages/base-manager/src/manager-base.ts | 7 ++- packages/base/src/errorwidget.ts | 4 +- packages/base/src/widget.ts | 56 ++++++++++------------- packages/base/test/src/widget_test.ts | 14 +++--- python/jupyterlab_widgets/src/manager.ts | 4 +- 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/packages/base-manager/src/manager-base.ts b/packages/base-manager/src/manager-base.ts index a5a3c85fd4..337820b912 100644 --- a/packages/base-manager/src/manager-base.ts +++ b/packages/base-manager/src/manager-base.ts @@ -499,6 +499,9 @@ export abstract class ManagerBase implements IWidgetManager { model.constructor as typeof WidgetModel )._deserialize_state(state.state, this); model!.set_state(deserializedState); + if (!model.comm_live) { + model.comm = await this._create_comm('jupyter.widget', widget_id); + } } } catch (error) { // Failed to create a widget model, we continue creating other models so that @@ -750,12 +753,12 @@ export abstract class ManagerBase implements IWidgetManager { /** * Disconnect the widget manager from the kernel, setting each model's comm - * as dead. + * as undefined. */ disconnect(): void { Object.keys(this._models).forEach((i) => { this._models[i].then((model) => { - model.comm_live = false; + model.comm = undefined; }); }); } diff --git a/packages/base/src/errorwidget.ts b/packages/base/src/errorwidget.ts index 1e6a837c93..818466acdf 100644 --- a/packages/base/src/errorwidget.ts +++ b/packages/base/src/errorwidget.ts @@ -24,7 +24,9 @@ export function createErrorWidgetModel( error: error, }; super(attributes, options); - this.comm_live = true; + } + get comm_live(): boolean { + return true; } } return ErrorWidget; diff --git a/packages/base/src/widget.ts b/packages/base/src/widget.ts index 2a5118a33a..03112e1cc6 100644 --- a/packages/base/src/widget.ts +++ b/packages/base/src/widget.ts @@ -160,7 +160,7 @@ export class WidgetModel extends Backbone.Model { // Attributes should be initialized here, since user initialization may depend on it this.widget_manager = options.widget_manager; this.model_id = options.model_id; - const comm = options.comm; + this.comm = options.comm; this.views = Object.create(null); this.state_change = Promise.resolve(); @@ -174,27 +174,23 @@ export class WidgetModel extends Backbone.Model { // _buffered_state_diff must be created *after* the super.initialize // call above. See the note in the set() method below. this._buffered_state_diff = {}; + } - if (comm) { - // Remember comm associated with the model. - this.comm = comm; + get comm() { + return this._comm; + } - // Hook comm messages up to model. + set comm(comm: IClassicComm | undefined) { + this._comm = comm; + if (comm) { comm.on_close(this._handle_comm_closed.bind(this)); comm.on_msg(this._handle_comm_msg.bind(this)); - - this.comm_live = true; - } else { - this.comm_live = false; } + this.trigger('comm_live_update'); } - get comm_live(): boolean { - return this._comm_live; - } - set comm_live(x) { - this._comm_live = x; - this.trigger('comm_live_update'); + get comm_live() { + return Boolean(this.comm); } /** @@ -218,42 +214,42 @@ export class WidgetModel extends Backbone.Model { * * @returns - a promise that is fulfilled when all the associated views have been removed. */ - close(comm_closed = false): Promise { + async close(comm_closed = false): Promise { // can only be closed once. if (this._closed) { - return Promise.resolve(); + return; } this._closed = true; - if (this.comm && !comm_closed && this.comm_live) { + if (this._comm && !comm_closed) { try { - this.comm.close(); + this._comm.close(); } catch (err) { // Do Nothing } } this.stopListening(); this.trigger('destroy', this); - if (this.comm) { - delete this.comm; - } + delete this._comm; + // Delete all views of this model if (this.views) { const views = Object.keys(this.views).map((id: string) => { return this.views![id].then((view) => view.remove()); }); delete this.views; - return Promise.all(views).then(() => { - return; - }); + await Promise.all(views); + return; } - return Promise.resolve(); } /** * Handle when a widget comm is closed. */ _handle_comm_closed(msg: KernelMessage.ICommCloseMsg): void { - this.comm_live = false; + if (!this.comm) { + return; + } + this.comm = undefined; this.trigger('comm:close'); if (!this._closed) { this.close(true); @@ -642,7 +638,7 @@ export class WidgetModel extends Backbone.Model { * This invokes a Backbone.Sync. */ save_changes(callbacks?: {}): void { - if (this.comm_live) { + if (this.comm) { const options: any = { patch: true }; if (callbacks) { options.callbacks = callbacks; @@ -728,11 +724,9 @@ export class WidgetModel extends Backbone.Model { model_id: string; views?: { [key: string]: Promise }; state_change: Promise; - comm?: IClassicComm; name: string; module: string; - - private _comm_live: boolean; + private _comm?: IClassicComm; private _closed: boolean; private _state_lock: any; private _buffered_state_diff: any; diff --git a/packages/base/test/src/widget_test.ts b/packages/base/test/src/widget_test.ts index 911f130127..6f2a3492fb 100644 --- a/packages/base/test/src/widget_test.ts +++ b/packages/base/test/src/widget_test.ts @@ -194,15 +194,15 @@ describe('WidgetModel', function () { }); }); - it('sets the widget_manager, id, comm, and comm_live properties', function () { - const widgetDead = new WidgetModel({}, { - model_id: 'widgetDead', + it('sets the widget_manager, id, comm, properties', function () { + const widgetNoComm = new WidgetModel({}, { + model_id: 'noComm', widget_manager: this.manager, } as IBackboneModelOptions); - expect(widgetDead.model_id).to.equal('widgetDead'); - expect(widgetDead.widget_manager).to.equal(this.manager); - expect(widgetDead.comm).to.be.undefined; - expect(widgetDead.comm_live).to.be.false; + expect(widgetNoComm.model_id).to.equal('noComm'); + expect(widgetNoComm.widget_manager).to.equal(this.manager); + expect(widgetNoComm.comm).to.be.undefined; + expect(widgetNoComm.comm_live).to.be.false; const comm = new MockComm(); const widgetLive = new WidgetModel({}, { diff --git a/python/jupyterlab_widgets/src/manager.ts b/python/jupyterlab_widgets/src/manager.ts index 8708430f9b..4bd1fa2096 100644 --- a/python/jupyterlab_widgets/src/manager.ts +++ b/python/jupyterlab_widgets/src/manager.ts @@ -302,7 +302,7 @@ abstract class LabWidgetManager extends ManagerBase implements IDisposable { get_state_sync(options: IStateOptions = {}): ReadonlyPartialJSONValue { const models = []; for (const model of this._modelsSync.values()) { - if (model.comm_live) { + if (model.comm) { models.push(model); } } @@ -488,6 +488,7 @@ export class KernelWidgetManager extends LabWidgetManager { case 'restarting': case 'dead': this.disconnect(); + this.clear_state(); break; } } @@ -510,7 +511,6 @@ export class KernelWidgetManager extends LabWidgetManager { this._restoredStatus = false; this._kernelRestoreInProgress = true; try { - await this.clear_state(); await this._loadFromKernel(); } catch { /* empty */