Skip to content

Commit

Permalink
Add support for widgets in JupyterLab code consoles (#3004)
Browse files Browse the repository at this point in the history
* Widgets support in JupyterLab console

Co-authored-by: Duc Trung LE <[email protected]>

* toArray -> Array.from

* Backward compatibility

---------

Co-authored-by: Duc Trung LE <[email protected]>
Co-authored-by: martinRenou <[email protected]>
  • Loading branch information
3 people committed May 24, 2024
1 parent ecddab9 commit b12dae7
Show file tree
Hide file tree
Showing 4 changed files with 3,745 additions and 2,804 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ ui-tests/playwright-report
**/lite/.cache
**/*.doit.*
**/docs/typedoc/

.yarn
3 changes: 2 additions & 1 deletion python/jupyterlab_widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"@jupyter-widgets/controls": "^5.0.8",
"@jupyter-widgets/output": "^6.0.7",
"@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",
Expand All @@ -65,7 +67,6 @@
"@lumino/algorithm": "^1.11.1 || ^2.0.0",
"@lumino/coreutils": "^1.11.1 || ^2.1",
"@lumino/disposable": "^1.10.1 || ^2.1",
"@lumino/properties": "^1.8.1 || ^2.1",
"@lumino/signaling": "^1.10.1 || ^2.1",
"@lumino/widgets": "^1.30.0 || ^2.1",
"@types/backbone": "1.4.14",
Expand Down
272 changes: 230 additions & 42 deletions python/jupyterlab_widgets/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
// Distributed under the terms of the Modified BSD License.

import { ISettingRegistry } from '@jupyterlab/settingregistry';
import * as nbformat from '@jupyterlab/nbformat';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import * as nbformat from '@jupyterlab/nbformat';

import {
IConsoleTracker,
CodeConsole,
ConsolePanel,
} from '@jupyterlab/console';

import {
INotebookModel,
INotebookTracker,
Expand All @@ -30,11 +37,13 @@ import { filter } from '@lumino/algorithm';

import { DisposableDelegate } from '@lumino/disposable';

import { AttachedProperty } from '@lumino/properties';

import { WidgetRenderer } from './renderer';

import { WidgetManager, WIDGET_VIEW_MIMETYPE } from './manager';
import {
WidgetManager,
WIDGET_VIEW_MIMETYPE,
KernelWidgetManager,
} from './manager';

import { OutputModel, OutputView, OUTPUT_WIDGET_VERSION } from './output';

Expand All @@ -48,6 +57,7 @@ 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[] = [];

Expand All @@ -59,7 +69,7 @@ const SETTINGS: WidgetManager.Settings = { saveState: false };
/**
* Iterate through all widget renderers in a notebook.
*/
function* widgetRenderers(
function* notebookWidgetRenderers(
nb: Notebook
): Generator<WidgetRenderer, void, unknown> {
for (const cell of nb.widgets) {
Expand All @@ -77,6 +87,25 @@ function* widgetRenderers(
}
}

/**
* Iterate through all widget renderers in a console.
*/
function* consoleWidgetRenderers(
console: CodeConsole
): Generator<WidgetRenderer, void, unknown> {
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
*/
Expand Down Expand Up @@ -109,16 +138,69 @@ function* chain<T>(
}
}

export function registerWidgetManager(
context: DocumentRegistry.IContext<INotebookModel>,
/**
* 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<Private.IWidgetManagerOwner> {
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<WidgetRenderer>} 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<WidgetRenderer>
): DisposableDelegate {
let wManager = Private.widgetManagerProperty.get(context);
renderers: IterableIterator<WidgetRenderer>,
widgetManagerFactory: () => WidgetManager | KernelWidgetManager
): Promise<DisposableDelegate> {
const wManagerOwner = await getWidgetManagerOwner(sessionContext);
let wManager = Private.widgetManagerProperty.get(wManagerOwner);
let currentOwner: string;

if (!wManager) {
wManager = new WidgetManager(context, rendermime, SETTINGS);
wManager = widgetManagerFactory();
WIDGET_REGISTRY.forEach((data) => wManager!.register(data));
Private.widgetManagerProperty.set(context, wManager);
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) {
Expand All @@ -145,6 +227,92 @@ export function registerWidgetManager(
});
}

// Kept for backward compat ipywidgets<=8, but not used here anymore
export function registerWidgetManager(
context: DocumentRegistry.IContext<INotebookModel>,
rendermime: IRenderMimeRegistry,
renderers: IterableIterator<WidgetRenderer>
): 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;
if (rendermime) {
rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE);
}
wManager!.dispose();
});
}

export async function registerNotebookWidgetManager(
panel: NotebookPanel,
renderers: IterableIterator<WidgetRenderer>
): Promise<DisposableDelegate> {
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<WidgetRenderer>
): Promise<DisposableDelegate> {
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
);
}

/**
* The widget manager provider.
*/
Expand All @@ -154,6 +322,7 @@ export const managerPlugin: JupyterFrontEndPlugin<base.IJupyterWidgetRegistry> =
requires: [IRenderMimeRegistry],
optional: [
INotebookTracker,
IConsoleTracker,
ISettingRegistry,
IMainMenu,
ILoggerRegistry,
Expand All @@ -175,6 +344,7 @@ function activateWidgetExtension(
app: JupyterFrontEnd,
rendermime: IRenderMimeRegistry,
tracker: INotebookTracker | null,
consoleTracker: IConsoleTracker | null,
settingRegistry: ISettingRegistry | null,
menu: IMainMenu | null,
loggerRegistry: ILoggerRegistry | null,
Expand All @@ -183,15 +353,23 @@ function activateWidgetExtension(
const { commands } = app;
const trans = (translator ?? nullTranslator).load('jupyterlab_widgets');

const bindUnhandledIOPubMessageSignal = (nb: NotebookPanel): void => {
const bindUnhandledIOPubMessageSignal = async (
nb: NotebookPanel
): Promise<void> => {
if (!loggerRegistry) {
return;
}
const wManagerOwner = await getWidgetManagerOwner(
nb.context.sessionContext
);
const wManager = Private.widgetManagerProperty.get(wManagerOwner);

const wManager = Private.widgetManagerProperty.get(nb.context);
if (wManager) {
wManager.onUnhandledIOPubMessage.connect(
(sender: WidgetManager, msg: KernelMessage.IIOPubMessage) => {
(
sender: WidgetManager | KernelWidgetManager,
msg: KernelMessage.IIOPubMessage
) => {
const logger = loggerRegistry.getLogger(nb.context.path);
let level: LogLevel = 'warning';
if (
Expand Down Expand Up @@ -233,32 +411,32 @@ function activateWidgetExtension(
);

if (tracker !== null) {
tracker.forEach((panel) => {
registerWidgetManager(
panel.context,
panel.content.rendermime,
chain(
widgetRenderers(panel.content),
outputViews(app, panel.context.path)
)
const rendererIterator = (panel: NotebookPanel) =>
chain(
notebookWidgetRenderers(panel.content),
outputViews(app, panel.context.path)
);

tracker.forEach(async (panel) => {
await registerNotebookWidgetManager(panel, rendererIterator(panel));
bindUnhandledIOPubMessageSignal(panel);
});
tracker.widgetAdded.connect((sender, panel) => {
registerWidgetManager(
panel.context,
panel.content.rendermime,
chain(
widgetRenderers(panel.content),
outputViews(app, panel.context.path)
)
);

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.
commands.addCommand('@jupyter-widgets/jupyterlab-manager:saveWidgetState', {
Expand Down Expand Up @@ -378,13 +556,23 @@ export default [
];
namespace Private {
/**
* A private attached property for a widget manager.
* A type alias for keys of `widgetManagerProperty` .
*/
export const widgetManagerProperty = new AttachedProperty<
DocumentRegistry.Context,
WidgetManager | undefined
>({
name: 'widgetManager',
create: (owner: DocumentRegistry.Context): undefined => undefined,
});
export type IWidgetManagerOwner = string;

/**
* A type alias for values of `widgetManagerProperty` .
*/
export type IWidgetManagerValue =
| WidgetManager
| KernelWidgetManager
| undefined;

/**
* A private map for a widget manager.
*/
export const widgetManagerProperty = new Map<
IWidgetManagerOwner,
IWidgetManagerValue
>();
}
Loading

0 comments on commit b12dae7

Please sign in to comment.