Skip to content

Commit

Permalink
Dynamic loading of scripts to support IPyWidget 8
Browse files Browse the repository at this point in the history
  • Loading branch information
DonJayamanne committed Jan 24, 2023
1 parent a301e16 commit 5fa050d
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 52 deletions.
3 changes: 3 additions & 0 deletions src/kernels/common/delayedFutureExecute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Kernel, KernelMessage } from '@jupyterlab/services';
import { traceInfoIfCI } from '../../platform/logging';
import { createDeferred } from '../../platform/common/utils/async';
import { CancellationError } from 'vscode';
import { noop } from '../../platform/common/utils/misc';

// Wraps a future so that a requestExecute on a session will wait for the previous future to finish before actually executing
export class DelayedFutureExecute
Expand Down Expand Up @@ -40,6 +41,8 @@ export class DelayedFutureExecute
private disposeOnDone?: boolean,
private metadata?: JSONObject
) {
// Ensure we don't have any unhandled promises.
this.doneDeferred.promise.catch(noop);
// Setup our request based on the previous link finishing
previousLink.done.then(() => this.requestExecute()).catch((e) => this.doneDeferred.reject(e));

Expand Down
104 changes: 102 additions & 2 deletions src/kernels/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Uri,
NotebookDocument
} from 'vscode';
import { CodeSnippets, Identifiers } from '../platform/common/constants';
import { CodeSnippets, Identifiers, WIDGET_MIMETYPE } from '../platform/common/constants';
import { IApplicationShell } from '../platform/common/application/types';
import { WrappedError } from '../platform/errors/types';
import { disposeAllDisposables } from '../platform/common/helpers';
Expand Down Expand Up @@ -54,13 +54,41 @@ import { Cancellation, isCancellationError } from '../platform/common/cancellati
import { KernelProgressReporter } from '../platform/progress/kernelProgressReporter';
import { DisplayOptions } from './displayOptions';
import { SilentExecutionErrorOptions } from './helpers';
import dedent from 'dedent';
import { IAnyMessageArgs } from '@jupyterlab/services/lib/kernel/kernel';

const widgetVersionOutPrefix = 'e976ee50-99ed-4aba-9b6b-9dcd5634d07d:IPyWidgets:';
/**
* Sometimes we send code internally, e.g. to determine version of IPyWidgets and the like.
* Such messages need not be mirrored with the renderer.
*/
export function shouldMessageBeMirroredWithRenderer(msg: KernelMessage.IExecuteRequestMsg | string) {
let code = typeof msg === 'string' ? msg : '';
if (typeof msg !== 'string' && 'content' in msg && 'code' in msg.content && typeof msg.content.code === 'string') {
code = msg.content.code;
}

if (code.includes(widgetVersionOutPrefix)) {
return false;
}
return true;
}

type Hook = (...args: unknown[]) => Promise<void>;
/**
* Represents an active kernel process running on the jupyter (or local) machine.
*/
abstract class BaseKernel implements IBaseKernel {
protected readonly disposables: IDisposable[] = [];
private _ipywidgetsVersion?: 7 | 8;
public get ipywidgetsVersion() {
return this._ipywidgetsVersion;
}
private _onIPyWidgetsVersionChanged = new EventEmitter<7 | 8>();
public get onIPyWidgetsVersionChanged() {
return this._onIPyWidgetsVersionChanged.event;
}

get onStatusChanged(): Event<KernelMessage.Status> {
return this._onStatusChanged.event;
}
Expand Down Expand Up @@ -143,6 +171,7 @@ abstract class BaseKernel implements IBaseKernel {
this.disposables.push(this._onRestarted);
this.disposables.push(this._onStarted);
this.disposables.push(this._onDisposed);
this.disposables.push(this._onIPyWidgetsVersionChanged);
this.disposables.push({ dispose: () => this._kernelSocket.unsubscribe() });
trackKernelResourceInformation(this.resourceUri, {
kernelConnection: this.kernelConnectionMetadata,
Expand Down Expand Up @@ -657,6 +686,7 @@ abstract class BaseKernel implements IBaseKernel {
// Restart sessions and retries might make this hard to do correctly otherwise.
session.registerCommTarget(Identifiers.DefaultCommTarget, noop);

await this.determineVersionOfIPyWidgets();
// Gather all of the startup code at one time and execute as one cell
const startupCode = await this.gatherInternalStartupCode();
await this.executeSilently(session, startupCode, {
Expand Down Expand Up @@ -709,6 +739,76 @@ abstract class BaseKernel implements IBaseKernel {
traceVerbose('End running kernel initialization, session is idle');
}
}
private async determineVersionOfIPyWidgets() {
const determineVersionImpl = async () => {
const session = this.session;
if (!session) {
traceVerbose('No session to determine version of ipywidgets');
return;
}
const codeToDetermineIPyWidgetsVersion = dedent`
try:
import ipywidgets as _VSCODE_ipywidgets
print("${widgetVersionOutPrefix}" + _VSCODE_ipywidgets.__version__)
del _VSCODE_ipywidgets
except:
pass
`;

const version = await this.executeSilently(session, [codeToDetermineIPyWidgetsVersion]).catch((ex) =>
traceError('Failed to determine version of IPyWidgets', ex)
);
traceError('Determined IPyKernel Version', JSON.stringify(version));
if (Array.isArray(version)) {
const isVersion8 = version.some((output) =>
(output.text || '')?.toString().includes(`${widgetVersionOutPrefix}8.`)
);
const isVersion7 = version.some((output) =>
(output.text || '')?.toString().includes(`${widgetVersionOutPrefix}7.`)
);

const oldVersion = this._ipywidgetsVersion;
const newVersion = (this._ipywidgetsVersion = isVersion7 ? 7 : isVersion8 ? 8 : undefined);
if (oldVersion !== newVersion && newVersion) {
traceError('Determined IPyKernel Version and event fired', JSON.stringify(newVersion));
// If user does not have ipywidgets installed, then this event will never get fired.
this._onIPyWidgetsVersionChanged.fire(newVersion);
}
}
};
await determineVersionImpl();

// If we do not have the version of IPyWidgets, its possible the user has not installed it.
// However while running cells users can install IPykernel via `!pip install ipywidgets` or the like.
// Hence we need to monitor messages that require widgets and the determine the version of widgets at that point in time.
// This is not ideal, but its the best we can do.
if (!this._ipywidgetsVersion && this.session?.kernel) {
const anyMessageHandler = // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_: unknown, msg: IAnyMessageArgs) => {
if (msg.direction === 'send') {
return;
}
const message = msg.msg;
if (
message.content &&
'data' in message.content &&
message.content.data &&
(message.content.data[WIDGET_MIMETYPE] ||
('target_name' in message.content &&
message.content.target_name === Identifiers.DefaultCommTarget))
) {
if (!this._ipywidgetsVersion) {
determineVersionImpl().catch(noop);
if (this.session?.kernel) {
this.session.kernel.anyMessage.disconnect(anyMessageHandler, this);
}
}
}
};

this.session.kernel.anyMessage.connect(anyMessageHandler, this);
}
}

protected async gatherInternalStartupCode(): Promise<string[]> {
// Gather all of the startup code into a giant string array so we
Expand Down Expand Up @@ -809,7 +909,7 @@ abstract class BaseKernel implements IBaseKernel {
traceVerbose(`Not executing startup session: ${session ? 'Object' : 'undefined'}, code: ${code}`);
return;
}
await executeSilently(session, code.join('\n'), errorOptions);
return executeSilently(session, code.join('\n'), errorOptions);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/kernels/kernelExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IOutput } from '@jupyterlab/nbformat';
import { NotebookCell, EventEmitter, notebooks, NotebookCellExecutionState, NotebookDocument, workspace } from 'vscode';
import { NotebookCellKind } from 'vscode-languageserver-protocol';
import { IApplicationShell } from '../platform/common/application/types';
import { disposeAllDisposables } from '../platform/common/helpers';
import { getDisplayPath } from '../platform/common/platform/fs-paths';
import { IDisposable, IExtensionContext } from '../platform/common/types';
import { traceInfo, traceVerbose } from '../platform/logging';
Expand Down Expand Up @@ -71,6 +72,9 @@ export class NotebookKernelExecution implements INotebookKernelExecution {
kernel.addHook('willRestart', (sessionPromise) => this.onWillRestart(sessionPromise), this, this.disposables);
this.disposables.push(this._onPreExecute);
}
public dispose() {
disposeAllDisposables(this.disposables);
}
public get pendingCells(): readonly NotebookCell[] {
return this.documentExecutions.get(this.notebook)?.queue || [];
}
Expand Down
2 changes: 2 additions & 0 deletions src/kernels/kernelProvider.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export abstract class BaseCoreKernelProvider implements IKernelProvider {
)}`
);
}
this.executions.get(kernel)?.dispose();
this.pendingDisposables.delete(kernel);
},
this,
Expand All @@ -138,6 +139,7 @@ export abstract class BaseCoreKernelProvider implements IKernelProvider {
`Disposing kernel associated with ${getDisplayPath(notebook.uri)}, isClosed=${notebook.isClosed}`
);
this.pendingDisposables.add(kernelToDispose.kernel);
this.executions.get(kernelToDispose.kernel)?.dispose();
kernelToDispose.kernel
.dispose()
.catch((ex) => traceWarning('Failed to dispose old kernel', ex))
Expand Down
4 changes: 3 additions & 1 deletion src/kernels/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ export type KernelHooks =
| 'didStart'
| 'willCancel';
export interface IBaseKernel extends IAsyncDisposable {
readonly ipywidgetsVersion?: 7 | 8;
readonly onIPyWidgetsVersionChanged: Event<7 | 8>;
readonly uri: Uri;
/**
* In the case of Notebooks, this is the same as the Notebook Uri.
Expand Down Expand Up @@ -393,7 +395,7 @@ export interface IKernel extends IBaseKernel {
readonly creator: 'jupyterExtension';
}

export interface INotebookKernelExecution {
export interface INotebookKernelExecution extends IDisposable {
/**
* Total execution count on this kernel
*/
Expand Down
2 changes: 2 additions & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export enum IPyWidgetMessages {
IPyWidgets_AttemptToDownloadFailedWidgetsAgain = 'IPyWidgets_AttemptToDownloadFailedWidgetsAgain',
IPyWidgets_IsOnline = 'IPyWidgets_IsOnline',
IPyWidgets_Ready = 'IPyWidgets_Ready',
IPyWidgets_Request_Widget_Script_Url = 'IPyWidgets_Request_Widget_Script_Url',
IPyWidgets_Reply_Widget_Script_Url = 'IPyWidgets_Reply_Widget_Script_Url',
IPyWidgets_onRestartKernel = 'IPyWidgets_onRestartKernel',
IPyWidgets_onKernelChanged = 'IPyWidgets_onKernelChanged',
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'use strict';

import type { KernelMessage } from '@jupyterlab/services';
import { Event, EventEmitter, NotebookDocument } from 'vscode';
import { Event, EventEmitter, NotebookDocument, Uri } from 'vscode';
import { IApplicationShell, ICommandManager } from '../../../../platform/common/application/types';
import { STANDARD_OUTPUT_CHANNEL } from '../../../../platform/common/constants';
import { traceVerbose, traceError, traceInfo, traceInfoIfCI } from '../../../../platform/logging';
Expand All @@ -13,7 +13,9 @@ import {
IOutputChannel,
IConfigurationService,
IHttpClient,
IsWebExtension
IsWebExtension,
IExtensionContext,
IDisposable
} from '../../../../platform/common/types';
import { Common, DataScience } from '../../../../platform/common/utils/localize';
import { noop } from '../../../../platform/common/utils/misc';
Expand All @@ -29,14 +31,17 @@ import { IServiceContainer } from '../../../../platform/ioc/types';
import { sendTelemetryEvent, Telemetry } from '../../../../telemetry';
import { getTelemetrySafeHashedString } from '../../../../platform/telemetry/helpers';
import { Commands } from '../../../../platform/common/constants';
import { IKernelProvider } from '../../../../kernels/types';
import { IKernel, IKernelProvider } from '../../../../kernels/types';
import { IPyWidgetMessageDispatcherFactory } from './ipyWidgetMessageDispatcherFactory';
import { IPyWidgetScriptSource } from '../scriptSourceProvider/ipyWidgetScriptSource';
import { IIPyWidgetMessageDispatcher, IWidgetScriptSourceProviderFactory } from '../types';
import { ConsoleForegroundColors } from '../../../../platform/logging/types';
import { IWebviewCommunication } from '../../../../platform/webviews/types';
import { swallowExceptions } from '../../../../platform/common/utils/decorators';
import { CDNWidgetScriptSourceProvider } from '../scriptSourceProvider/cdnWidgetScriptSourceProvider';
import { createDeferred } from '../../../../platform/common/utils/async';
import { disposeAllDisposables } from '../../../../platform/common/helpers';
import { StopWatch } from '../../../../platform/common/utils/stopWatch';

/**
* This class wraps all of the ipywidgets communication with a backing notebook
Expand Down Expand Up @@ -117,9 +122,72 @@ export class CommonMessageCoordinator {
this.disposables
);
webview.onDidReceiveMessage(
(m) => {
async (m) => {
traceInfoIfCI(`${ConsoleForegroundColors.Green}Widget Coordinator received ${m.type}`);
this.onMessage(webview, m.type, m.payload);
if (m.type === IPyWidgetMessages.IPyWidgets_Request_Widget_Script_Url) {
// Determine the version of ipywidgets and send the appropriate script url to the webview.
const stopWatch = new StopWatch();
traceVerbose('Attempting to determine version of IPyWidgets');
const disposables: IDisposable[] = [];
const kernelProvider = this.serviceContainer.get<IKernelProvider>(IKernelProvider);
const deferred = createDeferred<7 | 8>();
const kernelPromise = createDeferred<IKernel>();
if (kernelProvider.get(this.document)) {
kernelPromise.resolve(kernelProvider.get(this.document));
} else {
kernelProvider.onDidCreateKernel(
(e) => {
if (e.notebook === this.document) {
kernelPromise.resolve(e);
}
},
this,
disposables
);
}
const kernel = await kernelPromise.promise;
if (kernel) {
if (kernel.ipywidgetsVersion) {
deferred.resolve(kernel.ipywidgetsVersion);
} else {
traceVerbose('Waiting for IPyWidgets version');
kernel.onIPyWidgetsVersionChanged(
() => {
if (kernel.ipywidgetsVersion) {
deferred.resolve(kernel.ipywidgetsVersion);
disposeAllDisposables(disposables);
}
},
this,
disposables
);
}
}
if (disposables.length) {
this.disposables.push(...disposables);
}
traceVerbose('Waiting for IPyWidgets version promise');
// IPyWidgets scripts will not be loaded if we're unable to determine the version of IPyWidgets.
const version = await deferred.promise;
traceVerbose(`Version of IPyWidgets ${version} determined after ${stopWatch.elapsedTime / 1000}s`);
const context = this.serviceContainer.get<IExtensionContext>(IExtensionContext);
const scriptPath = Uri.joinPath(
context.extensionUri,
'node_modules',
'@vscode',
version === 7 ? 'jupyter-ipywidgets7' : 'jupyter-ipywidgets8',
'dist',
'ipywidgets.js'
);
const url = webview.asWebviewUri(scriptPath);
webview
.postMessage({
type: IPyWidgetMessages.IPyWidgets_Reply_Widget_Script_Url,
payload: url.toString()
})
.then(noop, noop);
}
if (m.type === IPyWidgetMessages.IPyWidgets_Ready) {
traceInfoIfCI('Web view is ready to receive widget messages');
this.readyMessageReceived = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { IPyWidgetMessages, IInteractiveWindowMapping } from '../../../../messag
import { sendTelemetryEvent, Telemetry } from '../../../../telemetry';
import { IKernel, IKernelProvider, KernelSocketInformation } from '../../../../kernels/types';
import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from '../types';
import { shouldMessageBeMirroredWithRenderer } from '../../../../kernels/kernel';

type PendingMessage = {
resultPromise: Deferred<void>;
Expand Down Expand Up @@ -239,6 +240,9 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const msg = this.deserialize(data) as KernelMessage.IExecuteRequestMsg;
if (msg.channel === 'shell' && msg.header.msg_type === 'execute_request') {
if (!shouldMessageBeMirroredWithRenderer(msg)) {
return;
}
const promise = this.mirrorExecuteRequest(msg as KernelMessage.IExecuteRequestMsg); // NOSONAR
// If there are no ipywidgets thusfar in the notebook, then no need to synchronize messages.
if (this.isUsingIPyWidgets) {
Expand Down Expand Up @@ -290,7 +294,9 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
this.waitingMessageIds.set(msgUuid, { startTime: Date.now(), resultPromise: promise });

if (typeof data === 'string') {
this.raisePostMessage(IPyWidgetMessages.IPyWidgets_msg, { id: msgUuid, data });
if (shouldMessageBeMirroredWithRenderer(data)) {
this.raisePostMessage(IPyWidgetMessages.IPyWidgets_msg, { id: msgUuid, data });
}
} else {
this.raisePostMessage(IPyWidgetMessages.IPyWidgets_binary_msg, {
id: msgUuid,
Expand All @@ -309,6 +315,9 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher {
data.includes('comm_msg');
if (mustDeserialize) {
const message = this.deserialize(data as any) as any;
if (!shouldMessageBeMirroredWithRenderer(message)) {
return;
}

// Check for hints that would indicate whether ipywidgest are used in outputs.
if (
Expand Down
Loading

0 comments on commit 5fa050d

Please sign in to comment.