Skip to content

Commit b12dae7

Browse files
jtpiotrungleducmartinRenou
authored
Add support for widgets in JupyterLab code consoles (#3004)
* 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]>
1 parent ecddab9 commit b12dae7

File tree

4 files changed

+3745
-2804
lines changed

4 files changed

+3745
-2804
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ ui-tests/playwright-report
4949
**/lite/.cache
5050
**/*.doit.*
5151
**/docs/typedoc/
52+
53+
.yarn

python/jupyterlab_widgets/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
"@jupyter-widgets/controls": "^5.0.8",
5252
"@jupyter-widgets/output": "^6.0.7",
5353
"@jupyterlab/application": "^3.0.0 || ^4.0.0",
54+
"@jupyterlab/apputils": "^3.0.0 || ^4.0.0",
55+
"@jupyterlab/console": "^3.0.0 || ^4.0.0",
5456
"@jupyterlab/docregistry": "^3.0.0 || ^4.0.0",
5557
"@jupyterlab/logconsole": "^3.0.0 || ^4.0.0",
5658
"@jupyterlab/mainmenu": "^3.0.0 || ^4.0.0",
@@ -65,7 +67,6 @@
6567
"@lumino/algorithm": "^1.11.1 || ^2.0.0",
6668
"@lumino/coreutils": "^1.11.1 || ^2.1",
6769
"@lumino/disposable": "^1.10.1 || ^2.1",
68-
"@lumino/properties": "^1.8.1 || ^2.1",
6970
"@lumino/signaling": "^1.10.1 || ^2.1",
7071
"@lumino/widgets": "^1.30.0 || ^2.1",
7172
"@types/backbone": "1.4.14",

python/jupyterlab_widgets/src/plugin.ts

+230-42
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
// Distributed under the terms of the Modified BSD License.
33

44
import { ISettingRegistry } from '@jupyterlab/settingregistry';
5-
import * as nbformat from '@jupyterlab/nbformat';
65

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

8+
import * as nbformat from '@jupyterlab/nbformat';
9+
10+
import {
11+
IConsoleTracker,
12+
CodeConsole,
13+
ConsolePanel,
14+
} from '@jupyterlab/console';
15+
916
import {
1017
INotebookModel,
1118
INotebookTracker,
@@ -30,11 +37,13 @@ import { filter } from '@lumino/algorithm';
3037

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

33-
import { AttachedProperty } from '@lumino/properties';
34-
3540
import { WidgetRenderer } from './renderer';
3641

37-
import { WidgetManager, WIDGET_VIEW_MIMETYPE } from './manager';
42+
import {
43+
WidgetManager,
44+
WIDGET_VIEW_MIMETYPE,
45+
KernelWidgetManager,
46+
} from './manager';
3847

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

@@ -48,6 +57,7 @@ import '@jupyter-widgets/base/css/index.css';
4857
import '@jupyter-widgets/controls/css/widgets-base.css';
4958
import { KernelMessage } from '@jupyterlab/services';
5059
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
60+
import { ISessionContext } from '@jupyterlab/apputils';
5161

5262
const WIDGET_REGISTRY: base.IWidgetRegistryData[] = [];
5363

@@ -59,7 +69,7 @@ const SETTINGS: WidgetManager.Settings = { saveState: false };
5969
/**
6070
* Iterate through all widget renderers in a notebook.
6171
*/
62-
function* widgetRenderers(
72+
function* notebookWidgetRenderers(
6373
nb: Notebook
6474
): Generator<WidgetRenderer, void, unknown> {
6575
for (const cell of nb.widgets) {
@@ -77,6 +87,25 @@ function* widgetRenderers(
7787
}
7888
}
7989

90+
/**
91+
* Iterate through all widget renderers in a console.
92+
*/
93+
function* consoleWidgetRenderers(
94+
console: CodeConsole
95+
): Generator<WidgetRenderer, void, unknown> {
96+
for (const cell of Array.from(console.cells)) {
97+
if (cell.model.type === 'code') {
98+
for (const codecell of (cell as unknown as CodeCell).outputArea.widgets) {
99+
for (const output of Array.from(codecell.children())) {
100+
if (output instanceof WidgetRenderer) {
101+
yield output;
102+
}
103+
}
104+
}
105+
}
106+
}
107+
}
108+
80109
/**
81110
* Iterate through all matching linked output views
82111
*/
@@ -109,16 +138,69 @@ function* chain<T>(
109138
}
110139
}
111140

112-
export function registerWidgetManager(
113-
context: DocumentRegistry.IContext<INotebookModel>,
141+
/**
142+
* Get the kernel id of current notebook or console panel, this value
143+
* is used as key for `Private.widgetManagerProperty` to store the widget
144+
* manager of current notebook or console panel.
145+
*
146+
* @param {ISessionContext} sessionContext The session context of notebook or
147+
* console panel.
148+
*/
149+
async function getWidgetManagerOwner(
150+
sessionContext: ISessionContext
151+
): Promise<Private.IWidgetManagerOwner> {
152+
await sessionContext.ready;
153+
return sessionContext.session!.kernel!.id;
154+
}
155+
156+
/**
157+
* Common handler for registering both notebook and console
158+
* `WidgetManager`
159+
*
160+
* @param {(Notebook | CodeConsole)} content Context of panel.
161+
* @param {ISessionContext} sessionContext Session context of panel.
162+
* @param {IRenderMimeRegistry} rendermime Rendermime of panel.
163+
* @param {IterableIterator<WidgetRenderer>} renderers Iterator of
164+
* `WidgetRenderer` inside panel
165+
* @param {(() => WidgetManager | KernelWidgetManager)} widgetManagerFactory
166+
* function to create widget manager.
167+
*/
168+
async function registerWidgetHandler(
169+
content: Notebook | CodeConsole,
170+
sessionContext: ISessionContext,
114171
rendermime: IRenderMimeRegistry,
115-
renderers: IterableIterator<WidgetRenderer>
116-
): DisposableDelegate {
117-
let wManager = Private.widgetManagerProperty.get(context);
172+
renderers: IterableIterator<WidgetRenderer>,
173+
widgetManagerFactory: () => WidgetManager | KernelWidgetManager
174+
): Promise<DisposableDelegate> {
175+
const wManagerOwner = await getWidgetManagerOwner(sessionContext);
176+
let wManager = Private.widgetManagerProperty.get(wManagerOwner);
177+
let currentOwner: string;
178+
118179
if (!wManager) {
119-
wManager = new WidgetManager(context, rendermime, SETTINGS);
180+
wManager = widgetManagerFactory();
120181
WIDGET_REGISTRY.forEach((data) => wManager!.register(data));
121-
Private.widgetManagerProperty.set(context, wManager);
182+
Private.widgetManagerProperty.set(wManagerOwner, wManager);
183+
currentOwner = wManagerOwner;
184+
content.disposed.connect((_) => {
185+
const currentwManager = Private.widgetManagerProperty.get(currentOwner);
186+
if (currentwManager) {
187+
Private.widgetManagerProperty.delete(currentOwner);
188+
}
189+
});
190+
191+
sessionContext.kernelChanged.connect((_, args) => {
192+
const { newValue } = args;
193+
if (newValue) {
194+
const newKernelId = newValue.id;
195+
const oldwManager = Private.widgetManagerProperty.get(currentOwner);
196+
197+
if (oldwManager) {
198+
Private.widgetManagerProperty.delete(currentOwner);
199+
Private.widgetManagerProperty.set(newKernelId, oldwManager);
200+
}
201+
currentOwner = newKernelId;
202+
}
203+
});
122204
}
123205

124206
for (const r of renderers) {
@@ -145,6 +227,92 @@ export function registerWidgetManager(
145227
});
146228
}
147229

230+
// Kept for backward compat ipywidgets<=8, but not used here anymore
231+
export function registerWidgetManager(
232+
context: DocumentRegistry.IContext<INotebookModel>,
233+
rendermime: IRenderMimeRegistry,
234+
renderers: IterableIterator<WidgetRenderer>
235+
): DisposableDelegate {
236+
let wManager: WidgetManager;
237+
const managerReady = getWidgetManagerOwner(context.sessionContext).then(
238+
(wManagerOwner) => {
239+
const currentManager = Private.widgetManagerProperty.get(
240+
wManagerOwner
241+
) as WidgetManager;
242+
if (!currentManager) {
243+
wManager = new WidgetManager(context, rendermime, SETTINGS);
244+
WIDGET_REGISTRY.forEach((data) => wManager!.register(data));
245+
Private.widgetManagerProperty.set(wManagerOwner, wManager);
246+
} else {
247+
wManager = currentManager;
248+
}
249+
250+
for (const r of renderers) {
251+
r.manager = wManager;
252+
}
253+
254+
// Replace the placeholder widget renderer with one bound to this widget
255+
// manager.
256+
rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE);
257+
rendermime.addFactory(
258+
{
259+
safe: false,
260+
mimeTypes: [WIDGET_VIEW_MIMETYPE],
261+
createRenderer: (options) => new WidgetRenderer(options, wManager),
262+
},
263+
-10
264+
);
265+
}
266+
);
267+
268+
return new DisposableDelegate(async () => {
269+
await managerReady;
270+
if (rendermime) {
271+
rendermime.removeMimeType(WIDGET_VIEW_MIMETYPE);
272+
}
273+
wManager!.dispose();
274+
});
275+
}
276+
277+
export async function registerNotebookWidgetManager(
278+
panel: NotebookPanel,
279+
renderers: IterableIterator<WidgetRenderer>
280+
): Promise<DisposableDelegate> {
281+
const content = panel.content;
282+
const context = panel.context;
283+
const sessionContext = context.sessionContext;
284+
const rendermime = content.rendermime;
285+
const widgetManagerFactory = () =>
286+
new WidgetManager(context, rendermime, SETTINGS);
287+
288+
return registerWidgetHandler(
289+
content,
290+
sessionContext,
291+
rendermime,
292+
renderers,
293+
widgetManagerFactory
294+
);
295+
}
296+
297+
export async function registerConsoleWidgetManager(
298+
panel: ConsolePanel,
299+
renderers: IterableIterator<WidgetRenderer>
300+
): Promise<DisposableDelegate> {
301+
const content = panel.console;
302+
const sessionContext = content.sessionContext;
303+
const rendermime = content.rendermime;
304+
const widgetManagerFactory = () =>
305+
new KernelWidgetManager(sessionContext.session!.kernel!, rendermime);
306+
307+
return registerWidgetHandler(
308+
content,
309+
sessionContext,
310+
rendermime,
311+
renderers,
312+
widgetManagerFactory
313+
);
314+
}
315+
148316
/**
149317
* The widget manager provider.
150318
*/
@@ -154,6 +322,7 @@ export const managerPlugin: JupyterFrontEndPlugin<base.IJupyterWidgetRegistry> =
154322
requires: [IRenderMimeRegistry],
155323
optional: [
156324
INotebookTracker,
325+
IConsoleTracker,
157326
ISettingRegistry,
158327
IMainMenu,
159328
ILoggerRegistry,
@@ -175,6 +344,7 @@ function activateWidgetExtension(
175344
app: JupyterFrontEnd,
176345
rendermime: IRenderMimeRegistry,
177346
tracker: INotebookTracker | null,
347+
consoleTracker: IConsoleTracker | null,
178348
settingRegistry: ISettingRegistry | null,
179349
menu: IMainMenu | null,
180350
loggerRegistry: ILoggerRegistry | null,
@@ -183,15 +353,23 @@ function activateWidgetExtension(
183353
const { commands } = app;
184354
const trans = (translator ?? nullTranslator).load('jupyterlab_widgets');
185355

186-
const bindUnhandledIOPubMessageSignal = (nb: NotebookPanel): void => {
356+
const bindUnhandledIOPubMessageSignal = async (
357+
nb: NotebookPanel
358+
): Promise<void> => {
187359
if (!loggerRegistry) {
188360
return;
189361
}
362+
const wManagerOwner = await getWidgetManagerOwner(
363+
nb.context.sessionContext
364+
);
365+
const wManager = Private.widgetManagerProperty.get(wManagerOwner);
190366

191-
const wManager = Private.widgetManagerProperty.get(nb.context);
192367
if (wManager) {
193368
wManager.onUnhandledIOPubMessage.connect(
194-
(sender: WidgetManager, msg: KernelMessage.IIOPubMessage) => {
369+
(
370+
sender: WidgetManager | KernelWidgetManager,
371+
msg: KernelMessage.IIOPubMessage
372+
) => {
195373
const logger = loggerRegistry.getLogger(nb.context.path);
196374
let level: LogLevel = 'warning';
197375
if (
@@ -233,32 +411,32 @@ function activateWidgetExtension(
233411
);
234412

235413
if (tracker !== null) {
236-
tracker.forEach((panel) => {
237-
registerWidgetManager(
238-
panel.context,
239-
panel.content.rendermime,
240-
chain(
241-
widgetRenderers(panel.content),
242-
outputViews(app, panel.context.path)
243-
)
414+
const rendererIterator = (panel: NotebookPanel) =>
415+
chain(
416+
notebookWidgetRenderers(panel.content),
417+
outputViews(app, panel.context.path)
244418
);
245-
419+
tracker.forEach(async (panel) => {
420+
await registerNotebookWidgetManager(panel, rendererIterator(panel));
246421
bindUnhandledIOPubMessageSignal(panel);
247422
});
248-
tracker.widgetAdded.connect((sender, panel) => {
249-
registerWidgetManager(
250-
panel.context,
251-
panel.content.rendermime,
252-
chain(
253-
widgetRenderers(panel.content),
254-
outputViews(app, panel.context.path)
255-
)
256-
);
257-
423+
tracker.widgetAdded.connect(async (sender, panel) => {
424+
await registerNotebookWidgetManager(panel, rendererIterator(panel));
258425
bindUnhandledIOPubMessageSignal(panel);
259426
});
260427
}
261428

429+
if (consoleTracker !== null) {
430+
const rendererIterator = (panel: ConsolePanel) =>
431+
chain(consoleWidgetRenderers(panel.console));
432+
433+
consoleTracker.forEach(async (panel) => {
434+
await registerConsoleWidgetManager(panel, rendererIterator(panel));
435+
});
436+
consoleTracker.widgetAdded.connect(async (sender, panel) => {
437+
await registerConsoleWidgetManager(panel, rendererIterator(panel));
438+
});
439+
}
262440
if (settingRegistry !== null) {
263441
// Add a command for automatically saving (jupyter-)widget state.
264442
commands.addCommand('@jupyter-widgets/jupyterlab-manager:saveWidgetState', {
@@ -378,13 +556,23 @@ export default [
378556
];
379557
namespace Private {
380558
/**
381-
* A private attached property for a widget manager.
559+
* A type alias for keys of `widgetManagerProperty` .
382560
*/
383-
export const widgetManagerProperty = new AttachedProperty<
384-
DocumentRegistry.Context,
385-
WidgetManager | undefined
386-
>({
387-
name: 'widgetManager',
388-
create: (owner: DocumentRegistry.Context): undefined => undefined,
389-
});
561+
export type IWidgetManagerOwner = string;
562+
563+
/**
564+
* A type alias for values of `widgetManagerProperty` .
565+
*/
566+
export type IWidgetManagerValue =
567+
| WidgetManager
568+
| KernelWidgetManager
569+
| undefined;
570+
571+
/**
572+
* A private map for a widget manager.
573+
*/
574+
export const widgetManagerProperty = new Map<
575+
IWidgetManagerOwner,
576+
IWidgetManagerValue
577+
>();
390578
}

0 commit comments

Comments
 (0)