Skip to content

Commit 9394edb

Browse files
authored
Fix appending rich widgets into an output (#16891)
* Fix appending rich widgets into an output * Updates
1 parent ee82de9 commit 9394edb

File tree

6 files changed

+122
-15
lines changed

6 files changed

+122
-15
lines changed

src/test/datascience/notebook/helper.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -834,10 +834,10 @@ export async function waitForKernelToGetAutoSelectedImpl(
834834
return waitForKernelToChange(searchCriteria, notebookEditor!, timeout, skipAutoSelection);
835835
}
836836

837-
const prewarmNotebooksDone = { done: false };
837+
let prewarmNotebooksDone: { ipyWidgetVersion: 7 | 8 | undefined } | undefined = undefined;
838838
export async function prewarmNotebooks() {
839-
if (prewarmNotebooksDone.done) {
840-
return;
839+
if (prewarmNotebooksDone) {
840+
return prewarmNotebooksDone;
841841
}
842842
const { serviceContainer } = await getServices();
843843
await closeActiveWindows();
@@ -855,11 +855,14 @@ export async function prewarmNotebooks() {
855855
const cell = window.activeNotebookEditor!.notebook.cellAt(0)!;
856856
logger.ci(`Running all cells in prewarm notebooks`);
857857
await Promise.all([waitForExecutionCompletedSuccessfully(cell, 60_000), runAllCellsInActiveNotebook()]);
858+
const kernel = serviceContainer.get<IKernelProvider>(IKernelProvider).get(notebookEditor.notebook);
859+
const ipyWidgetVersion = kernel?.ipywidgetsVersion;
858860
await closeActiveWindows();
859861
await shutdownAllNotebooks();
862+
prewarmNotebooksDone = { ipyWidgetVersion };
863+
return prewarmNotebooksDone;
860864
} finally {
861865
disposables.forEach((d) => d.dispose());
862-
prewarmNotebooksDone.done = true;
863866
}
864867
}
865868

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "a0f3c2c2",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"import asyncio\n",
11+
"from IPython.display import display\n",
12+
"from ipywidgets import HTML, Output\n",
13+
"outputs = [Output() for _ in range(10)]\n",
14+
"display(*outputs)\n",
15+
"async def reproduce_bug():\n",
16+
" for i in range(10):\n",
17+
" await asyncio.sleep(0.01)\n",
18+
" outputs[i].append_display_data(HTML(f\"Content {i}\"))\n",
19+
"asyncio.create_task(reproduce_bug())"
20+
]
21+
}
22+
],
23+
"metadata": {
24+
"language_info": {
25+
"name": "python"
26+
}
27+
},
28+
"nbformat": 4,
29+
"nbformat_minor": 5
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "0b12e473",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"from IPython.display import display\n",
11+
"from ipywidgets import HTML, Output\n",
12+
"outputs = [Output() for _ in range(2)]\n",
13+
"display(*outputs)"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"id": "d40efbc0",
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"outputs[0].append_display_data(HTML(\"Content 0\"))"
24+
]
25+
}
26+
],
27+
"metadata": {
28+
"language_info": {
29+
"name": "python"
30+
}
31+
},
32+
"nbformat": 4,
33+
"nbformat_minor": 5
34+
}

src/test/datascience/widgets/standardWidgets.vscode.common.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ suite('Standard IPyWidget Tests @widgets', function () {
110110
this.retries(1);
111111
let editor: NotebookEditor;
112112
let comms: Utils;
113+
let ipyWidgetVersion: 7 | 8 | undefined;
113114
suiteSetup(async function () {
114115
logger.info('Suite Setup Standard IPyWidget Tests');
115116
this.timeout(120_000);
@@ -120,7 +121,7 @@ suite('Standard IPyWidget Tests @widgets', function () {
120121
logger.info('Suite Setup Standard IPyWidget Tests, Step 3');
121122
await startJupyterServer();
122123
logger.info('Suite Setup Standard IPyWidget Tests, Step 4');
123-
await prewarmNotebooks();
124+
({ ipyWidgetVersion } = await prewarmNotebooks());
124125
logger.info('Suite Setup Standard IPyWidget Tests, Step 5');
125126
sinon.restore();
126127
editor = (await createEmptyPythonNotebook(disposables, undefined, true)).editor;
@@ -593,4 +594,24 @@ suite('Standard IPyWidget Tests @widgets', function () {
593594
await assertOutputContainsHtml(cell, comms, ['Text Value is Bar']);
594595
});
595596
});
597+
test('Sync append_display_data renders', async function () {
598+
if (ipyWidgetVersion === 7) {
599+
return this.skip();
600+
}
601+
await initializeNotebookForWidgetTest(disposables, { templateFile: 'append_display_data_sync.ipynb' }, editor);
602+
const [cell1, cell2] = window.activeNotebookEditor!.notebook.getCells();
603+
await executeCellAndWaitForOutput(cell1, comms);
604+
await executeCellAndDontWaitForOutput(cell2);
605+
await assertOutputContainsHtml(cell1, comms, ['Content 0'], '.widget-output');
606+
});
607+
test('Async append_display_data renders', async function () {
608+
if (ipyWidgetVersion === 7) {
609+
return this.skip();
610+
}
611+
await initializeNotebookForWidgetTest(disposables, { templateFile: 'append_display_data_async.ipynb' }, editor);
612+
const [cell] = window.activeNotebookEditor!.notebook.getCells();
613+
await executeCellAndWaitForOutput(cell, comms);
614+
// Check that at least one output widget rendered content
615+
await assertOutputContainsHtml(cell, comms, ['Content 3'], '.widget-output');
616+
});
596617
});

src/webviews/extension-side/ipywidgets/rendererComms.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Disposable, NotebookDocument, NotebookEditor, NotebookRendererMessaging
99
import { IKernel, IKernelProvider } from '../../../kernels/types';
1010
import { IControllerRegistration } from '../../../notebooks/controllers/types';
1111
import { IExtensionSyncActivationService } from '../../../platform/activation/types';
12-
import { WIDGET_MIMETYPE } from '../../../platform/common/constants';
12+
import { WIDGET_MIMETYPE, Identifiers } from '../../../platform/common/constants';
1313
import { dispose } from '../../../platform/common/utils/lifecycle';
1414
import { IDisposable } from '../../../platform/common/types';
1515
import { noop } from '../../../platform/common/utils/misc';
@@ -84,11 +84,23 @@ export class IPyWidgetRendererComms implements IExtensionSyncActivationService {
8484
jupyterLab.KernelMessage.isExecuteResultMsg(msg)
8585
) {
8686
this.trackModelId(kernel.notebook, msg);
87+
} else if (
88+
jupyterLab.KernelMessage.isCommOpenMsg(msg) &&
89+
// Track widget model ids as soon as the comm opens to avoid races with renderer queries.
90+
msg.content?.target_name === Identifiers.DefaultCommTarget &&
91+
typeof msg.content?.comm_id === 'string'
92+
) {
93+
this.addModelId(kernel.notebook, msg.content.comm_id);
8794
}
8895
};
8996
iopubMessage.connect(handler);
9097
this.disposables.push(new Disposable(() => iopubMessage.disconnect(handler)));
9198
}
99+
private addModelId(notebook: NotebookDocument, modelId: string) {
100+
const set = this.widgetOutputsPerNotebook.get(notebook) || new Set<string>();
101+
set.add(modelId);
102+
this.widgetOutputsPerNotebook.set(notebook, set);
103+
}
92104
private trackModelId(
93105
notebook: NotebookDocument,
94106
msg: {
@@ -101,9 +113,7 @@ export class IPyWidgetRendererComms implements IExtensionSyncActivationService {
101113
if (output.data && typeof output.data === 'object' && WIDGET_MIMETYPE in output.data) {
102114
const widgetData = output.data[WIDGET_MIMETYPE] as WidgetData;
103115
if (widgetData && 'model_id' in widgetData) {
104-
const set = this.widgetOutputsPerNotebook.get(notebook) || new Set<string>();
105-
set.add(widgetData.model_id);
106-
this.widgetOutputsPerNotebook.set(notebook, set);
116+
this.addModelId(notebook, widgetData.model_id);
107117
}
108118
}
109119
}

src/webviews/webview-side/ipywidgets/kernel/manager.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
ScriptLoader
1919
} from './types';
2020
import { KernelSocketOptions } from '../../../../kernels/types';
21-
import { Deferred, createDeferred } from '../../../../platform/common/utils/async';
21+
import { Deferred, createDeferred, raceTimeout } from '../../../../platform/common/utils/async';
2222
import { IInteractiveWindowMapping, IPyWidgetMessages, InteractiveWindowMessages } from '../../../../messageTypes';
2323
import { WIDGET_MIMETYPE, WIDGET_STATE_MIMETYPE } from '../../../../platform/common/constants';
2424
import { NotebookMetadata } from '../../../../platform/common/utils';
@@ -181,7 +181,13 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
181181
// Check if we have processed the data for this model.
182182
// If not wait.
183183
if (!this.modelIdsToBeDisplayed.has(modelId)) {
184-
this.modelIdsToBeDisplayed.set(modelId, createDeferred());
184+
const deferred = createDeferred<void>();
185+
this.modelIdsToBeDisplayed.set(modelId, deferred);
186+
// Try to get the model if available.
187+
const modelPromise = this.manager.get_model(modelId);
188+
if (modelPromise && (await raceTimeout(1_000, modelPromise.catch(noop)))) {
189+
deferred.resolve();
190+
}
185191
}
186192
// Wait until it is flagged as ready to be processed.
187193
// This widget manager must have received this message and performed all operations before this.
@@ -315,17 +321,21 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
315321

316322
if (
317323
!jupyterLab.KernelMessage.isDisplayDataMsg(payload) &&
324+
!jupyterLab.KernelMessage.isUpdateDisplayDataMsg?.(payload) &&
318325
!jupyterLab.KernelMessage.isExecuteResultMsg(payload)
319326
) {
320327
return;
321328
}
322-
const displayMsg = payload as KernelMessage.IDisplayDataMsg | KernelMessage.IExecuteResultMsg;
329+
const displayMsg = payload as
330+
| KernelMessage.IDisplayDataMsg
331+
| KernelMessage.IUpdateDisplayDataMsg
332+
| KernelMessage.IExecuteResultMsg;
323333

324334
if (displayMsg.content && displayMsg.content.data && displayMsg.content.data[WIDGET_MIMETYPE]) {
325335
// eslint-disable-next-line @typescript-eslint/no-explicit-any
326336
const data = displayMsg.content.data[WIDGET_MIMETYPE] as any;
327337
const modelId = data.model_id;
328-
logMessage(`Received display data message ${modelId}`);
338+
logMessage(`Received display/update data message ${modelId}`);
329339
let deferred = this.modelIdsToBeDisplayed.get(modelId);
330340
if (!deferred) {
331341
deferred = createDeferred();
@@ -337,8 +347,7 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
337347
const modelPromise = this.manager.get_model(data.model_id);
338348
if (modelPromise) {
339349
try {
340-
const m = await modelPromise;
341-
console.log(m);
350+
await modelPromise;
342351
deferred.resolve();
343352
} catch (ex) {
344353
deferred.reject(ex);

0 commit comments

Comments
 (0)