Skip to content

Commit b7fca7e

Browse files
committed
Wire unhandled widget messages to the jupyter output (microsoft#11273)
1 parent bf6a523 commit b7fca7e

File tree

11 files changed

+73
-8
lines changed

11 files changed

+73
-8
lines changed

news/2 Fixes/11239.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show unhandled widget messages in the jupyter output window.

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,5 +469,6 @@
469469
"DataScience.useCDNForWidgets": "Widgets require us to download supporting files from a 3rd party website. Click [here](https://aka.ms/PVSCIPyWidgets) for more information.",
470470
"DataScience.loadThirdPartyWidgetScriptsPostEnabled": "Please restart the Kernel when changing the setting 'python.dataScience.widgetScriptSources'.",
471471
"DataScience.enableCDNForWidgetsSetting": "Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1}).",
472-
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."
472+
"DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork": "Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected.",
473+
"DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}"
473474
}

src/client/common/utils/localize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,12 @@ export namespace DataScience {
850850
'DataScience.enableCDNForWidgetsSetting',
851851
"Widgets require us to download supporting files from a 3rd party website. Click <a href='https://command:python.datascience.enableLoadingWidgetScriptsFromThirdPartySource'>here</a> to enable this or click <a href='https://aka.ms/PVSCIPyWidgets'>here</a> for more information. (Error loading {0}:{1})."
852852
);
853+
854+
export const unhandledMessage = localize(
855+
'DataScience.unhandledMessage',
856+
'Unhandled kernel message from a widget: {0} : {1}'
857+
);
858+
853859
export const widgetScriptNotFoundOnCDNWidgetMightNotWork = localize(
854860
'DataScience.widgetScriptNotFoundOnCDNWidgetMightNotWork',
855861
"Unable to load a compatible version of the widget '{0}'. Expected behavior may be affected."

src/client/datascience/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ export enum Telemetry {
301301
IPyWidgetPromptToUseCDN = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN',
302302
IPyWidgetPromptToUseCDNSelection = 'DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION',
303303
IPyWidgetOverhead = 'DS_INTERNAL.IPYWIDGET_OVERHEAD',
304-
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE'
304+
IPyWidgetRenderFailure = 'DS_INTERNAL.IPYWIDGET_RENDER_FAILURE',
305+
IPyWidgetUnhandledMessage = 'DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE'
305306
}
306307

307308
export enum NativeKeyboardCommandTelemetry {

src/client/datascience/interactive-common/interactiveWindowTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export enum InteractiveWindowMessages {
114114
UpdateDisplayData = 'update_display_data',
115115
IPyWidgetLoadSuccess = 'ipywidget_load_success',
116116
IPyWidgetLoadFailure = 'ipywidget_load_failure',
117-
IPyWidgetRenderFailure = 'ipywidget_render_failure'
117+
IPyWidgetRenderFailure = 'ipywidget_render_failure',
118+
IPyWidgetUnhandledKernelMessage = 'ipywidget_unhandled_kernel_message'
118119
}
119120

120121
export enum IPyWidgetMessages {
@@ -609,4 +610,5 @@ export class IInteractiveWindowMapping {
609610
public [InteractiveWindowMessages.ConvertUriForUseInWebViewRequest]: Uri;
610611
public [InteractiveWindowMessages.ConvertUriForUseInWebViewResponse]: { request: Uri; response: Uri };
611612
public [InteractiveWindowMessages.IPyWidgetRenderFailure]: Error;
613+
public [InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: KernelMessage.IMessage;
612614
}

src/client/datascience/interactive-common/synchronization.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ const messageWithMessageTypes: MessageMapping<IInteractiveWindowMapping> & Messa
116116
[InteractiveWindowMessages.IPyWidgetLoadSuccess]: MessageType.other,
117117
[InteractiveWindowMessages.IPyWidgetLoadFailure]: MessageType.other,
118118
[InteractiveWindowMessages.IPyWidgetRenderFailure]: MessageType.other,
119+
[InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage]: MessageType.other,
119120
[InteractiveWindowMessages.LoadAllCells]: MessageType.other,
120121
[InteractiveWindowMessages.LoadAllCellsComplete]: MessageType.other,
121122
[InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: MessageType.other,

src/client/datascience/ipywidgets/ipywidgetHandler.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33

44
'use strict';
55

6-
import { inject, injectable } from 'inversify';
6+
import type { KernelMessage } from '@jupyterlab/services';
7+
import { inject, injectable, named } from 'inversify';
8+
import stripAnsi from 'strip-ansi';
79
import { Event, EventEmitter, Uri } from 'vscode';
810
import {
911
ILoadIPyWidgetClassFailureAction,
1012
LoadIPyWidgetClassLoadAction
1113
} from '../../../datascience-ui/interactive-common/redux/reducers/types';
1214
import { EnableIPyWidgets } from '../../common/experimentGroups';
13-
import { traceError } from '../../common/logger';
14-
import { IDisposableRegistry, IExperimentsManager } from '../../common/types';
15+
import { traceError, traceInfo } from '../../common/logger';
16+
import { IDisposableRegistry, IExperimentsManager, IOutputChannel } from '../../common/types';
17+
import * as localize from '../../common/utils/localize';
1518
import { sendTelemetryEvent } from '../../telemetry';
16-
import { Telemetry } from '../constants';
19+
import { JUPYTER_OUTPUT_CHANNEL, Telemetry } from '../constants';
1720
import { INotebookIdentity, InteractiveWindowMessages } from '../interactive-common/interactiveWindowTypes';
1821
import { IInteractiveWindowListener, INotebookProvider } from '../types';
1922
import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory';
@@ -46,7 +49,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
4649
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
4750
@inject(IPyWidgetMessageDispatcherFactory)
4851
private readonly widgetMessageDispatcherFactory: IPyWidgetMessageDispatcherFactory,
49-
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager
52+
@inject(IExperimentsManager) readonly experimentsManager: IExperimentsManager,
53+
@inject(IOutputChannel) @named(JUPYTER_OUTPUT_CHANNEL) private jupyterOutput: IOutputChannel
5054
) {
5155
disposables.push(
5256
notebookProvider.onNotebookCreated(async (e) => {
@@ -73,6 +77,8 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
7377
this.sendLoadFailureTelemetry(payload);
7478
} else if (message === InteractiveWindowMessages.IPyWidgetRenderFailure) {
7579
this.sendRenderFailureTelemetry(payload);
80+
} else if (message === InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage) {
81+
this.handleUnhandledMessage(payload);
7682
}
7783
// tslint:disable-next-line: no-any
7884
this.getIPyWidgetMessageDispatcher()?.receiveMessage({ message: message as any, payload }); // NOSONAR
@@ -113,6 +119,26 @@ export class IPyWidgetHandler implements IInteractiveWindowListener {
113119
// Do nothing on a failure
114120
}
115121
}
122+
123+
private handleUnhandledMessage(msg: KernelMessage.IMessage) {
124+
// Skip status messages
125+
if (msg.header.msg_type !== 'status') {
126+
try {
127+
// Special case errors, strip ansi codes from tracebacks so they print better.
128+
if (msg.header.msg_type === 'error') {
129+
const errorMsg = msg as KernelMessage.IErrorMsg;
130+
errorMsg.content.traceback = errorMsg.content.traceback.map(stripAnsi);
131+
}
132+
traceInfo(`Unhandled widget kernel message: ${msg.header.msg_type} ${msg.content}`);
133+
this.jupyterOutput.appendLine(
134+
localize.DataScience.unhandledMessage().format(msg.header.msg_type, JSON.stringify(msg.content))
135+
);
136+
sendTelemetryEvent(Telemetry.IPyWidgetUnhandledMessage, undefined, { msg_type: msg.header.msg_type });
137+
} catch {
138+
// Don't care if this doesn't get logged
139+
}
140+
}
141+
}
116142
private getIPyWidgetMessageDispatcher() {
117143
if (!this.notebookIdentity || !this.enabled) {
118144
return;

src/client/telemetry/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,4 +2029,10 @@ export interface IEventNamePropertyMapping {
20292029
* Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures).
20302030
*/
20312031
[Telemetry.IPyWidgetRenderFailure]: never | undefined;
2032+
/**
2033+
* Telemetry event sent when the widget tries to send a kernel message but nothing was listening
2034+
*/
2035+
[Telemetry.IPyWidgetUnhandledMessage]: {
2036+
msg_type: string;
2037+
};
20322038
}

src/datascience-ui/ipywidgets/manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
1515
import { createDeferred, Deferred } from '../../client/common/utils/async';
1616
import {
1717
IInteractiveWindowMapping,
18+
InteractiveWindowMessages,
1819
IPyWidgetMessages
1920
} from '../../client/datascience/interactive-common/interactiveWindowTypes';
2021
import { KernelSocketOptions } from '../../client/datascience/types';
@@ -161,6 +162,9 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
161162
// Listen for display data messages so we can prime the model for a display data
162163
this.proxyKernel.iopubMessage.connect(this.handleDisplayDataMessage.bind(this));
163164

165+
// Listen for unhandled IO pub so we can forward to the extension
166+
this.manager.onUnhandledIOPubMessage.connect(this.handleUnhanldedIOPubMessage.bind(this));
167+
164168
// Tell the observable about our new manager
165169
WidgetManager._instance.next(this);
166170
} catch (ex) {
@@ -204,4 +208,12 @@ export class WidgetManager implements IIPyWidgetManager, IMessageHandler {
204208
}
205209
}
206210
}
211+
212+
private handleUnhanldedIOPubMessage(_manager: any, msg: KernelMessage.IIOPubMessage) {
213+
// Send this to the other side
214+
this.postOffice.sendMessage<IInteractiveWindowMapping>(
215+
InteractiveWindowMessages.IPyWidgetUnhandledKernelMessage,
216+
msg
217+
);
218+
}
207219
}

src/datascience-ui/ipywidgets/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as jupyterlab from '@jupyter-widgets/base/lib';
77
import type { Kernel, KernelMessage } from '@jupyterlab/services';
88
import type { nbformat } from '@jupyterlab/services/node_modules/@jupyterlab/coreutils';
9+
import { ISignal } from '@phosphor/signaling';
910
import { Widget } from '@phosphor/widgets';
1011
import { IInteractiveWindowMapping } from '../../client/datascience/interactive-common/interactiveWindowTypes';
1112

@@ -25,6 +26,10 @@ export type IJupyterLabWidgetManagerCtor = new (
2526
) => IJupyterLabWidgetManager;
2627

2728
export interface IJupyterLabWidgetManager {
29+
/**
30+
* Signal emitted when a view emits an IO Pub message but nothing handles it.
31+
*/
32+
readonly onUnhandledIOPubMessage: ISignal<this, KernelMessage.IIOPubMessage>;
2833
dispose(): void;
2934
/**
3035
* Close all widgets and empty the widget state.

src/ipywidgets/src/manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export class WidgetManager extends jupyterlab.WidgetManager {
9595
// This throws errors if enabled, can be added later.
9696
}
9797

98+
public get onUnhandledIOPubMessage() {
99+
return super.onUnhandledIOPubMessage;
100+
}
101+
98102
protected async loadClass(className: string, moduleName: string, moduleVersion: string): Promise<any> {
99103
// Call the base class to try and load. If that fails, look locally
100104
window.console.log(`WidgetManager: Loading class ${className}:${moduleName}:${moduleVersion}`);

0 commit comments

Comments
 (0)