Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for widgets in JupyterLab code consoles #3004

Merged
merged 3 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading