From 29c4be79f64df1858692321b43c3079bb77bdd69 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 19 Jul 2022 08:07:21 -0700 Subject: [PATCH 1/8] Documentation on all missing classes (#10855) --- src/kernels/activation.node.ts | 3 +++ src/kernels/common/baseJupyterSession.ts | 3 +++ src/kernels/common/kernelConnectionWrapper.ts | 3 +++ src/kernels/common/kernelSocketWrapper.ts | 5 +++++ src/kernels/debugger/kernelDebugAdapterBase.ts | 4 ++++ src/kernels/displayOptions.ts | 3 +++ .../invalidRemoteJupyterServerUriHandleError.ts | 3 +++ .../errors/jupyterDebuggerNotInstalledError.ts | 3 +++ src/kernels/errors/jupyterInvalidKernelError.ts | 3 +++ src/kernels/errors/jupyterKernelDependencyError.ts | 3 +++ src/kernels/errors/jupyterWaitForIdleError.ts | 3 +++ src/kernels/errors/kernelConnectionTimeoutError.ts | 3 +++ src/kernels/errors/kernelDeadError.ts | 3 +++ src/kernels/errors/kernelDiedError.ts | 3 +++ src/kernels/errors/kernelErrorHandler.node.ts | 3 +++ src/kernels/errors/kernelErrorHandler.ts | 3 +++ src/kernels/errors/kernelErrorHandler.web.ts | 3 +++ src/kernels/errors/kernelInterruptTimeoutError.ts | 3 +++ src/kernels/errors/kernelPortNotUsedTimeoutError.ts | 3 +++ src/kernels/errors/kernelProcessExitedError.ts | 3 +++ .../errors/remoteJupyterServerUriProviderError.ts | 3 +++ src/kernels/execution/cellDisplayIdTracker.ts | 3 +++ src/kernels/execution/cellExecution.ts | 3 +++ .../execution/cellExecutionMessageHandlerService.ts | 3 +++ src/kernels/installer/channelManager.node.ts | 3 +++ src/kernels/installer/moduleInstaller.node.ts | 3 +++ src/kernels/installer/pipEnvInstaller.node.ts | 3 +++ src/kernels/installer/pipInstaller.node.ts | 3 +++ src/kernels/installer/poetryInstaller.node.ts | 3 +++ src/kernels/installer/productInstaller.node.ts | 12 ++++++++---- src/kernels/installer/productPath.node.ts | 3 +++ src/kernels/installer/productService.node.ts | 3 +++ src/kernels/ipywidgets/baseIPyWidgetScriptManager.ts | 3 +++ .../ipywidgets/ipyWidgetScriptManagerFactory.node.ts | 3 +++ .../ipywidgets/ipyWidgetScriptManagerFactory.web.ts | 3 +++ src/kernels/ipywidgets/ipyWidgetScriptSource.ts | 3 +++ .../ipywidgets/nbExtensionsPathProvider.node.ts | 3 +++ .../ipywidgets/nbExtensionsPathProvider.web.ts | 3 +++ .../ipywidgets/remoteIPyWidgetScriptManager.ts | 3 +++ .../ipywidgets/scriptSourceProviderFactory.node.ts | 3 +++ .../ipywidgets/scriptSourceProviderFactory.web.ts | 3 +++ src/kernels/ipywidgets/scriptUriConverter.ts | 3 +++ src/kernels/jupyter/baseKernelConnectionWrapper.ts | 3 +++ src/kernels/jupyter/commands/commandLineSelector.ts | 3 +++ src/kernels/jupyter/commands/commandRegistry.ts | 3 +++ .../jupyter/interpreter/jupyterCommand.node.ts | 3 +++ .../jupyterInterpreterOldCacheStateStore.node.ts | 3 +++ .../jupyterInterpreterSelectionCommand.node.ts | 3 +++ .../interpreter/jupyterInterpreterService.node.ts | 4 ++++ .../nbconvertExportToPythonService.node.ts | 3 +++ .../nbconvertInterpreterDependencyChecker.node.ts | 3 +++ .../jupyter/jupyterCellOutputMimeTypeTracker.node.ts | 3 +++ src/kernels/jupyter/jupyterConnection.ts | 3 +++ .../jupyter/jupyterDetectionTelemetry.node.ts | 3 +++ src/kernels/jupyter/jupyterKernelService.node.ts | 2 +- src/kernels/jupyter/jupyterKernelService.web.ts | 2 +- src/kernels/jupyter/jupyterKernelSpec.ts | 3 +++ .../jupyter/jupyterRemoteCachedKernelValidator.ts | 3 +++ .../jupyter/jupyterUriProviderRegistration.ts | 4 ++++ src/kernels/jupyter/launcher/commandLineSelector.ts | 3 +++ .../jupyter/launcher/jupyterConnection.node.ts | 3 +++ src/kernels/jupyter/launcher/jupyterExecution.ts | 4 ++++ .../jupyter/launcher/jupyterPasswordConnect.ts | 3 +++ .../launcher/liveshare/hostJupyterExecution.ts | 3 +++ .../jupyter/launcher/liveshare/hostJupyterServer.ts | 3 +++ .../launcher/liveshare/hostJupyterServerFactory.ts | 3 +++ .../jupyter/launcher/liveshare/serverCache.ts | 3 +++ src/kernels/jupyter/launcher/notebookProvider.ts | 3 +++ .../jupyter/launcher/notebookServerProvider.ts | 3 +++ .../jupyter/preferredRemoteKernelIdProvider.ts | 3 +++ src/kernels/jupyter/serverSelector.ts | 4 ++++ src/kernels/kernel.base.ts | 3 +++ src/kernels/kernel.node.ts | 3 +++ src/kernels/kernelCrashMonitor.ts | 3 +++ src/kernels/kernelFinder.base.ts | 3 +++ src/kernels/kernelFinder.node.ts | 3 +++ src/kernels/kernelFinder.web.ts | 3 +++ src/kernels/kernelProvider.base.ts | 3 +++ src/kernels/kernelProvider.node.ts | 3 +++ src/kernels/kernelProvider.web.ts | 3 +++ src/kernels/port/portAttributeProvider.node.ts | 3 +++ src/kernels/raw/finder/jupyterPaths.node.ts | 3 +++ src/kernels/raw/finder/localKernelFinder.node.ts | 2 +- .../raw/finder/localKernelSpecFinderBase.node.ts | 3 +++ .../raw/finder/pythonKernelInterruptDaemon.node.ts | 5 +++++ .../raw/launcher/kernelEnvVarsService.node.ts | 3 +++ .../raw/session/hostRawNotebookProvider.node.ts | 3 +++ .../variables/debuggerVariableRegistration.node.ts | 3 +++ src/kernels/variables/debuggerVariables.ts | 3 +++ src/kernels/variables/kernelVariables.ts | 4 ++++ src/kernels/variables/preWarmVariables.node.ts | 3 +++ src/kernels/variables/pythonVariableRequester.ts | 3 +++ 92 files changed, 285 insertions(+), 7 deletions(-) diff --git a/src/kernels/activation.node.ts b/src/kernels/activation.node.ts index 3f1466d59114..fa66e96716b8 100644 --- a/src/kernels/activation.node.ts +++ b/src/kernels/activation.node.ts @@ -17,6 +17,9 @@ import { sendTelemetryEvent } from '../telemetry'; import { JupyterInterpreterService } from './jupyter/interpreter/jupyterInterpreterService.node'; import { IRawNotebookSupportedService } from './raw/types'; +/** + * Starts up a bunch of objects when running in a node environment. + */ @injectable() export class Activation implements IExtensionSingleActivationService { private notebookOpened = false; diff --git a/src/kernels/common/baseJupyterSession.ts b/src/kernels/common/baseJupyterSession.ts index 3a1b3e829593..4fd3eb0ef338 100644 --- a/src/kernels/common/baseJupyterSession.ts +++ b/src/kernels/common/baseJupyterSession.ts @@ -70,6 +70,9 @@ export class JupyterSessionStartError extends WrappedError { } } +/** + * Common code for a Jupyterlabs IKernelConnection. Raw and Jupyter both inherit from this. + */ export abstract class BaseJupyterSession implements IBaseKernelConnectionSession { /** * Keep a single instance of KernelConnectionWrapper. diff --git a/src/kernels/common/kernelConnectionWrapper.ts b/src/kernels/common/kernelConnectionWrapper.ts index df0b9730a439..9390990065be 100644 --- a/src/kernels/common/kernelConnectionWrapper.ts +++ b/src/kernels/common/kernelConnectionWrapper.ts @@ -5,6 +5,9 @@ import type { Kernel } from '@jupyterlab/services'; import { IDisposable } from '../../platform/common/types'; import { BaseKernelConnectionWrapper } from '../jupyter/baseKernelConnectionWrapper'; +/** + * Wrapper around an IKernelConnection that's exposed to 3rd parties. + */ export class KernelConnectionWrapper extends BaseKernelConnectionWrapper { /** * Use `kernelConnection` to access the value as its not a constant (can change over time). diff --git a/src/kernels/common/kernelSocketWrapper.ts b/src/kernels/common/kernelSocketWrapper.ts index 9f24a2ca6c4f..6ab3472c3188 100644 --- a/src/kernels/common/kernelSocketWrapper.ts +++ b/src/kernels/common/kernelSocketWrapper.ts @@ -45,6 +45,11 @@ export type IWebSocketLike = { * */ +/** + * Adds send/recieve hooks to a WebSocketLike object. These are necessary for things like IPyWidgets support. + * @param SuperClass The class to mix into + * @returns + */ export function KernelSocketWrapper>(SuperClass: T) { return class BaseKernelSocket extends SuperClass implements IKernelSocket { private receiveHooks: ((data: WebSocketWS.Data) => Promise)[]; diff --git a/src/kernels/debugger/kernelDebugAdapterBase.ts b/src/kernels/debugger/kernelDebugAdapterBase.ts index 463d3b5df04d..ac9a60f2e796 100644 --- a/src/kernels/debugger/kernelDebugAdapterBase.ts +++ b/src/kernels/debugger/kernelDebugAdapterBase.ts @@ -46,6 +46,10 @@ import { IDebugService } from '../../platform/common/application/types'; * https://jupyter-client.readthedocs.io/en/stable/messaging.html#debug-request * https://jupyter-client.readthedocs.io/en/stable/messaging.html#additions-to-the-dap */ + +/** + * Base class for a debug adapter for connecting to a jupyter kernel. The DebugAdapter is responsible for translating DAP requests for a kernel. + */ export abstract class KernelDebugAdapterBase implements DebugAdapter, IKernelDebugAdapter, IDisposable { protected readonly fileToCell = new Map(); private readonly sendMessage = new EventEmitter(); diff --git a/src/kernels/displayOptions.ts b/src/kernels/displayOptions.ts index b32bad89c402..3e5f30167e7e 100644 --- a/src/kernels/displayOptions.ts +++ b/src/kernels/displayOptions.ts @@ -4,6 +4,9 @@ import { Event, EventEmitter } from 'vscode'; import { IDisplayOptions } from '../platform/common/types'; +/** + * Settings used when doing auto starts to determine if messages should be shown to the user or not. + */ export class DisplayOptions implements IDisplayOptions { private _disableUI: boolean; public get disableUI(): boolean { diff --git a/src/kernels/errors/invalidRemoteJupyterServerUriHandleError.ts b/src/kernels/errors/invalidRemoteJupyterServerUriHandleError.ts index 7644724ee368..45db4d3e7c38 100644 --- a/src/kernels/errors/invalidRemoteJupyterServerUriHandleError.ts +++ b/src/kernels/errors/invalidRemoteJupyterServerUriHandleError.ts @@ -3,6 +3,9 @@ import { BaseError } from '../../platform/errors/types'; +/** + * Thrown when an extension gives us an invalid handle for a Jupyter server + */ export class InvalidRemoteJupyterServerUriHandleError extends BaseError { constructor( public readonly providerId: string, diff --git a/src/kernels/errors/jupyterDebuggerNotInstalledError.ts b/src/kernels/errors/jupyterDebuggerNotInstalledError.ts index fda2b9ade42c..1ecaeb17dfbe 100644 --- a/src/kernels/errors/jupyterDebuggerNotInstalledError.ts +++ b/src/kernels/errors/jupyterDebuggerNotInstalledError.ts @@ -6,6 +6,9 @@ import { DataScience } from '../../platform/common/utils/localize'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown when debugpy cannot be loaded into the kernel. + */ export class JupyterDebuggerNotInstalledError extends BaseKernelError { constructor(debuggerPkg: string, message: string | undefined, kernelConnectionMetadata: KernelConnectionMetadata) { const errorMessage = message ? message : DataScience.jupyterDebuggerNotInstalledError().format(debuggerPkg); diff --git a/src/kernels/errors/jupyterInvalidKernelError.ts b/src/kernels/errors/jupyterInvalidKernelError.ts index 80ca6ab0b028..ec2337e9a39f 100644 --- a/src/kernels/errors/jupyterInvalidKernelError.ts +++ b/src/kernels/errors/jupyterInvalidKernelError.ts @@ -7,6 +7,9 @@ import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown when kernel cannot be used + */ export class JupyterInvalidKernelError extends BaseKernelError { constructor(kernelConnectionMetadata: KernelConnectionMetadata) { super( diff --git a/src/kernels/errors/jupyterKernelDependencyError.ts b/src/kernels/errors/jupyterKernelDependencyError.ts index a9d5a862f97d..84c832f61af8 100644 --- a/src/kernels/errors/jupyterKernelDependencyError.ts +++ b/src/kernels/errors/jupyterKernelDependencyError.ts @@ -8,6 +8,9 @@ import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; import { KernelConnectionMetadata, KernelInterpreterDependencyResponse } from '../types'; import { BaseKernelError } from './types'; +/** + * Control flow exception to indicate a dependency is missing in a kernel + */ export class JupyterKernelDependencyError extends BaseKernelError { constructor( public reason: KernelInterpreterDependencyResponse, diff --git a/src/kernels/errors/jupyterWaitForIdleError.ts b/src/kernels/errors/jupyterWaitForIdleError.ts index 0209f325bbd8..13defaa5df0c 100644 --- a/src/kernels/errors/jupyterWaitForIdleError.ts +++ b/src/kernels/errors/jupyterWaitForIdleError.ts @@ -7,6 +7,9 @@ import { sendTelemetryEvent, Telemetry } from '../../telemetry'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown when kernel does not come back from wait for idle. + */ export class JupyterWaitForIdleError extends BaseKernelError { constructor(kernelConnectionMetadata: KernelConnectionMetadata) { super('timeout', DataScience.jupyterLaunchTimedOut(), kernelConnectionMetadata); diff --git a/src/kernels/errors/kernelConnectionTimeoutError.ts b/src/kernels/errors/kernelConnectionTimeoutError.ts index f7f708bb5c28..f8005424450c 100644 --- a/src/kernels/errors/kernelConnectionTimeoutError.ts +++ b/src/kernels/errors/kernelConnectionTimeoutError.ts @@ -6,6 +6,9 @@ import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown when a raw kernel times out trying to connect to one of its ports. + */ export class KernelConnectionTimeoutError extends BaseKernelError { constructor(kernelConnection: KernelConnectionMetadata) { super( diff --git a/src/kernels/errors/kernelDeadError.ts b/src/kernels/errors/kernelDeadError.ts index e14f3bc41c57..9a7b85b817ae 100644 --- a/src/kernels/errors/kernelDeadError.ts +++ b/src/kernels/errors/kernelDeadError.ts @@ -6,6 +6,9 @@ import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; import { KernelConnectionMetadata } from '../types'; import { WrappedKernelError } from './types'; +/** + * Thrown when a kernel dies during restart. + */ export class KernelDeadError extends WrappedKernelError { constructor(kernelConnectionMetadata: KernelConnectionMetadata) { super( diff --git a/src/kernels/errors/kernelDiedError.ts b/src/kernels/errors/kernelDiedError.ts index a7d606d8dfa6..f3b525943902 100644 --- a/src/kernels/errors/kernelDiedError.ts +++ b/src/kernels/errors/kernelDiedError.ts @@ -4,6 +4,9 @@ import { KernelConnectionMetadata } from '../types'; import { WrappedKernelError } from './types'; +/*** + * Thrown when a kernel dies during startup + */ export class KernelDiedError extends WrappedKernelError { constructor( message: string, diff --git a/src/kernels/errors/kernelErrorHandler.node.ts b/src/kernels/errors/kernelErrorHandler.node.ts index dccce7003d6d..fce4f905c48c 100644 --- a/src/kernels/errors/kernelErrorHandler.node.ts +++ b/src/kernels/errors/kernelErrorHandler.node.ts @@ -19,6 +19,9 @@ import { JupyterKernelStartFailureOverrideReservedName } from '../../platform/in import { DataScienceErrorHandler } from './kernelErrorHandler'; import { getDisplayPath } from '../../platform/common/platform/fs-paths'; +/** + * Common code for handling errors. This one is node specific. + */ @injectable() export class DataScienceErrorHandlerNode extends DataScienceErrorHandler { constructor( diff --git a/src/kernels/errors/kernelErrorHandler.ts b/src/kernels/errors/kernelErrorHandler.ts index f02ae25fca8e..7477a97c1618 100644 --- a/src/kernels/errors/kernelErrorHandler.ts +++ b/src/kernels/errors/kernelErrorHandler.ts @@ -58,6 +58,9 @@ import { RemoteJupyterServerUriProviderError } from './remoteJupyterServerUriPro import { InvalidRemoteJupyterServerUriHandleError } from './invalidRemoteJupyterServerUriHandleError'; import { BaseKernelError, IDataScienceErrorHandler, WrappedKernelError } from './types'; +/*** + * Common code for handling errors. + */ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandler { constructor( @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, diff --git a/src/kernels/errors/kernelErrorHandler.web.ts b/src/kernels/errors/kernelErrorHandler.web.ts index 522c7a8ba2e9..525aa4a0fa12 100644 --- a/src/kernels/errors/kernelErrorHandler.web.ts +++ b/src/kernels/errors/kernelErrorHandler.web.ts @@ -4,6 +4,9 @@ import { injectable } from 'inversify'; import { Resource } from '../../platform/common/types'; import { DataScienceErrorHandler } from './kernelErrorHandler'; +/** + * Web version of common error handler. It skips some things. + */ @injectable() export class DataScienceErrorHandlerWeb extends DataScienceErrorHandler { protected override async addErrorMessageIfPythonArePossiblyOverridingPythonModules( diff --git a/src/kernels/errors/kernelInterruptTimeoutError.ts b/src/kernels/errors/kernelInterruptTimeoutError.ts index 5c2f97265829..7feb645497dd 100644 --- a/src/kernels/errors/kernelInterruptTimeoutError.ts +++ b/src/kernels/errors/kernelInterruptTimeoutError.ts @@ -5,6 +5,9 @@ import { DataScience } from '../../platform/common/utils/localize'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown when an interrupt takes too long. + */ export class KernelInterruptTimeoutError extends BaseKernelError { constructor(kernelConnection: KernelConnectionMetadata) { super('kernelpromisetimeout', DataScience.interruptingKernelFailed(), kernelConnection); diff --git a/src/kernels/errors/kernelPortNotUsedTimeoutError.ts b/src/kernels/errors/kernelPortNotUsedTimeoutError.ts index 18babdca02f7..b561a1f7944e 100644 --- a/src/kernels/errors/kernelPortNotUsedTimeoutError.ts +++ b/src/kernels/errors/kernelPortNotUsedTimeoutError.ts @@ -6,6 +6,9 @@ import { getDisplayNameOrNameOfKernelConnection } from '../helpers'; import { KernelConnectionMetadata } from '../types'; import { BaseKernelError } from './types'; +/** + * Thrown if we cannot get a port for connecting to a raw kernel. + */ export class KernelPortNotUsedTimeoutError extends BaseKernelError { constructor(kernelConnection: KernelConnectionMetadata) { super( diff --git a/src/kernels/errors/kernelProcessExitedError.ts b/src/kernels/errors/kernelProcessExitedError.ts index 477ee9d35826..4fca67826730 100644 --- a/src/kernels/errors/kernelProcessExitedError.ts +++ b/src/kernels/errors/kernelProcessExitedError.ts @@ -5,6 +5,9 @@ import { KernelConnectionMetadata } from '../types'; import { DataScience } from '../../platform/common/utils/localize'; import { BaseKernelError } from './types'; +/** + * Thrown when a raw kernel exits unexpectedly. + */ export class KernelProcessExitedError extends BaseKernelError { constructor( public readonly exitCode: number = -1, diff --git a/src/kernels/errors/remoteJupyterServerUriProviderError.ts b/src/kernels/errors/remoteJupyterServerUriProviderError.ts index 293399f79509..bbbcfe59f508 100644 --- a/src/kernels/errors/remoteJupyterServerUriProviderError.ts +++ b/src/kernels/errors/remoteJupyterServerUriProviderError.ts @@ -2,6 +2,9 @@ // Licensed under the MIT License. import { BaseError } from '../../platform/errors/types'; +/** + * Thrown when a 3rd party extension has trouble computing a jupyter server URI + */ export class RemoteJupyterServerUriProviderError extends BaseError { constructor( public readonly providerId: string, diff --git a/src/kernels/execution/cellDisplayIdTracker.ts b/src/kernels/execution/cellDisplayIdTracker.ts index f529113964aa..dfe3c6f87ca1 100644 --- a/src/kernels/execution/cellDisplayIdTracker.ts +++ b/src/kernels/execution/cellDisplayIdTracker.ts @@ -5,6 +5,9 @@ import { injectable } from 'inversify'; import { NotebookCell, NotebookCellOutput, NotebookDocument, workspace } from 'vscode'; import { isJupyterNotebook } from '../../platform/common/utils'; +/** + * Tracks a cell's display id. Some messages are sent to other cells and the display id is used to identify them. + */ @injectable() export class CellOutputDisplayIdTracker { private displayIdCellOutputMappingPerDocument = new WeakMap< diff --git a/src/kernels/execution/cellExecution.ts b/src/kernels/execution/cellExecution.ts index e92351716971..3d5498f6d842 100644 --- a/src/kernels/execution/cellExecution.ts +++ b/src/kernels/execution/cellExecution.ts @@ -33,6 +33,9 @@ import { CellExecutionMessageHandlerService } from './cellExecutionMessageHandle import { IKernelConnectionSession, KernelConnectionMetadata, NotebookCellRunState } from '../../kernels/types'; import { NotebookCellStateTracker, traceCellMessage } from './helpers'; +/** + * Factory for CellExecution objects. + */ export class CellExecutionFactory { constructor( private readonly controller: NotebookController, diff --git a/src/kernels/execution/cellExecutionMessageHandlerService.ts b/src/kernels/execution/cellExecutionMessageHandlerService.ts index 6d5c19bb2a4b..a21682dd7a16 100644 --- a/src/kernels/execution/cellExecutionMessageHandlerService.ts +++ b/src/kernels/execution/cellExecutionMessageHandlerService.ts @@ -10,6 +10,9 @@ import { IDisposable, IExtensionContext } from '../../platform/common/types'; import { CellOutputDisplayIdTracker } from './cellDisplayIdTracker'; import { CellExecutionMessageHandler } from './cellExecutionMessageHandler'; +/** + * Allows registering a CellExecutionMessageHandler for a given execution. + */ export class CellExecutionMessageHandlerService { private readonly disposables: IDisposable[] = []; private notebook?: NotebookDocument; diff --git a/src/kernels/installer/channelManager.node.ts b/src/kernels/installer/channelManager.node.ts index 24892fab5b4c..7fe4233222da 100644 --- a/src/kernels/installer/channelManager.node.ts +++ b/src/kernels/installer/channelManager.node.ts @@ -9,6 +9,9 @@ import { Installer } from '../../platform/common/utils/localize'; import { IServiceContainer } from '../../platform/ioc/types'; import { IInstallationChannelManager, IModuleInstaller, Product } from './types'; +/** + * Finds IModuleInstaller instances for a particular environment (like pip, poetry, conda). + */ @injectable() export class InstallationChannelManager implements IInstallationChannelManager { constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} diff --git a/src/kernels/installer/moduleInstaller.node.ts b/src/kernels/installer/moduleInstaller.node.ts index 20706507982a..b1370642226e 100644 --- a/src/kernels/installer/moduleInstaller.node.ts +++ b/src/kernels/installer/moduleInstaller.node.ts @@ -24,6 +24,9 @@ export type ExecutionInstallArgs = { useShellExec?: boolean; }; +/** + * Base class for all module installers. + */ export abstract class ModuleInstaller implements IModuleInstaller { public abstract get priority(): number; public abstract get name(): string; diff --git a/src/kernels/installer/pipEnvInstaller.node.ts b/src/kernels/installer/pipEnvInstaller.node.ts index e22bd0437585..b1ef09bbfdd4 100644 --- a/src/kernels/installer/pipEnvInstaller.node.ts +++ b/src/kernels/installer/pipEnvInstaller.node.ts @@ -16,6 +16,9 @@ import { getFilePath } from '../../platform/common/platform/fs-paths'; export const pipenvName = 'pipenv'; +/** + * Installer for pipenv (not the same as pip) + */ @injectable() export class PipEnvInstaller extends ModuleInstaller { constructor( diff --git a/src/kernels/installer/pipInstaller.node.ts b/src/kernels/installer/pipInstaller.node.ts index 34a4dfddf0d3..a430dbdfcdf2 100644 --- a/src/kernels/installer/pipInstaller.node.ts +++ b/src/kernels/installer/pipInstaller.node.ts @@ -12,6 +12,9 @@ import { EnvironmentType, PythonEnvironment } from '../../platform/pythonEnviron import { IServiceContainer } from '../../platform/ioc/types'; import { translateProductToModule } from './utils'; +/** + * Installer for pip. Default installer for most everything. + */ @injectable() export class PipInstaller extends ModuleInstaller { // eslint-disable-next-line @typescript-eslint/no-useless-constructor diff --git a/src/kernels/installer/poetryInstaller.node.ts b/src/kernels/installer/poetryInstaller.node.ts index c0a09fe89f5e..1fc4fff7d883 100644 --- a/src/kernels/installer/poetryInstaller.node.ts +++ b/src/kernels/installer/poetryInstaller.node.ts @@ -15,6 +15,9 @@ import { ModuleInstallerType } from './types'; export const poetryName = 'poetry'; +/** + * Installer for poetry environments. + */ @injectable() export class PoetryInstaller extends ModuleInstaller { // eslint-disable-next-line class-methods-use-this diff --git a/src/kernels/installer/productInstaller.node.ts b/src/kernels/installer/productInstaller.node.ts index a3e15f2d1593..a2af97ee9633 100644 --- a/src/kernels/installer/productInstaller.node.ts +++ b/src/kernels/installer/productInstaller.node.ts @@ -60,7 +60,10 @@ export async function isModulePresentInEnvironment(memento: Memento, product: Pr } } -abstract class BaseInstaller { +/** + * Installer for this extension. Finds the installer for a module and then runs it. + */ +export class DataScienceInstaller { protected readonly appShell: IApplicationShell; protected readonly configService: IConfigurationService; @@ -147,8 +150,9 @@ abstract class BaseInstaller { } } -export class DataScienceInstaller extends BaseInstaller {} - +/** + * Main interface to installing. + */ @injectable() export class ProductInstaller implements IInstaller { private readonly productService: IProductService; @@ -230,7 +234,7 @@ export class ProductInstaller implements IInstaller { return translateProductToModule(product); } - private createInstaller(product: Product): BaseInstaller { + private createInstaller(product: Product): DataScienceInstaller { const productType = this.productService.getProductType(product); switch (productType) { case ProductType.DataScience: diff --git a/src/kernels/installer/productPath.node.ts b/src/kernels/installer/productPath.node.ts index e0c365c53dd0..5ab1b00fd5b1 100644 --- a/src/kernels/installer/productPath.node.ts +++ b/src/kernels/installer/productPath.node.ts @@ -10,6 +10,9 @@ import { IConfigurationService } from '../../platform/common/types'; import { IServiceContainer } from '../../platform/ioc/types'; import { IInstaller, IProductPathService, ModuleNamePurpose, Product } from './types'; +/** + * Determines if a product is a module or not + */ export abstract class BaseProductPathsService implements IProductPathService { protected readonly configService: IConfigurationService; protected readonly productInstaller: IInstaller; diff --git a/src/kernels/installer/productService.node.ts b/src/kernels/installer/productService.node.ts index 780d559cf15b..940fc8adcae3 100644 --- a/src/kernels/installer/productService.node.ts +++ b/src/kernels/installer/productService.node.ts @@ -6,6 +6,9 @@ import { injectable } from 'inversify'; import { IProductService, Product, ProductType } from './types'; +/** + * Legacy code. Determines what type of installer to use for a product. We only have one, so we could probably eliminate this class. + */ @injectable() export class ProductService implements IProductService { private ProductTypes = new Map(); diff --git a/src/kernels/ipywidgets/baseIPyWidgetScriptManager.ts b/src/kernels/ipywidgets/baseIPyWidgetScriptManager.ts index 5178219c1a2f..6a79d89ef5ea 100644 --- a/src/kernels/ipywidgets/baseIPyWidgetScriptManager.ts +++ b/src/kernels/ipywidgets/baseIPyWidgetScriptManager.ts @@ -113,6 +113,9 @@ export function extractRequireConfigFromWidgetEntry(baseUrl: Uri, widgetFolderNa return requireConfig; } +/** + * Maps require config entries to the corresponding uri. + */ export abstract class BaseIPyWidgetScriptManager implements IIPyWidgetScriptManager { protected readonly disposables: IDisposable[] = []; private widgetModuleMappings?: Promise | undefined>; diff --git a/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.node.ts b/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.node.ts index 5b5196cfd0c3..629c3c35da7e 100644 --- a/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.node.ts +++ b/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.node.ts @@ -11,6 +11,9 @@ import { RemoteIPyWidgetScriptManager } from './remoteIPyWidgetScriptManager'; import { LocalIPyWidgetScriptManager } from './localIPyWidgetScriptManager.node'; import { JupyterPaths } from '../raw/finder/jupyterPaths.node'; +/** + * Determines the IPyWidgetScriptManager for use in a node environment + */ @injectable() export class IPyWidgetScriptManagerFactory implements IIPyWidgetScriptManagerFactory { private readonly managers = new WeakMap(); diff --git a/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.web.ts b/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.web.ts index 7b80fb4faf49..567dd3ca0035 100644 --- a/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.web.ts +++ b/src/kernels/ipywidgets/ipyWidgetScriptManagerFactory.web.ts @@ -9,6 +9,9 @@ import { IKernel } from '../types'; import { RemoteIPyWidgetScriptManager } from './remoteIPyWidgetScriptManager'; import { IIPyWidgetScriptManager, IIPyWidgetScriptManagerFactory } from './types'; +/** + * Determines the IPyWidgetScriptManager for use in a web environment + */ @injectable() export class IPyWidgetScriptManagerFactory implements IIPyWidgetScriptManagerFactory { private readonly managers = new WeakMap(); diff --git a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts index 0a75ff0dea70..b4b5d20127e2 100644 --- a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts +++ b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts @@ -18,6 +18,9 @@ import { createDeferred, Deferred } from '../../platform/common/utils/async'; import { ScriptUriConverter } from './scriptUriConverter'; import { ResourceMap } from '../../platform/vscode-path/map'; +/** + * Handles messages from the kernel related to setting up widgets. + */ export class IPyWidgetScriptSource { // eslint-disable-next-line @typescript-eslint/no-explicit-any public get postMessage(): Event<{ message: string; payload: any }> { diff --git a/src/kernels/ipywidgets/nbExtensionsPathProvider.node.ts b/src/kernels/ipywidgets/nbExtensionsPathProvider.node.ts index 201071a0e89f..72deb40d6a4c 100644 --- a/src/kernels/ipywidgets/nbExtensionsPathProvider.node.ts +++ b/src/kernels/ipywidgets/nbExtensionsPathProvider.node.ts @@ -6,6 +6,9 @@ import { Uri } from 'vscode'; import { IKernel } from '../types'; import { INbExtensionsPathProvider } from './types'; +/** + * Returns the path to the nbExtensions folder for a given kernel (node) + */ @injectable() export class NbExtensionsPathProvider implements INbExtensionsPathProvider { getNbExtensionsParentPath(kernel: IKernel): Uri | undefined { diff --git a/src/kernels/ipywidgets/nbExtensionsPathProvider.web.ts b/src/kernels/ipywidgets/nbExtensionsPathProvider.web.ts index 041f6fcdd6ab..e113ae0d49c1 100644 --- a/src/kernels/ipywidgets/nbExtensionsPathProvider.web.ts +++ b/src/kernels/ipywidgets/nbExtensionsPathProvider.web.ts @@ -6,6 +6,9 @@ import { Uri } from 'vscode'; import { IKernel } from '../types'; import { INbExtensionsPathProvider } from './types'; +/** + * Returns the path to the nbExtensions folder for a given kernel (web) + */ @injectable() export class NbExtensionsPathProvider implements INbExtensionsPathProvider { getNbExtensionsParentPath(kernel: IKernel): Uri | undefined { diff --git a/src/kernels/ipywidgets/remoteIPyWidgetScriptManager.ts b/src/kernels/ipywidgets/remoteIPyWidgetScriptManager.ts index 1a306c1d88d4..fb5ceb6773af 100644 --- a/src/kernels/ipywidgets/remoteIPyWidgetScriptManager.ts +++ b/src/kernels/ipywidgets/remoteIPyWidgetScriptManager.ts @@ -15,6 +15,9 @@ import { sleep } from '../../platform/common/utils/async'; import { noop } from '../../platform/common/utils/misc'; import { IFileSystem } from '../../platform/common/platform/types'; +/** + * IPyWidgetScriptManager for remote kernels + */ export class RemoteIPyWidgetScriptManager extends BaseIPyWidgetScriptManager implements IIPyWidgetScriptManager { private readonly kernelConnection: RemoteKernelConnectionMetadata; private code?: Promise; diff --git a/src/kernels/ipywidgets/scriptSourceProviderFactory.node.ts b/src/kernels/ipywidgets/scriptSourceProviderFactory.node.ts index 63683491a757..d9235b55193f 100644 --- a/src/kernels/ipywidgets/scriptSourceProviderFactory.node.ts +++ b/src/kernels/ipywidgets/scriptSourceProviderFactory.node.ts @@ -16,6 +16,9 @@ import { IApplicationShell } from '../../platform/common/application/types'; import { Memento } from 'vscode'; import { CDNWidgetScriptSourceProvider } from './cdnWidgetScriptSourceProvider'; +/** + * Returns the IWidgetScriptSourceProvider for use in a node environment + */ @injectable() export class ScriptSourceProviderFactory implements IWidgetScriptSourceProviderFactory { constructor( diff --git a/src/kernels/ipywidgets/scriptSourceProviderFactory.web.ts b/src/kernels/ipywidgets/scriptSourceProviderFactory.web.ts index e9df7cebf87b..2a1dab851fb9 100644 --- a/src/kernels/ipywidgets/scriptSourceProviderFactory.web.ts +++ b/src/kernels/ipywidgets/scriptSourceProviderFactory.web.ts @@ -15,6 +15,9 @@ import { IWidgetScriptSourceProviderFactory } from './types'; +/** + * Determines the IWidgetScriptSourceProvider for use in a web environment + */ @injectable() export class ScriptSourceProviderFactory implements IWidgetScriptSourceProviderFactory { constructor( diff --git a/src/kernels/ipywidgets/scriptUriConverter.ts b/src/kernels/ipywidgets/scriptUriConverter.ts index 5d41f1f8109c..2f6671891cc2 100644 --- a/src/kernels/ipywidgets/scriptUriConverter.ts +++ b/src/kernels/ipywidgets/scriptUriConverter.ts @@ -4,6 +4,9 @@ import { Uri } from 'vscode'; import { ILocalResourceUriConverter } from './types'; +/** + * Converts the uri of a widget script for loading in a vscode webview + */ export class ScriptUriConverter implements ILocalResourceUriConverter { constructor(private readonly isWebExtension: boolean, private readonly converter: (input: Uri) => Promise) {} diff --git a/src/kernels/jupyter/baseKernelConnectionWrapper.ts b/src/kernels/jupyter/baseKernelConnectionWrapper.ts index 6a828ed19e28..c1a8efdcc8b4 100644 --- a/src/kernels/jupyter/baseKernelConnectionWrapper.ts +++ b/src/kernels/jupyter/baseKernelConnectionWrapper.ts @@ -36,6 +36,9 @@ import { Signal } from '@lumino/signaling'; import { Disposable } from 'vscode'; import { IDisposable } from '../../platform/common/types'; +/** + * Wrapper around a Kernel.IKernelConnection. + */ export abstract class BaseKernelConnectionWrapper implements Kernel.IKernelConnection { public readonly statusChanged = new Signal(this); public readonly connectionStatusChanged = new Signal(this); diff --git a/src/kernels/jupyter/commands/commandLineSelector.ts b/src/kernels/jupyter/commands/commandLineSelector.ts index 7a6a598dd149..b29ab5edcacc 100644 --- a/src/kernels/jupyter/commands/commandLineSelector.ts +++ b/src/kernels/jupyter/commands/commandLineSelector.ts @@ -9,6 +9,9 @@ import { Commands } from '../../../platform/common/constants'; import { IDisposable } from '../../../platform/common/types'; import { JupyterCommandLineSelector } from '../launcher/commandLineSelector'; +/** + * Handles the SelectJupyterCommandLine command + */ @injectable() export class JupyterCommandLineSelectorCommand implements IDisposable { private readonly disposables: IDisposable[] = []; diff --git a/src/kernels/jupyter/commands/commandRegistry.ts b/src/kernels/jupyter/commands/commandRegistry.ts index b5fd1f9e49a2..5b63e8cedeee 100644 --- a/src/kernels/jupyter/commands/commandRegistry.ts +++ b/src/kernels/jupyter/commands/commandRegistry.ts @@ -9,6 +9,9 @@ import { IWorkspaceService } from '../../../platform/common/application/types'; import { IDisposable } from '../../../platform/common/types'; import { JupyterCommandLineSelectorCommand } from './commandLineSelector'; +/** + * Registers jupyter (non ZMQ) specific commands + */ @injectable() export class CommandRegistry implements IExtensionSingleActivationService { private readonly disposables: IDisposable[] = []; diff --git a/src/kernels/jupyter/interpreter/jupyterCommand.node.ts b/src/kernels/jupyter/interpreter/jupyterCommand.node.ts index e8b3e0ed9aa8..e5c263adb4a0 100644 --- a/src/kernels/jupyter/interpreter/jupyterCommand.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterCommand.node.ts @@ -16,6 +16,9 @@ import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { JupyterCommands, JupyterDaemonModule } from '../../../platform/common/constants'; import { IJupyterCommand, IJupyterCommandFactory } from '../types.node'; +/** + * Launches jupyter using the current python environment. + */ class InterpreterJupyterCommand implements IJupyterCommand { protected interpreterPromise: Promise; private pythonLauncher: Promise; diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts index bc8296837580..0d58c4f14ea6 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterOldCacheStateStore.node.ts @@ -17,6 +17,9 @@ type CacheInfo = { state: IPersistentState; }; +/** + * Old way to store the global jupyter interpreter + */ @injectable() export class JupyterInterpreterOldCacheStateStore { private readonly workspaceJupyterInterpreter: CacheInfo; diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterSelectionCommand.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterSelectionCommand.node.ts index 5cc7dc92fb55..1c0c05328850 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterSelectionCommand.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterSelectionCommand.node.ts @@ -10,6 +10,9 @@ import { IDisposableRegistry } from '../../../platform/common/types'; import { sendTelemetryEvent, Telemetry } from '../../../telemetry'; import { JupyterInterpreterService } from './jupyterInterpreterService.node'; +/** + * Registers the command for setting the interpreter to launch jupyter with + */ @injectable() export class JupyterInterpreterSelectionCommand implements IExtensionSingleActivationService { constructor( diff --git a/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts b/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts index 3384a03779a8..69935b63965a 100644 --- a/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts +++ b/src/kernels/jupyter/interpreter/jupyterInterpreterService.node.ts @@ -19,6 +19,10 @@ import { JupyterInterpreterSelector } from './jupyterInterpreterSelector.node'; import { JupyterInterpreterStateStore } from './jupyterInterpreterStateStore.node'; import { JupyterInterpreterDependencyResponse } from '../types'; +/** + * Manages picking an interpreter that can run jupyter. + * This interpreter is how we start jupyter on a local machine when ZMQ doesn't work. + */ @injectable() export class JupyterInterpreterService { private _selectedInterpreter?: PythonEnvironment; diff --git a/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts b/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts index d9c70e2d56da..4257c1fb0a6f 100644 --- a/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts +++ b/src/kernels/jupyter/interpreter/nbconvertExportToPythonService.node.ts @@ -12,6 +12,9 @@ import { ReportableAction } from '../../../platform/progress/types'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { JupyterDaemonModule } from '../../../platform/common/constants'; +/** + * Implements exporting using nbconvert + */ @injectable() export class NbConvertExportToPythonService { constructor(@inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory) {} diff --git a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts index 5393d76d6692..8e17efb069f6 100644 --- a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts +++ b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts @@ -14,6 +14,9 @@ import { IInstaller, Product } from '../../installer/types'; import { INbConvertInterpreterDependencyChecker } from '../types'; import { IJupyterCommandFactory } from '../types.node'; +/** + * Checks the dependencies for nbconvert. + */ @injectable() export class NbConvertInterpreterDependencyChecker implements INbConvertInterpreterDependencyChecker { // Track interpreters that nbconvert has been installed into diff --git a/src/kernels/jupyter/jupyterCellOutputMimeTypeTracker.node.ts b/src/kernels/jupyter/jupyterCellOutputMimeTypeTracker.node.ts index 9e1c4c17b1f9..3c098e8f9fa7 100644 --- a/src/kernels/jupyter/jupyterCellOutputMimeTypeTracker.node.ts +++ b/src/kernels/jupyter/jupyterCellOutputMimeTypeTracker.node.ts @@ -17,6 +17,9 @@ import { createJupyterCellFromVSCNotebookCell } from '../execution/helpers'; // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); +/** + * Sends telemetry about cell output mime types + */ @injectable() export class CellOutputMimeTypeTracker implements IExtensionSingleActivationService, IDisposable { private pendingChecks = new Map(); diff --git a/src/kernels/jupyter/jupyterConnection.ts b/src/kernels/jupyter/jupyterConnection.ts index 483cb4a86c1f..61c53c9dab01 100644 --- a/src/kernels/jupyter/jupyterConnection.ts +++ b/src/kernels/jupyter/jupyterConnection.ts @@ -23,6 +23,9 @@ import { IServerConnectionType } from './types'; +/** + * Creates IJupyterConnection objects for URIs and 3rd party handles/ids. + */ @injectable() export class JupyterConnection implements IExtensionSyncActivationService { private uriToJupyterServerUri = new Map(); diff --git a/src/kernels/jupyter/jupyterDetectionTelemetry.node.ts b/src/kernels/jupyter/jupyterDetectionTelemetry.node.ts index 4fb77a9dda11..37c75c8f48b3 100644 --- a/src/kernels/jupyter/jupyterDetectionTelemetry.node.ts +++ b/src/kernels/jupyter/jupyterDetectionTelemetry.node.ts @@ -15,6 +15,9 @@ import { sendTelemetryEvent } from '../../telemetry'; const JupyterDetectionTelemetrySentMementoKey = 'JupyterDetectionTelemetrySentMementoKey'; +/** + * Sends telemetry about whether or not jupyter is installed anywhere. + */ @injectable() export class JupyterDetectionTelemetry implements IExtensionSyncActivationService { constructor( diff --git a/src/kernels/jupyter/jupyterKernelService.node.ts b/src/kernels/jupyter/jupyterKernelService.node.ts index 74a4d1b65214..39c00aee307c 100644 --- a/src/kernels/jupyter/jupyterKernelService.node.ts +++ b/src/kernels/jupyter/jupyterKernelService.node.ts @@ -42,7 +42,7 @@ import { IJupyterKernelService } from './types'; import { arePathsSame } from '../../platform/common/platform/fileUtils'; /** - * Responsible for registering and updating kernels + * Responsible for registering and updating kernels in a non ZMQ situation (kernel specs) * * @export * @class JupyterKernelService diff --git a/src/kernels/jupyter/jupyterKernelService.web.ts b/src/kernels/jupyter/jupyterKernelService.web.ts index 3e8f3ea6d39f..12ae15d3485f 100644 --- a/src/kernels/jupyter/jupyterKernelService.web.ts +++ b/src/kernels/jupyter/jupyterKernelService.web.ts @@ -12,7 +12,7 @@ import { KernelConnectionMetadata } from '../types'; import { IJupyterKernelService } from './types'; /** - * Responsible for registering and updating kernels + * Responsible for registering and updating kernels in a web situation * * @export * @class JupyterKernelService diff --git a/src/kernels/jupyter/jupyterKernelSpec.ts b/src/kernels/jupyter/jupyterKernelSpec.ts index 7310f343752a..16074d0e3441 100644 --- a/src/kernels/jupyter/jupyterKernelSpec.ts +++ b/src/kernels/jupyter/jupyterKernelSpec.ts @@ -5,6 +5,9 @@ import type { KernelSpec } from '@jupyterlab/services'; import { PythonEnvironment_PythonApi } from '../../platform/api/types'; import { IJupyterKernelSpec } from '../types'; +/** + * Concrete implementation of a Jupyter kernel spec. + */ export class JupyterKernelSpec implements IJupyterKernelSpec { public name: string; public originalName?: string; diff --git a/src/kernels/jupyter/jupyterRemoteCachedKernelValidator.ts b/src/kernels/jupyter/jupyterRemoteCachedKernelValidator.ts index 46e7e1fd01e3..f1fc0020f9db 100644 --- a/src/kernels/jupyter/jupyterRemoteCachedKernelValidator.ts +++ b/src/kernels/jupyter/jupyterRemoteCachedKernelValidator.ts @@ -12,6 +12,9 @@ import { ILiveRemoteKernelConnectionUsageTracker } from './types'; +/** + * Used to verify remote jupyter connections from 3rd party URIs are still valid. + */ @injectable() export class JupyterRemoteCachedKernelValidator implements IJupyterRemoteCachedKernelValidator { constructor( diff --git a/src/kernels/jupyter/jupyterUriProviderRegistration.ts b/src/kernels/jupyter/jupyterUriProviderRegistration.ts index 15c80fe447a8..cb321720bd9e 100644 --- a/src/kernels/jupyter/jupyterUriProviderRegistration.ts +++ b/src/kernels/jupyter/jupyterUriProviderRegistration.ts @@ -18,6 +18,10 @@ import { } from './types'; const REGISTRATION_ID_EXTENSION_OWNER_MEMENTO_KEY = 'REGISTRATION_ID_EXTENSION_OWNER_MEMENTO_KEY'; + +/** + * Handles registration of 3rd party URI providers. + */ @injectable() export class JupyterUriProviderRegistration implements IJupyterUriProviderRegistration { private readonly _onProvidersChanged = new EventEmitter(); diff --git a/src/kernels/jupyter/launcher/commandLineSelector.ts b/src/kernels/jupyter/launcher/commandLineSelector.ts index 1b76364b7a6d..cf308b81ca5c 100644 --- a/src/kernels/jupyter/launcher/commandLineSelector.ts +++ b/src/kernels/jupyter/launcher/commandLineSelector.ts @@ -19,6 +19,9 @@ import { } from '../../../platform/common/utils/multiStepInput'; import { captureTelemetry, sendTelemetryEvent, Telemetry } from '../../../telemetry'; +/** + * Provide a quick pick to let a user select command line options for starting jupyter + */ @injectable() export class JupyterCommandLineSelector { private readonly defaultLabel = `$(zap) ${DataScience.jupyterCommandLineDefaultLabel()}`; diff --git a/src/kernels/jupyter/launcher/jupyterConnection.node.ts b/src/kernels/jupyter/launcher/jupyterConnection.node.ts index 7339dba25ecd..0d33c49bc426 100644 --- a/src/kernels/jupyter/launcher/jupyterConnection.node.ts +++ b/src/kernels/jupyter/launcher/jupyterConnection.node.ts @@ -25,6 +25,9 @@ import { getFilePath } from '../../../platform/common/platform/fs-paths'; const namedRegexp = require('named-js-regexp'); const urlMatcher = namedRegexp(RegExpValues.UrlPatternRegEx); +/** + * When starting a local jupyter server, this object waits for the server to come up. + */ export class JupyterConnectionWaiter implements IDisposable { private startPromise: Deferred; private launchTimeout: NodeJS.Timer | number; diff --git a/src/kernels/jupyter/launcher/jupyterExecution.ts b/src/kernels/jupyter/launcher/jupyterExecution.ts index 1cdfa402fae6..f4cdbacb9f43 100644 --- a/src/kernels/jupyter/launcher/jupyterExecution.ts +++ b/src/kernels/jupyter/launcher/jupyterExecution.ts @@ -31,6 +31,10 @@ import { JupyterSelfCertsExpiredError } from '../../../platform/errors/jupyterSe const LocalHosts = ['localhost', '127.0.0.1', '::1']; +/** + * Responsible for starting a Jupyter server. It's a base class because we used + * to have a host and a guest version. Host version could be consolidated into this class now. + */ export class JupyterExecutionBase implements IJupyterExecution { private usablePythonInterpreter: PythonEnvironment | undefined; private disposed: boolean = false; diff --git a/src/kernels/jupyter/launcher/jupyterPasswordConnect.ts b/src/kernels/jupyter/launcher/jupyterPasswordConnect.ts index b000f9a8ea06..890083473c91 100644 --- a/src/kernels/jupyter/launcher/jupyterPasswordConnect.ts +++ b/src/kernels/jupyter/launcher/jupyterPasswordConnect.ts @@ -16,6 +16,9 @@ import { IJupyterServerUriStorage } from '../types'; +/** + * Responsible for intercepting connections to a remote server and asking for a password if necessary + */ @injectable() export class JupyterPasswordConnect implements IJupyterPasswordConnect { private savedConnectInfo = new Map>(); diff --git a/src/kernels/jupyter/launcher/liveshare/hostJupyterExecution.ts b/src/kernels/jupyter/launcher/liveshare/hostJupyterExecution.ts index bdeab57c5f3d..d253f31c3bb0 100644 --- a/src/kernels/jupyter/launcher/liveshare/hostJupyterExecution.ts +++ b/src/kernels/jupyter/launcher/liveshare/hostJupyterExecution.ts @@ -31,6 +31,9 @@ import { JupyterConnection } from '../../jupyterConnection'; /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Jupyter server implementation that uses the JupyterExecutionBase class to launch Jupyter. + */ @injectable() export class HostJupyterExecution extends JupyterExecutionBase implements IJupyterExecution { private serverCache: ServerCache; diff --git a/src/kernels/jupyter/launcher/liveshare/hostJupyterServer.ts b/src/kernels/jupyter/launcher/liveshare/hostJupyterServer.ts index 794c2ca75786..9e524d565105 100644 --- a/src/kernels/jupyter/launcher/liveshare/hostJupyterServer.ts +++ b/src/kernels/jupyter/launcher/liveshare/hostJupyterServer.ts @@ -38,6 +38,9 @@ import { Uri } from 'vscode'; import { RemoteJupyterServerConnectionError } from '../../../../platform/errors/remoteJupyterServerConnectionError'; /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Represents a connection to a Jupyter server. + */ export class HostJupyterServer implements INotebookServer { private connectionInfoDisconnectHandler: IDisposable | undefined; private serverExitCode: number | undefined; diff --git a/src/kernels/jupyter/launcher/liveshare/hostJupyterServerFactory.ts b/src/kernels/jupyter/launcher/liveshare/hostJupyterServerFactory.ts index 421a0bbb1fa1..376e7a2358f8 100644 --- a/src/kernels/jupyter/launcher/liveshare/hostJupyterServerFactory.ts +++ b/src/kernels/jupyter/launcher/liveshare/hostJupyterServerFactory.ts @@ -13,6 +13,9 @@ import { JupyterSessionManager } from '../../session/jupyterSessionManager'; import { IJupyterSessionManagerFactory, INotebookServer, INotebookServerFactory } from '../../types'; import { HostJupyterServer } from './hostJupyterServer'; +/** + * Factory for HostJupyterServer. + */ @injectable() export class HostJupyterServerFactory implements INotebookServerFactory { constructor( diff --git a/src/kernels/jupyter/launcher/liveshare/serverCache.ts b/src/kernels/jupyter/launcher/liveshare/serverCache.ts index ffb4ed885ab9..712f5cebddf7 100644 --- a/src/kernels/jupyter/launcher/liveshare/serverCache.ts +++ b/src/kernels/jupyter/launcher/liveshare/serverCache.ts @@ -15,6 +15,9 @@ interface IServerData { resolved: boolean; } +/** + * Cache of connections to notebook servers. + */ export class ServerCache implements IAsyncDisposable { private cache: Map = new Map(); private disposed = false; diff --git a/src/kernels/jupyter/launcher/notebookProvider.ts b/src/kernels/jupyter/launcher/notebookProvider.ts index 6ba6a65b1ba0..259578a17a89 100644 --- a/src/kernels/jupyter/launcher/notebookProvider.ts +++ b/src/kernels/jupyter/launcher/notebookProvider.ts @@ -22,6 +22,9 @@ import { IRawNotebookProvider } from '../../raw/types'; import { IJupyterNotebookProvider, IServerConnectionType } from '../types'; import { sendKernelTelemetryWhenDone } from '../../telemetry/sendKernelTelemetryEvent'; +/** + * Generic class for connecting to a server. Probably could be renamed as it doesn't provide notebooks, but rather connections. + */ @injectable() export class NotebookProvider implements INotebookProvider { private readonly startupUi = new DisplayOptions(true); diff --git a/src/kernels/jupyter/launcher/notebookServerProvider.ts b/src/kernels/jupyter/launcher/notebookServerProvider.ts index 7cabdf41f958..a0ad34b9f4ef 100644 --- a/src/kernels/jupyter/launcher/notebookServerProvider.ts +++ b/src/kernels/jupyter/launcher/notebookServerProvider.ts @@ -25,6 +25,9 @@ import { NotSupportedInWebError } from '../../../platform/errors/notSupportedInW import { getFilePath } from '../../../platform/common/platform/fs-paths'; import { isCancellationError } from '../../../platform/common/cancellation'; +/** + * Starts jupyter servers locally. + */ const localCacheKey = 'LocalJupyterSererCacheKey'; @injectable() export class NotebookServerProvider implements IJupyterServerProvider { diff --git a/src/kernels/jupyter/preferredRemoteKernelIdProvider.ts b/src/kernels/jupyter/preferredRemoteKernelIdProvider.ts index 0532e3546e4e..ae2efadfc96e 100644 --- a/src/kernels/jupyter/preferredRemoteKernelIdProvider.ts +++ b/src/kernels/jupyter/preferredRemoteKernelIdProvider.ts @@ -19,6 +19,9 @@ type KernelIdListEntry = { kernelId: string | undefined; }; +/** + * Saves the preferred kernel for a given notebook + */ @injectable() export class PreferredRemoteKernelIdProvider { constructor( diff --git a/src/kernels/jupyter/serverSelector.ts b/src/kernels/jupyter/serverSelector.ts index eb704c229861..5f63b8af7286 100644 --- a/src/kernels/jupyter/serverSelector.ts +++ b/src/kernels/jupyter/serverSelector.ts @@ -63,6 +63,10 @@ export type SelectJupyterUriCommandSource = | 'nativeNotebookToolbar' | 'errorHandler' | 'prompt'; + +/** + * Provides the UI for picking a remote server. Multiplexes to one of two implementations based on the 'showOnlyOneTypeOfKernel' experiment. + */ @injectable() export class JupyterServerSelector { private impl: IJupyterServerSelector; diff --git a/src/kernels/kernel.base.ts b/src/kernels/kernel.base.ts index 7db514fda124..9493d7300d71 100644 --- a/src/kernels/kernel.base.ts +++ b/src/kernels/kernel.base.ts @@ -63,6 +63,9 @@ import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { traceCellMessage } from './execution/helpers'; import { KernelExecution } from './execution/kernelExecution'; +/** + * Represents an active kernel process running on the jupyter (or local) machine. + */ export abstract class BaseKernel implements IKernel { private readonly disposables: IDisposable[] = []; get onStatusChanged(): Event { diff --git a/src/kernels/kernel.node.ts b/src/kernels/kernel.node.ts index 64e8b0a131d8..9c17ea446660 100644 --- a/src/kernels/kernel.node.ts +++ b/src/kernels/kernel.node.ts @@ -26,6 +26,9 @@ import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { getFilePath } from '../platform/common/platform/fs-paths'; import { IFileSystem } from '../platform/common/platform/types'; +/** + * Node specific changes for a BaseKernel. + */ export class Kernel extends BaseKernel { constructor( uri: Uri, diff --git a/src/kernels/kernelCrashMonitor.ts b/src/kernels/kernelCrashMonitor.ts index 7415e2fd5fec..f4925ca34e05 100644 --- a/src/kernels/kernelCrashMonitor.ts +++ b/src/kernels/kernelCrashMonitor.ts @@ -16,6 +16,9 @@ import { getDisplayNameOrNameOfKernelConnection } from './helpers'; import { IKernel, IKernelProvider } from './types'; import { swallowExceptions } from '../platform/common/utils/decorators'; +/** + * Monitors kernel crashes and on the event of a crash will display the results in the most recent cell. + */ @injectable() export class KernelCrashMonitor implements IExtensionSyncActivationService { private lastExecutedCellPerKernel = new WeakMap(); diff --git a/src/kernels/kernelFinder.base.ts b/src/kernels/kernelFinder.base.ts index 12d264cb0df4..79c60999e5e3 100644 --- a/src/kernels/kernelFinder.base.ts +++ b/src/kernels/kernelFinder.base.ts @@ -30,6 +30,9 @@ import { export const LocalKernelSpecsCacheKey = 'JUPYTER_LOCAL_KERNELSPECS_V3'; export const RemoteKernelSpecsCacheKey = 'JUPYTER_REMOTE_KERNELSPECS_V3'; +/** + * Generic class for finding kernels (both remote and local). Handles all of the caching of the results. + */ export abstract class BaseKernelFinder implements IKernelFinder { private startTimeForFetching?: StopWatch; private fetchingTelemetrySent = new Set(); diff --git a/src/kernels/kernelFinder.node.ts b/src/kernels/kernelFinder.node.ts index 64f0a9770165..2e52bd08eac2 100644 --- a/src/kernels/kernelFinder.node.ts +++ b/src/kernels/kernelFinder.node.ts @@ -10,6 +10,9 @@ import { ILocalKernelFinder, IRemoteKernelFinder } from './raw/types'; import { INotebookProvider, KernelConnectionMetadata } from './types'; import { IFileSystem } from '../platform/common/platform/types'; +/** + * Node version of a KernelFinder. Node has different ways to validate than web. + */ @injectable() export class KernelFinder extends BaseKernelFinder { constructor( diff --git a/src/kernels/kernelFinder.web.ts b/src/kernels/kernelFinder.web.ts index fe453ab7fae5..43f823868a8d 100644 --- a/src/kernels/kernelFinder.web.ts +++ b/src/kernels/kernelFinder.web.ts @@ -9,6 +9,9 @@ import { PreferredRemoteKernelIdProvider } from './jupyter/preferredRemoteKernel import { IRemoteKernelFinder } from './raw/types'; import { INotebookProvider, KernelConnectionMetadata } from './types'; +/** + * Web version of a KernelFinder. Web has different ways to validate than node. + */ @injectable() export class KernelFinder extends BaseKernelFinder { constructor( diff --git a/src/kernels/kernelProvider.base.ts b/src/kernels/kernelProvider.base.ts index 1bdeee4a8a4d..33e0e267885d 100644 --- a/src/kernels/kernelProvider.base.ts +++ b/src/kernels/kernelProvider.base.ts @@ -11,6 +11,9 @@ import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposableRegistry } from import { noop } from '../platform/common/utils/misc'; import { IKernel, IKernelProvider, KernelOptions } from './types'; +/** + * Provides kernels to the system. Generally backed by a URI or a notebook object. + */ export abstract class BaseKernelProvider implements IKernelProvider { /** * Use a separate dictionary to track kernels by Notebook, so that diff --git a/src/kernels/kernelProvider.node.ts b/src/kernels/kernelProvider.node.ts index 7f839fe65389..2ab56531d9b2 100644 --- a/src/kernels/kernelProvider.node.ts +++ b/src/kernels/kernelProvider.node.ts @@ -20,6 +20,9 @@ import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { IFileSystem } from '../platform/common/platform/types'; +/** + * Node version of a kernel provider. Needed in order to create the node version of a kernel. + */ @injectable() export class KernelProvider extends BaseKernelProvider { constructor( diff --git a/src/kernels/kernelProvider.web.ts b/src/kernels/kernelProvider.web.ts index a9a9beb63bd3..b74c837ed2d8 100644 --- a/src/kernels/kernelProvider.web.ts +++ b/src/kernels/kernelProvider.web.ts @@ -19,6 +19,9 @@ import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { IFileSystem } from '../platform/common/platform/types'; +/** + * Web version of a kernel provider. Needed in order to create the web version of a kernel. + */ @injectable() export class KernelProvider extends BaseKernelProvider { constructor( diff --git a/src/kernels/port/portAttributeProvider.node.ts b/src/kernels/port/portAttributeProvider.node.ts index 1eed0b5fe5a3..dab5960c3979 100644 --- a/src/kernels/port/portAttributeProvider.node.ts +++ b/src/kernels/port/portAttributeProvider.node.ts @@ -10,6 +10,9 @@ import { IExtensionSyncActivationService } from '../../platform/activation/types import { traceError } from '../../platform/logging'; import { IDisposableRegistry } from '../../platform/common/types'; +/** + * Used to determine how ports can be used when creating a raw kernel. + */ @injectable() export class PortAttributesProviders implements PortAttributesProvider, IExtensionSyncActivationService { constructor(@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry) {} diff --git a/src/kernels/raw/finder/jupyterPaths.node.ts b/src/kernels/raw/finder/jupyterPaths.node.ts index b1530d60dd2f..42fa8b27ae8f 100644 --- a/src/kernels/raw/finder/jupyterPaths.node.ts +++ b/src/kernels/raw/finder/jupyterPaths.node.ts @@ -35,6 +35,9 @@ export const baseKernelPath = path.join('share', 'jupyter', 'kernels'); const CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH = 'CACHE_KEY_FOR_JUPYTER_KERNELSPEC_ROOT_PATH.'; export const CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS = 'CACHE_KEY_FOR_JUPYTER_KERNEL_PATHS_.'; +/** + * Finds locations to search for jupyter kernels. + */ @injectable() export class JupyterPaths { private cachedKernelSpecRootPath?: Promise; diff --git a/src/kernels/raw/finder/localKernelFinder.node.ts b/src/kernels/raw/finder/localKernelFinder.node.ts index 85f7fdeb77b2..35bfce3d831a 100644 --- a/src/kernels/raw/finder/localKernelFinder.node.ts +++ b/src/kernels/raw/finder/localKernelFinder.node.ts @@ -13,7 +13,7 @@ import { Resource } from '../../../platform/common/types'; import { captureTelemetry, Telemetry } from '../../../telemetry'; import { ILocalKernelFinder } from '../types'; -// This class searches for a kernel that matches the given kernel name. +// This class searches for local kernels. // First it searches on a global persistent state, then on the installed python interpreters, // and finally on the default locations that jupyter installs kernels on. @injectable() diff --git a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts index 0338c68ce6ce..c6dbddd514b8 100644 --- a/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts +++ b/src/kernels/raw/finder/localKernelSpecFinderBase.node.ts @@ -30,6 +30,9 @@ type KernelSpecFileWithContainingInterpreter = { interpreter?: PythonEnvironment export const isDefaultPythonKernelSpecSpecName = /python\s\d*.?\d*$/; export const oldKernelsSpecFolderName = '__old_vscode_kernelspecs'; +/** + * Base class for searching for local kernels that are based on a kernel spec file. + */ export abstract class LocalKernelSpecFinderBase { private _oldKernelSpecsFolder?: string; private findKernelSpecsInPathCache = new Map>(); diff --git a/src/kernels/raw/finder/pythonKernelInterruptDaemon.node.ts b/src/kernels/raw/finder/pythonKernelInterruptDaemon.node.ts index dccf057f2e09..881c6dbea6e8 100644 --- a/src/kernels/raw/finder/pythonKernelInterruptDaemon.node.ts +++ b/src/kernels/raw/finder/pythonKernelInterruptDaemon.node.ts @@ -12,6 +12,11 @@ import { IPythonExecutionService } from '../../../platform/common/process/types. import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { IPythonKernelDaemon } from '../types'; +/** + * Special daemon (process) creator to handle allowing interrupt on windows. + * On windows we need a separate process to handle an interrupt signal that we custom send from the extension. + * Things like SIGTERM don't work on windows. + */ export class PythonKernelInterruptDaemon extends BasePythonDaemon implements IPythonKernelDaemon { private killed?: boolean; // eslint-disable-next-line @typescript-eslint/no-useless-constructor diff --git a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts index 649258013ff0..96c79778a163 100644 --- a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts +++ b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts @@ -14,6 +14,9 @@ import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { IJupyterKernelSpec } from '../../types'; import { Uri } from 'vscode'; +/** + * Class used to fetch environment variables for a kernel. + */ @injectable() export class KernelEnvironmentVariablesService { constructor( diff --git a/src/kernels/raw/session/hostRawNotebookProvider.node.ts b/src/kernels/raw/session/hostRawNotebookProvider.node.ts index 736ecff93811..a13017961d63 100644 --- a/src/kernels/raw/session/hostRawNotebookProvider.node.ts +++ b/src/kernels/raw/session/hostRawNotebookProvider.node.ts @@ -31,6 +31,9 @@ import { noop } from '../../../platform/common/utils/misc'; // eslint-disable-next-line @typescript-eslint/no-require-imports /* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Implements IRawNotebookProvider for raw kernel connections. + */ @injectable() export class HostRawNotebookProvider implements IRawNotebookProvider { public get id(): string { diff --git a/src/kernels/variables/debuggerVariableRegistration.node.ts b/src/kernels/variables/debuggerVariableRegistration.node.ts index 78351e6c39d0..dee093c4c884 100644 --- a/src/kernels/variables/debuggerVariableRegistration.node.ts +++ b/src/kernels/variables/debuggerVariableRegistration.node.ts @@ -11,6 +11,9 @@ import { pythonIWKernelDebugAdapter, pythonKernelDebugAdapter } from '../debugge import { IJupyterDebugService } from '../debugger/types'; import { IJupyterVariables } from './types'; +/** + * Registes a DebugAdapter for handling variable values when debugging. + */ @injectable() export class DebuggerVariableRegistration implements IExtensionSingleActivationService, DebugAdapterTrackerFactory { constructor( diff --git a/src/kernels/variables/debuggerVariables.ts b/src/kernels/variables/debuggerVariables.ts index 98017633d78a..0a86b2f1a38d 100644 --- a/src/kernels/variables/debuggerVariables.ts +++ b/src/kernels/variables/debuggerVariables.ts @@ -34,6 +34,9 @@ import { getAssociatedNotebookDocument } from '../helpers'; const KnownExcludedVariables = new Set(['In', 'Out', 'exit', 'quit']); const MaximumRowChunkSizeForDebugger = 100; +/** + * Class responsible for computing variables while debugging. + */ @injectable() export class DebuggerVariables extends DebugLocationTracker diff --git a/src/kernels/variables/kernelVariables.ts b/src/kernels/variables/kernelVariables.ts index 96d38379c07f..5fd7a3155d53 100644 --- a/src/kernels/variables/kernelVariables.ts +++ b/src/kernels/variables/kernelVariables.ts @@ -44,6 +44,10 @@ interface INotebookState { variables: IJupyterVariable[]; } +/** + * Reponsible for providing variable data when connected to a kernel and not debugging + * (Kernels are paused while debugging so we have to use another means to query data) + */ @injectable() export class KernelVariables implements IJupyterVariables { private variableRequesters = new Map(); diff --git a/src/kernels/variables/preWarmVariables.node.ts b/src/kernels/variables/preWarmVariables.node.ts index e60b9901d755..1c85d84cd807 100644 --- a/src/kernels/variables/preWarmVariables.node.ts +++ b/src/kernels/variables/preWarmVariables.node.ts @@ -15,6 +15,9 @@ import { IEnvironmentActivationService } from '../../platform/interpreter/activa import { JupyterInterpreterService } from '../jupyter/interpreter/jupyterInterpreterService.node'; import { IRawNotebookSupportedService } from '../raw/types'; +/** + * Computes interpreter environment variables when starting up. + */ @injectable() export class PreWarmActivatedJupyterEnvironmentVariables implements IExtensionSingleActivationService { constructor( diff --git a/src/kernels/variables/pythonVariableRequester.ts b/src/kernels/variables/pythonVariableRequester.ts index 5ee49df71f55..bdfb0e172ca5 100644 --- a/src/kernels/variables/pythonVariableRequester.ts +++ b/src/kernels/variables/pythonVariableRequester.ts @@ -59,6 +59,9 @@ async function safeExecuteSilently( } } +/** + * When a kernel is a python kernel, the KernelVariables class will use this object to request variables. + */ @injectable() export class PythonVariablesRequester implements IKernelVariableRequester { constructor( From 22d43ce4b6461aa12862b2eb53c17fcc1cd68a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Rodr=C3=ADguez?= Date: Tue, 19 Jul 2022 15:53:54 -0400 Subject: [PATCH 2/8] Web version of the DataFrame viewer (#10825) * indirect changes * wip * all the changes * test fix * cleanups * unnecessary code * throwing if an interpreter is provided on the web * renamed file * Refactor: Two implementations and one service * Telemetry update * removing unnecessary injectable * removing unecessary code paths and adding the error message to the failedToInstallPandas message * telemetry update * rework of the pandas version * missed this one * sending the error through sendTelemetryEvent * localization fixes * pandasMinimumVersionSupportedByVariableViewer * isPythonEnvironment * telemetry cleanups * BaseDataViewerDependencyImplementation * coerced kernel with session * telemetry: PandasOK and PandasInstallCanceled * unlocalizing DataScience.noActiveKernelSession * Refactoring based on https://github.com/microsoft/vscode-jupyter/pull/10825#discussion_r923809738 * removed console.logs --- TELEMETRY.md | 266 +++++++++++++++++- news/1 Enhancements/9665.md | 1 + package.nls.json | 5 +- ...onvertInterpreterDependencyChecker.node.ts | 2 +- src/platform/common/constants.ts | 8 +- src/platform/common/utils.node.ts | 12 - src/platform/common/utils.ts | 12 + src/platform/common/utils/localize.ts | 14 +- src/telemetry.ts | 16 ++ .../dataViewerDependencyService.unit.test.ts | 194 +++++++------ ...ndencyServiceInterpreter.node.unit.test.ts | 132 +++++++++ ...rDependencyServiceKernel.node.unit.test.ts | 159 +++++++++++ .../baseDataViewerDependencyImplementation.ts | 94 +++++++ .../extension-side/dataviewer/constants.ts | 6 + .../dataViewerDependencyService.node.ts | 138 ++------- .../dataviewer/dataViewerDependencyService.ts | 29 ++ ...DataViewerDependencyImplementation.node.ts | 76 +++++ .../dataviewer/jupyterVariableDataProvider.ts | 5 +- ...ernelDataViewerDependencyImplementation.ts | 71 +++++ .../extension-side/dataviewer/types.ts | 5 +- .../extension-side/serviceRegistry.web.ts | 6 + .../interactive-common/variableExplorer.tsx | 1 - .../variableExplorerButtonCellFormatter.tsx | 3 +- 23 files changed, 1026 insertions(+), 229 deletions(-) create mode 100644 news/1 Enhancements/9665.md create mode 100644 src/test/datascience/data-viewing/dataViewerDependencyServiceInterpreter.node.unit.test.ts create mode 100644 src/test/datascience/data-viewing/dataViewerDependencyServiceKernel.node.unit.test.ts create mode 100644 src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts create mode 100644 src/webviews/extension-side/dataviewer/constants.ts create mode 100644 src/webviews/extension-side/dataviewer/dataViewerDependencyService.ts create mode 100644 src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts create mode 100644 src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts diff --git a/TELEMETRY.md b/TELEMETRY.md index a6d751786b8b..7e6b78d7c3b7 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -1276,6 +1276,25 @@ No properties for event No properties for event +## Locations Used + +Event can be removed. Not referenced anywhere + + +
+ DATASCIENCE.FAILED_TO_INSTALL_PANDAS + +## Description + + +No description provided + +## Properties + + +No properties for event + + ## Locations Used Event can be removed. Not referenced anywhere @@ -2636,6 +2655,37 @@ No description provided Event can be removed. Not referenced anywhere +
+
+ DATASCIENCE.NO_ACTIVE_KERNEL_SESSION + +## Description + + + + + Useful when we need an active kernel session in order to execute commands silently. + Used by the dataViewerDependencyService. + +## Properties + + +No properties for event + + +## Locations Used + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + sendTelemetryEvent(Telemetry.DataViewerUsingKernel); + + if (!kernelHasSession(kernel)) { + sendTelemetryEvent(Telemetry.NoActiveKernelSession); + throw new Error('No no active kernel session.'); + } + +``` +
DATASCIENCE.NOTEBOOK_INTERRUPT @@ -4546,15 +4596,27 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts) +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript sendTelemetryEvent(Telemetry.UserInstalledPandas); } } else { sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); - throw new Error(DataScience.pandasRequiredForViewing()); - } - } + throw new Error( + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) + ); +``` + + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + throw new Error(DataScience.failedToInstallPandas()); + } + } else { + sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); + throw new Error( + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) + ); ```
@@ -4626,7 +4688,7 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts) +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript cancellationPromise ]); @@ -4637,6 +4699,30 @@ No properties for event sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); ``` + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + if (await this.promptInstall()) { + try { + await this.execute(command, kernel); + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } catch (e) { + sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); + throw new Error(DataScience.failedToInstallPandas()); +``` + + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + await this.execute(command, kernel); + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } catch (e) { + sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); + throw new Error(DataScience.failedToInstallPandas()); + } + } else { +``` +
DATASCIENCE.USER_STARTUP_CODE_FAILURE @@ -4780,6 +4866,66 @@ No properties for event Event can be removed. Not referenced anywhere +
+
+ DATAVIEWER.USING_INTERPRETER + +## Description + + + + + When the Data Viewer installer is using the Python interpreter. + +## Properties + + +No properties for event + + +## Locations Used + +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +```typescript + } + + public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise { + sendTelemetryEvent(Telemetry.DataViewerUsingInterpreter); + + const tokenSource = new CancellationTokenSource(); + try { +``` + +
+
+ DATAVIEWER.USING_KERNEL + +## Description + + + + + When the Data Viewer installer is using the Kernel. + +## Properties + + +No properties for event + + +## Locations Used + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + } + + async checkAndInstallMissingDependencies(kernel: IKernel): Promise { + sendTelemetryEvent(Telemetry.DataViewerUsingKernel); + + if (!kernelHasSession(kernel)) { + sendTelemetryEvent(Telemetry.NoActiveKernelSession); +``` +
DS_INTERNAL.ACTIVE_INTERPRETER_LISTING_PERF @@ -7612,7 +7758,7 @@ No description provided ``` -[src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts) +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript interpreter: PythonEnvironment, tokenSource: CancellationTokenSource @@ -7623,6 +7769,18 @@ No description provided pythonEnvType: interpreter?.envType ``` + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + } + + private async installMissingDependencies(kernel: IKernelWithSession): Promise { + sendTelemetryEvent(Telemetry.PythonModuleInstall, undefined, { + action: 'displayed', + moduleName: ProductNames.get(Product.pandas)! + }); +``` +
DS_INTERNAL.PYTHON_NOT_INSTALLED @@ -8511,7 +8669,7 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts) +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); } @@ -8522,6 +8680,84 @@ No properties for event tokenSource.dispose(); ``` + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); + } + + sendTelemetryEvent(Telemetry.PandasNotInstalled); + + await this.installMissingDependencies(kernel); + } +``` + +
+
+ DS_INTERNAL.SHOW_DATA_PANDAS_INSTALL_CANCELED + +## Description + + +No description provided + +## Properties + +- version: string + +## Locations Used + +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +```typescript + const pandasVersion = await this.getVersion(interpreter, tokenSource.token); + + if (Cancellation.isCanceled(tokenSource.token)) { + sendTelemetryEvent(Telemetry.PandasInstallCanceled); + return; + } + +``` + +
+
+ DS_INTERNAL.SHOW_DATA_PANDAS_OK + +## Description + + +No description provided + +## Properties + + +No properties for event + + +## Locations Used + +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +```typescript + + if (pandasVersion) { + if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) { + sendTelemetryEvent(Telemetry.PandasOK); + return; + } + sendTelemetryEvent(Telemetry.PandasTooOld); +``` + + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + + if (pandasVersion) { + if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) { + sendTelemetryEvent(Telemetry.PandasOK); + return; + } + sendTelemetryEvent(Telemetry.PandasTooOld); +``` +
DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD @@ -8539,9 +8775,9 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts) +[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript - if (isVersionOfPandasSupported(pandasVersion)) { + sendTelemetryEvent(Telemetry.PandasOK); return; } sendTelemetryEvent(Telemetry.PandasTooOld); @@ -8550,6 +8786,18 @@ No properties for event throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); ``` + +[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +```typescript + sendTelemetryEvent(Telemetry.PandasOK); + return; + } + sendTelemetryEvent(Telemetry.PandasTooOld); + // Warn user that we cannot start because pandas is too old. + const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; + throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); +``` +
DS_INTERNAL.START_EXECUTE_NOTEBOOK_CELL_PERCEIVED_COLD diff --git a/news/1 Enhancements/9665.md b/news/1 Enhancements/9665.md new file mode 100644 index 000000000000..338a427afc7e --- /dev/null +++ b/news/1 Enhancements/9665.md @@ -0,0 +1 @@ +DataFrame viewer enabled on the web. \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 30a9f6dbfd95..296119706c1f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -673,8 +673,9 @@ }, "DataScience.serverNotStarted": "Not Started", "DataScience.localJupyterServer": "local", - "DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", - "DataScience.pandasRequiredForViewing": "Python package 'pandas' is required for viewing data.", + "DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version {1} or greater is required for viewing data.", + "DataScience.pandasRequiredForViewing": "Python package 'pandas' version {0} (or above) is required for viewing data.", + "DataScience.failedToGetVersionOfPandas": "Failed to get version of Pandas to use the Data Viewer.", "DataScience.valuesColumn": "values", "DataScience.liveShareInvalid": "One or more guests in the session do not have the Jupyter [extension](https://marketplace.visualstudio.com/itemdetails?itemName=ms-toolsai.jupyter) installed.\r\nYour Live Share session cannot continue and will be closed.", "Common.noIWillDoItLater": "No, I will do it later", diff --git a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts index 8e17efb069f6..de3bd46e8647 100644 --- a/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts +++ b/src/kernels/jupyter/interpreter/nbconvertInterpreterDependencyChecker.node.ts @@ -6,7 +6,7 @@ import { inject, injectable } from 'inversify'; import { SemVer } from 'semver'; import { CancellationToken } from 'vscode'; -import { parseSemVer } from '../../../platform/common/utils.node'; +import { parseSemVer } from '../../../platform/common/utils'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { ResourceSet } from '../../../platform/vscode-path/map'; import { JupyterCommands } from '../../../telemetry'; diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index 6c3a2d15f6b2..0ef235cb3c10 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -449,6 +449,8 @@ export enum Telemetry { DebugFileInteractive = 'DATASCIENCE.DEBUG_FILE_INTERACTIVE', PandasNotInstalled = 'DS_INTERNAL.SHOW_DATA_NO_PANDAS', PandasTooOld = 'DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD', + PandasOK = 'DS_INTERNAL.SHOW_DATA_PANDAS_OK', + PandasInstallCanceled = 'DS_INTERNAL.SHOW_DATA_PANDAS_INSTALL_CANCELED', DataScienceSettings = 'DS_INTERNAL.SETTINGS', VariableExplorerToggled = 'DATASCIENCE.VARIABLE_EXPLORER_TOGGLE', VariableExplorerVariableCount = 'DS_INTERNAL.VARIABLE_EXPLORER_VARIABLE_COUNT', @@ -520,6 +522,7 @@ export enum Telemetry { UserInstalledPandas = 'DATASCIENCE.USER_INSTALLED_PANDAS', UserDidNotInstallJupyter = 'DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER', UserDidNotInstallPandas = 'DATASCIENCE.USER_DID_NOT_INSTALL_PANDAS', + FailedToInstallPandas = 'DATASCIENCE.FAILED_TO_INSTALL_PANDAS', OpenedInteractiveWindow = 'DATASCIENCE.OPENED_INTERACTIVE', OpenNotebookFailure = 'DS_INTERNAL.NATIVE.OPEN_NOTEBOOK_FAILURE', FindKernelForLocalConnection = 'DS_INTERNAL.FIND_KERNEL_FOR_LOCAL_CONNECTION', @@ -641,7 +644,10 @@ export enum Telemetry { FetchError = 'DS_INTERNAL.WEB_FETCH_ERROR', TerminalShellIdentification = 'TERMINAL_SHELL_IDENTIFICATION', TerminalEnvVariableExtraction = 'TERMINAL_ENV_VAR_EXTRACTION', - JupyterInstalled = 'JUPYTER_IS_INSTALLED' + JupyterInstalled = 'JUPYTER_IS_INSTALLED', + NoActiveKernelSession = 'DATASCIENCE.NO_ACTIVE_KERNEL_SESSION', + DataViewerUsingInterpreter = 'DATAVIEWER.USING_INTERPRETER', + DataViewerUsingKernel = 'DATAVIEWER.USING_KERNEL' } export enum NativeKeyboardCommandTelemetry { diff --git a/src/platform/common/utils.node.ts b/src/platform/common/utils.node.ts index ec36b03bab47..eefb60ec0785 100644 --- a/src/platform/common/utils.node.ts +++ b/src/platform/common/utils.node.ts @@ -4,7 +4,6 @@ import * as path from '../../platform/vscode-path/path'; import * as fsExtra from 'fs-extra'; -import { SemVer, parse } from 'semver'; import { Uri } from 'vscode'; import { IWorkspaceService } from './application/types'; import { IConfigurationService, Resource } from './types'; @@ -71,14 +70,3 @@ export async function calculateWorkingDirectory( } return workingDir; } - -// For the given string parse it out to a SemVer or return undefined -export function parseSemVer(versionString: string): SemVer | undefined { - const versionMatch = /^\s*(\d+)\.(\d+)\.(.+)\s*$/.exec(versionString); - if (versionMatch && versionMatch.length > 2) { - const major = parseInt(versionMatch[1], 10); - const minor = parseInt(versionMatch[2], 10); - const build = parseInt(versionMatch[3], 10); - return parse(`${major}.${minor}.${build}`, true) ?? undefined; - } -} diff --git a/src/platform/common/utils.ts b/src/platform/common/utils.ts index 9c6f82221c05..d1b15dff9b7a 100644 --- a/src/platform/common/utils.ts +++ b/src/platform/common/utils.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; +import { SemVer, parse } from 'semver'; import type * as nbformat from '@jupyterlab/nbformat'; import * as uriPath from '../../platform/vscode-path/resources'; import { NotebookData, NotebookDocument, TextDocument, Uri, workspace } from 'vscode'; @@ -366,3 +367,14 @@ export function removeLinesFromFrontAndBack(code: string | string[]): string { const lines = Array.isArray(code) ? code : code.splitLines({ trim: false, removeEmptyEntries: false }); return removeLinesFromFrontAndBackNoConcat(lines).join('\n'); } + +// For the given string parse it out to a SemVer or return undefined +export function parseSemVer(versionString: string): SemVer | undefined { + const versionMatch = /^\s*(\d+)\.(\d+)\.(.+)\s*$/.exec(versionString); + if (versionMatch && versionMatch.length > 2) { + const major = parseInt(versionMatch[1], 10); + const minor = parseInt(versionMatch[2], 10); + const build = parseInt(versionMatch[3], 10); + return parse(`${major}.${minor}.${build}`, true) ?? undefined; + } +} diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index b4633be16f94..d604bed54286 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -800,7 +800,7 @@ export namespace DataScience { key: 'DataScience.pandasTooOldForViewingFormat', comment: ["{Locked='pandas'", 'This is the name of the pandas package'] }, - "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data." + "Python package 'pandas' is version {0}. Version {1} or greater is required for viewing data." ); export const pandasRequiredForViewing = () => localize( @@ -808,7 +808,7 @@ export namespace DataScience { key: 'DataScience.pandasRequiredForViewing', comment: ["{Locked='pandas'", 'This is the name of the pandas package'] }, - "Python package 'pandas' is required for viewing data." + "Python package 'pandas' version {0} (or above) is required for viewing data." ); export const valuesColumn = () => localize('DataScience.valuesColumn', 'values'); export const liveShareInvalid = () => @@ -1396,6 +1396,16 @@ export namespace DataScience { ); export const listOfFilesWithLinksThatMightNeedToBeRenamed = () => localize('DataScience.listOfFilesWithLinksThatMightNeedToBeRenamed', 'File(s): {0} might need to be renamed.'); + export const failedToGetVersionOfPandas = () => + localize( + { key: 'DataScience.failedToGetVersionOfPandas', comment: ['{Locked="Pandas"}'] }, + 'Failed to get version of Pandas to use the Data Viewer.' + ); + export const failedToInstallPandas = () => + localize( + { key: 'DataScience.failedToInstallPandas', comment: ['{Locked="Pandas"}'] }, + 'Failed to install Pandas to use the Data Viewer.' + ); } export namespace Deprecated { diff --git a/src/telemetry.ts b/src/telemetry.ts index af3c8562df08..c32de9e47dce 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -408,6 +408,8 @@ export interface IEventNamePropertyMapping { [Telemetry.NotebookOpenTime]: number; [Telemetry.PandasNotInstalled]: never | undefined; [Telemetry.PandasTooOld]: never | undefined; + [Telemetry.PandasOK]: never | undefined; + [Telemetry.PandasInstallCanceled]: { version: string }; [Telemetry.DebugpyInstallCancelled]: never | undefined; [Telemetry.DebugpyInstallFailed]: never | undefined; [Telemetry.DebugpyPromptToInstall]: never | undefined; @@ -494,6 +496,7 @@ export interface IEventNamePropertyMapping { [Telemetry.UserInstalledPandas]: never | undefined; [Telemetry.UserDidNotInstallJupyter]: never | undefined; [Telemetry.UserDidNotInstallPandas]: never | undefined; + [Telemetry.FailedToInstallPandas]: never | undefined; [Telemetry.PythonNotInstalled]: { action: | 'displayed' // Message displayed. @@ -1374,4 +1377,17 @@ export interface IEventNamePropertyMapping { * Total time take to copy the nb extensions folder. */ [Telemetry.IPyWidgetNbExtensionCopyTime]: never | undefined; + /** + * Useful when we need an active kernel session in order to execute commands silently. + * Used by the dataViewerDependencyService. + */ + [Telemetry.NoActiveKernelSession]: never | undefined; + /** + * When the Data Viewer installer is using the Python interpreter. + */ + [Telemetry.DataViewerUsingInterpreter]: never | undefined; + /** + * When the Data Viewer installer is using the Kernel. + */ + [Telemetry.DataViewerUsingKernel]: never | undefined; } diff --git a/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts b/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts index 7366f028a5b4..fef55f7c601b 100644 --- a/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts +++ b/src/test/datascience/data-viewing/dataViewerDependencyService.unit.test.ts @@ -4,118 +4,138 @@ 'use strict'; import { assert } from 'chai'; -import * as path from '../../../platform/vscode-path/path'; -import { SemVer } from 'semver'; -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, when } from 'ts-mockito'; import { ApplicationShell } from '../../../platform/common/application/applicationShell'; import { IApplicationShell } from '../../../platform/common/application/types'; -import { PythonExecutionFactory } from '../../../platform/common/process/pythonExecutionFactory.node'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../../../platform/common/process/types.node'; +import { DataViewerDependencyService } from '../../../webviews/extension-side/dataviewer/dataViewerDependencyService'; +import { IKernel } from '../../../kernels/types'; import { Common, DataScience } from '../../../platform/common/utils/localize'; -import { IInterpreterService } from '../../../platform/interpreter/contracts'; -import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; -import { ProductInstaller } from '../../../kernels/installer/productInstaller.node'; -import { IInstaller, Product } from '../../../kernels/installer/types'; -import { DataViewerDependencyService } from '../../../webviews/extension-side/dataviewer/dataViewerDependencyService.node'; -import { Uri } from 'vscode'; - -suite('DataScience - DataViewerDependencyService', () => { +import * as helpers from '../../../kernels/helpers'; +import * as sinon from 'sinon'; +import { kernelGetPandasVersion } from '../../../webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation'; +import { pandasMinimumVersionSupportedByVariableViewer } from '../../../webviews/extension-side/dataviewer/constants'; + +suite('DataScience - DataViewerDependencyService (IKernel, Web)', () => { let dependencyService: DataViewerDependencyService; let appShell: IApplicationShell; - let pythonExecFactory: IPythonExecutionFactory; - let installer: IInstaller; - let interpreter: PythonEnvironment; - let interpreterService: IInterpreterService; - let pythonExecService: IPythonExecutionService; + let kernel: IKernel; + setup(async () => { - interpreter = { - displayName: '', - uri: Uri.file(path.join('users', 'python', 'bin', 'python.exe')), - sysPrefix: '', - sysVersion: '', - version: new SemVer('3.3.3') - }; - pythonExecService = mock(); - installer = mock(ProductInstaller); appShell = mock(ApplicationShell); - pythonExecFactory = mock(PythonExecutionFactory); - interpreterService = mock(); - - dependencyService = new DataViewerDependencyService( - instance(appShell), - instance(installer), - instance(pythonExecFactory), - instance(interpreterService), - false - ); + kernel = instance(mock()); + dependencyService = new DataViewerDependencyService(instance(appShell), false); + }); - when(interpreterService.getActiveInterpreter()).thenResolve(interpreter); - when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (instance(pythonExecService) as any).then = undefined; + teardown(() => { + sinon.restore(); + }); + + test('What if there are no kernel sessions?', async () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (pythonExecService as any).then = undefined; - when(pythonExecFactory.createActivatedEnvironment(anything())).thenResolve(instance(pythonExecService)); + (kernel.session as any) = undefined; + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + + await assert.isRejected( + resultPromise, + 'No no active kernel session.', + 'Failed to determine if there was an active kernel session' + ); }); + test('All ok, if pandas is installed and version is > 1.20', async () => { - when( - pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) - ).thenResolve({ stdout: '0.30.0' }); - await dependencyService.checkAndInstallMissingDependencies(interpreter); + const version = '3.3.3'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const result = await dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(result, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); }); + + test('All ok, if pandas is installed and version is > 1.20, even if the command returns with a new line', async () => { + const version = '1.4.2\n'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const result = await dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(result, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); + test('Throw exception if pandas is installed and version is = 0.20', async () => { - when( - pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) - ).thenResolve({ stdout: '0.20.0' }); + const version = '0.20.0'; - const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); - await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.20.')); + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer), + 'Failed to identify too old pandas' + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); }); + test('Throw exception if pandas is installed and version is < 0.20', async () => { - when( - pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) - ).thenResolve({ stdout: '0.10.0' }); + const version = '0.10.0'; - const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); - await assert.isRejected(promise, DataScience.pandasTooOldForViewingFormat().format('0.10.')); + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer), + 'Failed to identify too old pandas' + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); }); - test('Prompt to install pandas and install pandas', async () => { - when( - pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) - ).thenReject(new Error('Not Found')); + + test('Prompt to install pandas, then install pandas', async () => { + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: '' }])); + // eslint-disable-next-line @typescript-eslint/no-explicit-any when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(Common.install() as any); - when(installer.install(Product.pandas, interpreter, anything())).thenResolve(); - - await dependencyService.checkAndInstallMissingDependencies(interpreter); - - verify( - appShell.showErrorMessage( - DataScience.pandasRequiredForViewing(), - deepEqual({ modal: true }), - Common.install() - ) - ).once(); - verify(installer.install(Product.pandas, interpreter, anything())).once(); + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(await resultPromise, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion, '%pip install pandas'] + ); }); + test('Prompt to install pandas and throw error if user does not install pandas', async () => { - when( - pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) - ).thenReject(new Error('Not Found')); + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: '' }])); + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(); - const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); - - await assert.isRejected(promise, DataScience.pandasRequiredForViewing()); - verify( - appShell.showErrorMessage( - DataScience.pandasRequiredForViewing(), - deepEqual({ modal: true }), - Common.install() - ) - ).once(); - verify(installer.install(anything(), anything(), anything())).never(); + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); }); }); diff --git a/src/test/datascience/data-viewing/dataViewerDependencyServiceInterpreter.node.unit.test.ts b/src/test/datascience/data-viewing/dataViewerDependencyServiceInterpreter.node.unit.test.ts new file mode 100644 index 000000000000..64aedb03f017 --- /dev/null +++ b/src/test/datascience/data-viewing/dataViewerDependencyServiceInterpreter.node.unit.test.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as path from '../../../platform/vscode-path/path'; +import { SemVer } from 'semver'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../platform/common/application/applicationShell'; +import { IApplicationShell } from '../../../platform/common/application/types'; +import { PythonExecutionFactory } from '../../../platform/common/process/pythonExecutionFactory.node'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../platform/common/process/types.node'; +import { Common, DataScience } from '../../../platform/common/utils/localize'; +import { IInterpreterService } from '../../../platform/interpreter/contracts'; +import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; +import { ProductInstaller } from '../../../kernels/installer/productInstaller.node'; +import { IInstaller, Product } from '../../../kernels/installer/types'; +import { DataViewerDependencyService } from '../../../webviews/extension-side/dataviewer/dataViewerDependencyService.node'; +import { Uri } from 'vscode'; +import { pandasMinimumVersionSupportedByVariableViewer } from '../../../webviews/extension-side/dataviewer/constants'; + +suite('DataScience - DataViewerDependencyService (PythonEnvironment, Node)', () => { + let dependencyService: DataViewerDependencyService; + let appShell: IApplicationShell; + let pythonExecFactory: IPythonExecutionFactory; + let installer: IInstaller; + let interpreter: PythonEnvironment; + let interpreterService: IInterpreterService; + let pythonExecService: IPythonExecutionService; + + setup(async () => { + interpreter = { + displayName: '', + uri: Uri.file(path.join('users', 'python', 'bin', 'python.exe')), + sysPrefix: '', + sysVersion: '', + version: new SemVer('3.3.3') + }; + pythonExecService = mock(); + installer = mock(ProductInstaller); + appShell = mock(ApplicationShell); + pythonExecFactory = mock(PythonExecutionFactory); + interpreterService = mock(); + + dependencyService = new DataViewerDependencyService( + instance(installer), + instance(pythonExecFactory), + instance(interpreterService), + instance(appShell), + false + ); + + when(interpreterService.getActiveInterpreter()).thenResolve(interpreter); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (instance(pythonExecService) as any).then = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (pythonExecService as any).then = undefined; + when(pythonExecFactory.createActivatedEnvironment(anything())).thenResolve(instance(pythonExecService)); + }); + test('All ok, if pandas is installed and version is > 1.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.30.0' }); + await dependencyService.checkAndInstallMissingDependencies(interpreter); + }); + test('Throw exception if pandas is installed and version is = 0.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.20.0' }); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected( + promise, + DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer) + ); + }); + test('Throw exception if pandas is installed and version is < 0.20', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenResolve({ stdout: '0.10.0' }); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected( + promise, + DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer) + ); + }); + test('Prompt to install pandas and install pandas', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenReject(new Error('Not Found')); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(Common.install() as any); + when(installer.install(Product.pandas, interpreter, anything())).thenResolve(); + + await dependencyService.checkAndInstallMissingDependencies(interpreter); + + verify( + appShell.showErrorMessage( + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer), + deepEqual({ modal: true }), + Common.install() + ) + ).once(); + verify(installer.install(Product.pandas, interpreter, anything())).once(); + }); + test('Prompt to install pandas and throw error if user does not install pandas', async () => { + when( + pythonExecService.exec(deepEqual(['-c', 'import pandas;print(pandas.__version__)']), anything()) + ).thenReject(new Error('Not Found')); + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(); + + const promise = dependencyService.checkAndInstallMissingDependencies(interpreter); + + await assert.isRejected( + promise, + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) + ); + verify( + appShell.showErrorMessage( + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer), + deepEqual({ modal: true }), + Common.install() + ) + ).once(); + verify(installer.install(anything(), anything(), anything())).never(); + }); +}); diff --git a/src/test/datascience/data-viewing/dataViewerDependencyServiceKernel.node.unit.test.ts b/src/test/datascience/data-viewing/dataViewerDependencyServiceKernel.node.unit.test.ts new file mode 100644 index 000000000000..35616a8b6802 --- /dev/null +++ b/src/test/datascience/data-viewing/dataViewerDependencyServiceKernel.node.unit.test.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../platform/common/application/applicationShell'; +import { IApplicationShell } from '../../../platform/common/application/types'; +import { DataViewerDependencyService } from '../../../webviews/extension-side/dataviewer/dataViewerDependencyService.node'; +import { IKernel } from '../../../kernels/types'; +import { Common, DataScience } from '../../../platform/common/utils/localize'; +import * as helpers from '../../../kernels/helpers'; +import * as sinon from 'sinon'; +import { kernelGetPandasVersion } from '../../../webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation'; +import { IPythonExecutionFactory } from '../../../platform/common/process/types.node'; +import { IInstaller } from '../../../kernels/installer/types'; +import { IInterpreterService } from '../../../platform/interpreter/contracts'; +import { ProductInstaller } from '../../../kernels/installer/productInstaller.node'; +import { PythonExecutionFactory } from '../../../platform/common/process/pythonExecutionFactory.node'; +import { pandasMinimumVersionSupportedByVariableViewer } from '../../../webviews/extension-side/dataviewer/constants'; + +suite('DataScience - DataViewerDependencyService (IKernel, Node)', () => { + let dependencyService: DataViewerDependencyService; + let appShell: IApplicationShell; + let pythonExecFactory: IPythonExecutionFactory; + let installer: IInstaller; + let interpreterService: IInterpreterService; + let kernel: IKernel; + + setup(async () => { + installer = mock(ProductInstaller); + appShell = mock(ApplicationShell); + pythonExecFactory = mock(PythonExecutionFactory); + interpreterService = mock(); + kernel = instance(mock()); + + dependencyService = new DataViewerDependencyService( + instance(installer), + instance(pythonExecFactory), + instance(interpreterService), + instance(appShell), + false + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('What if there are no kernel sessions?', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (kernel.session as any) = undefined; + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + + await assert.isRejected( + resultPromise, + 'No no active kernel session.', + 'Failed to determine if there was an active kernel session' + ); + }); + + test('All ok, if pandas is installed and version is > 1.20', async () => { + const version = '3.3.3'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const result = await dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(result, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); + + test('All ok, if pandas is installed and version is > 1.20, even if the command returns with a new line', async () => { + const version = '1.4.2\n'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const result = await dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(result, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); + + test('Throw exception if pandas is installed and version is = 0.20', async () => { + const version = '0.20.0'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasTooOldForViewingFormat().format('0.20.', pandasMinimumVersionSupportedByVariableViewer), + 'Failed to identify too old pandas' + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); + + test('Throw exception if pandas is installed and version is < 0.20', async () => { + const version = '0.10.0'; + + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: version }])); + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasTooOldForViewingFormat().format('0.10.', pandasMinimumVersionSupportedByVariableViewer), + 'Failed to identify too old pandas' + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); + + test('Prompt to install pandas, then install pandas', async () => { + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: '' }])); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(Common.install() as any); + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + assert.equal(await resultPromise, undefined); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion, '%pip install pandas'] + ); + }); + + test('Prompt to install pandas and throw error if user does not install pandas', async () => { + const stub = sinon.stub(helpers, 'executeSilently'); + stub.returns(Promise.resolve([{ ename: 'stdout', output_type: 'stream', text: '' }])); + + when(appShell.showErrorMessage(anything(), anything(), anything())).thenResolve(); + + const resultPromise = dependencyService.checkAndInstallMissingDependencies(kernel); + await assert.isRejected( + resultPromise, + DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) + ); + assert.deepEqual( + stub.getCalls().map((call) => call.lastArg), + [kernelGetPandasVersion] + ); + }); +}); diff --git a/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts b/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts new file mode 100644 index 000000000000..add42a7b8e4f --- /dev/null +++ b/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IApplicationShell } from '../../../platform/common/application/types'; +import { DataScience, Common } from '../../../platform/common/utils/localize'; +import { IKernel } from '../../../kernels/types'; +import { IDataViewerDependencyService } from './types'; +import { pandasMinimumVersionSupportedByVariableViewer } from './constants'; +import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; +import { parseSemVer } from '../../../platform/common/utils'; +import { SemVer } from 'semver'; +import { captureTelemetry, sendTelemetryEvent, Telemetry } from '../../../telemetry'; +import { ProductNames } from '../../../kernels/installer/productNames'; +import { Product } from '../../../kernels/installer/types'; +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { Cancellation } from '../../../platform/common/cancellation'; +import { traceWarning } from '../../../platform/logging'; + +/** + * base class of the data viewer dependency implementation. + */ +export abstract class BaseDataViewerDependencyImplementation implements IDataViewerDependencyService { + constructor(private readonly applicationShell: IApplicationShell, private isCodeSpace: boolean) {} + + abstract checkAndInstallMissingDependencies(executionEnvironment: IKernel | PythonEnvironment): Promise; + + protected abstract _getVersion(executer: TExecuter, token: CancellationToken): Promise; + protected abstract _doInstall(executer: TExecuter, tokenSource: CancellationTokenSource): Promise; + + protected async getVersion(executer: TExecuter, token: CancellationToken): Promise { + try { + const version = await this._getVersion(executer, token); + return typeof version === 'string' ? parseSemVer(version) : version; + } catch (e) { + traceWarning(DataScience.failedToGetVersionOfPandas(), e.message); + return; + } + } + + @captureTelemetry(Telemetry.PythonModuleInstall, { + action: 'displayed', + moduleName: ProductNames.get(Product.pandas)! + }) + protected async promptInstall( + executer: TExecuter, + tokenSource: CancellationTokenSource, + version?: string + ): Promise { + let message = version + ? DataScience.pandasTooOldForViewingFormat().format(version, pandasMinimumVersionSupportedByVariableViewer) + : DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer); + + let selection = this.isCodeSpace + ? Common.install() + : await this.applicationShell.showErrorMessage(message, { modal: true }, Common.install()); + + if (selection === Common.install()) { + await this._doInstall(executer, tokenSource); + } else { + sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); + throw new Error(message); + } + } + + protected async checkOrInstall(executer: TExecuter): Promise { + const tokenSource = new CancellationTokenSource(); + + try { + const pandasVersion = await this.getVersion(executer, tokenSource.token); + + if (Cancellation.isCanceled(tokenSource.token)) { + sendTelemetryEvent(Telemetry.PandasInstallCanceled); + return; + } + + if (pandasVersion) { + if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) { + sendTelemetryEvent(Telemetry.PandasOK); + return; + } + sendTelemetryEvent(Telemetry.PandasTooOld); + // Warn user that we cannot start because pandas is too old. + const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; + await this.promptInstall(executer, tokenSource, versionStr); + } + sendTelemetryEvent(Telemetry.PandasNotInstalled); + await this.promptInstall(executer, tokenSource); + } finally { + tokenSource.dispose(); + } + } +} diff --git a/src/webviews/extension-side/dataviewer/constants.ts b/src/webviews/extension-side/dataviewer/constants.ts new file mode 100644 index 000000000000..d555c0261db1 --- /dev/null +++ b/src/webviews/extension-side/dataviewer/constants.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const pandasMinimumVersionSupportedByVariableViewer = '0.20.0'; diff --git a/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts b/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts index 15003308e8db..48f4647fd9a8 100644 --- a/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts +++ b/src/webviews/extension-side/dataviewer/dataViewerDependencyService.node.ts @@ -4,26 +4,21 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { SemVer } from 'semver'; -import { CancellationToken, CancellationTokenSource } from 'vscode'; -import { ProductNames } from '../../../kernels/installer/productNames'; -import { IInstaller, Product, InstallerResponse } from '../../../kernels/installer/types'; +import { IInstaller } from '../../../kernels/installer/types'; +import { IKernel } from '../../../kernels/types'; import { IApplicationShell } from '../../../platform/common/application/types'; -import { Cancellation, createPromiseFromCancellation } from '../../../platform/common/cancellation'; -import { traceWarning } from '../../../platform/logging'; import { IPythonExecutionFactory } from '../../../platform/common/process/types.node'; import { IsCodeSpace } from '../../../platform/common/types'; -import { parseSemVer } from '../../../platform/common/utils.node'; -import { DataScience, Common } from '../../../platform/common/utils/localize'; import { IInterpreterService } from '../../../platform/interpreter/contracts'; import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; -import { sendTelemetryEvent, Telemetry } from '../../../telemetry'; +import { InterpreterDataViewerDependencyImplementation } from './interpreterDataViewerDependencyImplementation.node'; +import { KernelDataViewerDependencyImplementation } from './kernelDataViewerDependencyImplementation'; import { IDataViewerDependencyService } from './types'; -const minimumSupportedPandaVersion = '0.20.0'; - -function isVersionOfPandasSupported(version: SemVer) { - return version.compare(minimumSupportedPandaVersion) > 0; +// TypeScript will narrow the type to PythonEnvironment in any block guarded by a call to isPythonEnvironment +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isPythonEnvironment(env: any): env is PythonEnvironment { + return 'sysPrefix' in env && typeof env.sysPrefix === 'string'; } /** @@ -31,105 +26,32 @@ function isVersionOfPandasSupported(version: SemVer) { */ @injectable() export class DataViewerDependencyService implements IDataViewerDependencyService { - constructor( - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IInstaller) private readonly installer: IInstaller, - @inject(IPythonExecutionFactory) private pythonFactory: IPythonExecutionFactory, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IsCodeSpace) private isCodeSpace: boolean - ) {} - - public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise { - const tokenSource = new CancellationTokenSource(); - try { - const pandasVersion = await this.getVersionOfPandas(interpreter, tokenSource.token); - - if (Cancellation.isCanceled(tokenSource.token)) { - return; - } + private withKernel: IDataViewerDependencyService; + private withInterpreter: IDataViewerDependencyService; - if (pandasVersion) { - if (isVersionOfPandasSupported(pandasVersion)) { - return; - } - sendTelemetryEvent(Telemetry.PandasTooOld); - // Warn user that we cannot start because pandas is too old. - const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; - throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); - } - - sendTelemetryEvent(Telemetry.PandasNotInstalled); - await this.installMissingDependencies(interpreter, tokenSource); - } finally { - tokenSource.dispose(); - } + constructor( + @inject(IInstaller) installer: IInstaller, + @inject(IPythonExecutionFactory) pythonFactory: IPythonExecutionFactory, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(IsCodeSpace) isCodeSpace: boolean + ) { + this.withKernel = new KernelDataViewerDependencyImplementation(applicationShell, isCodeSpace); + this.withInterpreter = new InterpreterDataViewerDependencyImplementation( + installer, + pythonFactory, + interpreterService, + applicationShell, + isCodeSpace + ); } - private async installMissingDependencies( - interpreter: PythonEnvironment, - tokenSource: CancellationTokenSource - ): Promise { - sendTelemetryEvent(Telemetry.PythonModuleInstall, undefined, { - action: 'displayed', - moduleName: ProductNames.get(Product.pandas)!, - pythonEnvType: interpreter?.envType - }); - const selection = this.isCodeSpace - ? Common.install() - : await this.applicationShell.showErrorMessage( - DataScience.pandasRequiredForViewing(), - { modal: true }, - Common.install() - ); - - // All data science dependencies require an interpreter to be passed in - // Default to the active interpreter if no interpreter is available - const interpreterToInstallDependenciesInto = - interpreter || (await this.interpreterService.getActiveInterpreter()); - - if (Cancellation.isCanceled(tokenSource.token)) { - return; - } - - if (selection === Common.install()) { - const cancellationPromise = createPromiseFromCancellation({ - cancelAction: 'resolve', - defaultValue: InstallerResponse.Ignore, - token: tokenSource.token - }); - // Always pass a cancellation token to `install`, to ensure it waits until the module is installed. - const response = await Promise.race([ - this.installer.install(Product.pandas, interpreterToInstallDependenciesInto, tokenSource), - cancellationPromise - ]); - if (response === InstallerResponse.Installed) { - sendTelemetryEvent(Telemetry.UserInstalledPandas); - } + async checkAndInstallMissingDependencies(executionEnvironment: IKernel | PythonEnvironment): Promise { + // IKernel and PythonEnvironment are only types, so I can't compare prototypes or instances of. + if (isPythonEnvironment(executionEnvironment)) { + return this.withInterpreter.checkAndInstallMissingDependencies(executionEnvironment); } else { - sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); - throw new Error(DataScience.pandasRequiredForViewing()); - } - } - - private async getVersionOfPandas( - interpreter: PythonEnvironment, - token?: CancellationToken - ): Promise { - const launcher = await this.pythonFactory.createActivatedEnvironment({ - resource: undefined, - interpreter, - allowEnvironmentFetchExceptions: true - }); - try { - const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], { - throwOnStdErr: true, - token - }); - - return parseSemVer(result.stdout); - } catch (ex) { - traceWarning('Failed to get version of Pandas to use Data Viewer', ex); - return; + return this.withKernel.checkAndInstallMissingDependencies(executionEnvironment); } } } diff --git a/src/webviews/extension-side/dataviewer/dataViewerDependencyService.ts b/src/webviews/extension-side/dataviewer/dataViewerDependencyService.ts new file mode 100644 index 000000000000..e98c146862cc --- /dev/null +++ b/src/webviews/extension-side/dataviewer/dataViewerDependencyService.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IKernel } from '../../../kernels/types'; +import { IApplicationShell } from '../../../platform/common/application/types'; +import { IsCodeSpace } from '../../../platform/common/types'; +import { KernelDataViewerDependencyImplementation } from './kernelDataViewerDependencyImplementation'; +import { IDataViewerDependencyService } from './types'; + +/** + * Responsible for managing dependencies of a Data Viewer. + */ +@injectable() +export class DataViewerDependencyService implements IDataViewerDependencyService { + private withKernel: IDataViewerDependencyService; + constructor( + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(IsCodeSpace) isCodeSpace: boolean + ) { + this.withKernel = new KernelDataViewerDependencyImplementation(applicationShell, isCodeSpace); + } + + async checkAndInstallMissingDependencies(kernel: IKernel): Promise { + return this.withKernel.checkAndInstallMissingDependencies(kernel); + } +} diff --git a/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts b/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts new file mode 100644 index 000000000000..9da1de67bd44 --- /dev/null +++ b/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, CancellationTokenSource } from 'vscode'; +import { IInstaller, Product, InstallerResponse } from '../../../kernels/installer/types'; +import { IApplicationShell } from '../../../platform/common/application/types'; +import { Cancellation, createPromiseFromCancellation } from '../../../platform/common/cancellation'; +import { IPythonExecutionFactory } from '../../../platform/common/process/types.node'; +import { IInterpreterService } from '../../../platform/interpreter/contracts'; +import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; +import { sendTelemetryEvent, Telemetry } from '../../../telemetry'; +import { BaseDataViewerDependencyImplementation } from './baseDataViewerDependencyImplementation'; + +/** + * Uses the Python interpreter to manage dependencies of a Data Viewer. + */ +export class InterpreterDataViewerDependencyImplementation extends BaseDataViewerDependencyImplementation { + constructor( + private readonly installer: IInstaller, + private pythonFactory: IPythonExecutionFactory, + private interpreterService: IInterpreterService, + applicationShell: IApplicationShell, + isCodeSpace: boolean + ) { + super(applicationShell, isCodeSpace); + } + + protected async _getVersion( + interpreter: PythonEnvironment, + token?: CancellationToken + ): Promise { + const launcher = await this.pythonFactory.createActivatedEnvironment({ + resource: undefined, + interpreter, + allowEnvironmentFetchExceptions: true + }); + const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], { + throwOnStdErr: true, + token + }); + return result.stdout; + } + + protected async _doInstall(interpreter: PythonEnvironment, tokenSource: CancellationTokenSource): Promise { + // All data science dependencies require an interpreter to be passed in + // Default to the active interpreter if no interpreter is available + const interpreterToInstallDependenciesInto = + interpreter || (await this.interpreterService.getActiveInterpreter()); + + if (Cancellation.isCanceled(tokenSource.token)) { + return; + } + + const cancellationPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token: tokenSource.token + }); + // Always pass a cancellation token to `install`, to ensure it waits until the module is installed. + const response = await Promise.race([ + this.installer.install(Product.pandas, interpreterToInstallDependenciesInto, tokenSource), + cancellationPromise + ]); + if (response === InstallerResponse.Installed) { + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } + } + + public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise { + sendTelemetryEvent(Telemetry.DataViewerUsingInterpreter); + + await this.checkOrInstall(interpreter); + } +} diff --git a/src/webviews/extension-side/dataviewer/jupyterVariableDataProvider.ts b/src/webviews/extension-side/dataviewer/jupyterVariableDataProvider.ts index 4a493699006b..1e220a410758 100644 --- a/src/webviews/extension-side/dataviewer/jupyterVariableDataProvider.ts +++ b/src/webviews/extension-side/dataviewer/jupyterVariableDataProvider.ts @@ -17,6 +17,7 @@ import { IJupyterVariable, IJupyterVariables } from '../../../kernels/variables/ import { traceError } from '../../../platform/logging'; import { Identifiers } from '../../../platform/common/constants'; import { getFilePath } from '../../../platform/common/platform/fs-paths'; +import { isWeb } from '../../../platform/common/utils/misc'; @injectable() export class JupyterVariableDataProvider implements IJupyterVariableDataProvider { @@ -161,9 +162,9 @@ export class JupyterVariableDataProvider implements IJupyterVariableDataProvider // Postpone pre-req and variable initialization until data is requested. if (!this.initialized && this.variable) { this.initialized = true; - if (this._kernel?.kernelConnectionMetadata.interpreter && this.dependencyService) { + if (this._kernel?.kernelConnectionMetadata && this.dependencyService) { await this.dependencyService.checkAndInstallMissingDependencies( - this._kernel?.kernelConnectionMetadata.interpreter + isWeb() ? this._kernel : this._kernel?.kernelConnectionMetadata.interpreter || this._kernel ); } this.variable = await this.variableManager.getDataFrameInfo(this.variable, this._kernel); diff --git a/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts b/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts new file mode 100644 index 000000000000..f6f25c350a70 --- /dev/null +++ b/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { traceWarning } from '../../../platform/logging'; +import { DataScience } from '../../../platform/common/utils/localize'; +import { EnvironmentType } from '../../../platform/pythonEnvironments/info'; +import { sendTelemetryEvent, Telemetry } from '../../../telemetry'; +import { executeSilently } from '../../../kernels/helpers'; +import { IKernel, IKernelConnectionSession } from '../../../kernels/types'; +import { BaseDataViewerDependencyImplementation } from './baseDataViewerDependencyImplementation'; + +export const kernelGetPandasVersion = + 'import pandas as _VSCODE_pandas;print(_VSCODE_pandas.__version__);del _VSCODE_pandas'; + +function kernelPackaging(kernel: IKernel): '%conda' | '%pip' { + const envType = kernel.kernelConnectionMetadata.interpreter?.envType; + const isConda = envType === EnvironmentType.Conda; + // From https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-pip (%conda is here as well). + return isConda ? '%conda' : '%pip'; +} + +type IKernelWithSession = IKernel & { session: IKernelConnectionSession }; + +// TypeScript will narrow the type to PythonEnvironment in any block guarded by a call to isPythonEnvironment +function kernelHasSession(kernel: IKernel): kernel is IKernelWithSession { + return Boolean(kernel.session); +} + +/** + * Uses the Kernel to manage the dependencies of a Data Viewer. + */ +export class KernelDataViewerDependencyImplementation extends BaseDataViewerDependencyImplementation { + protected async execute(command: string, kernel: IKernelWithSession): Promise<(string | undefined)[]> { + const outputs = await executeSilently(kernel.session, command); + const error = outputs.find((item) => item.output_type === 'error'); + if (error) { + traceWarning(DataScience.failedToGetVersionOfPandas(), error.message); + } + return outputs.map((item) => item.text?.toString()); + } + + protected async _getVersion(kernel: IKernelWithSession): Promise { + const outputs = await this.execute(kernelGetPandasVersion, kernel); + return outputs.map((text) => (text ? text.toString() : undefined)).find((item) => item); + } + + protected async _doInstall(kernel: IKernelWithSession): Promise { + const command = `${kernelPackaging(kernel)} install pandas`; + + try { + await this.execute(command, kernel); + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } catch (e) { + sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); + throw new Error(DataScience.failedToInstallPandas()); + } + } + + async checkAndInstallMissingDependencies(kernel: IKernel): Promise { + sendTelemetryEvent(Telemetry.DataViewerUsingKernel); + + if (!kernelHasSession(kernel)) { + sendTelemetryEvent(Telemetry.NoActiveKernelSession); + throw new Error('No no active kernel session.'); + } + + await this.checkOrInstall(kernel); + } +} diff --git a/src/webviews/extension-side/dataviewer/types.ts b/src/webviews/extension-side/dataviewer/types.ts index 20df843d3c30..f1c711967a89 100644 --- a/src/webviews/extension-side/dataviewer/types.ts +++ b/src/webviews/extension-side/dataviewer/types.ts @@ -7,8 +7,8 @@ import { IKernel } from '../../../kernels/types'; import { IJupyterVariable } from '../../../kernels/variables/types'; import { IDisposable } from '../../../platform/common/types'; import { SharedMessages } from '../../../messageTypes'; -import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; import { SliceOperationSource } from '../../../platform/telemetry/constants'; +import { PythonEnvironment } from '../../../platform/pythonEnvironments/info'; export const CellFetchAllLimit = 100000; export const CellFetchSizeFirst = 100000; @@ -129,6 +129,7 @@ export interface IJupyterVariableDataProviderFactory { } export const IDataViewerDependencyService = Symbol('IDataViewerDependencyService'); + export interface IDataViewerDependencyService { - checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise; + checkAndInstallMissingDependencies(executionEnvironment: IKernel | PythonEnvironment): Promise; } diff --git a/src/webviews/extension-side/serviceRegistry.web.ts b/src/webviews/extension-side/serviceRegistry.web.ts index 986caa03d2a2..3eef632d50b7 100644 --- a/src/webviews/extension-side/serviceRegistry.web.ts +++ b/src/webviews/extension-side/serviceRegistry.web.ts @@ -5,6 +5,7 @@ import { JupyterVariableDataProvider } from './dataviewer/jupyterVariableDataPro import { JupyterVariableDataProviderFactory } from './dataviewer/jupyterVariableDataProviderFactory'; import { IDataViewer, + IDataViewerDependencyService, IDataViewerFactory, IJupyterVariableDataProvider, IJupyterVariableDataProviderFactory @@ -15,6 +16,7 @@ import { DataViewerFactory } from './dataviewer/dataViewerFactory'; import { DataViewer } from './dataviewer/dataViewer'; import { IServiceManager } from '../../platform/ioc/types'; import { IExtensionSingleActivationService } from '../../platform/activation/types'; +import { DataViewerDependencyService } from './dataviewer/dataViewerDependencyService'; export function registerTypes(serviceManager: IServiceManager) { // Data viewer @@ -24,6 +26,10 @@ export function registerTypes(serviceManager: IServiceManager) { ); serviceManager.add(IDataViewer, DataViewer); serviceManager.addSingleton(IDataViewerFactory, DataViewerFactory); + serviceManager.addSingleton( + IDataViewerDependencyService, + DataViewerDependencyService + ); // Variables view serviceManager.addSingleton(INotebookWatcher, NotebookWatcher); diff --git a/src/webviews/webview-side/interactive-common/variableExplorer.tsx b/src/webviews/webview-side/interactive-common/variableExplorer.tsx index 40121a673620..699e18d44497 100644 --- a/src/webviews/webview-side/interactive-common/variableExplorer.tsx +++ b/src/webviews/webview-side/interactive-common/variableExplorer.tsx @@ -122,7 +122,6 @@ export class VariableExplorer extends React.Component this.props.isWeb} /> ) }, diff --git a/src/webviews/webview-side/interactive-common/variableExplorerButtonCellFormatter.tsx b/src/webviews/webview-side/interactive-common/variableExplorerButtonCellFormatter.tsx index f9efe7d2ec2d..cf391feed1c6 100644 --- a/src/webviews/webview-side/interactive-common/variableExplorerButtonCellFormatter.tsx +++ b/src/webviews/webview-side/interactive-common/variableExplorerButtonCellFormatter.tsx @@ -21,7 +21,6 @@ interface IVariableExplorerButtonCellFormatterProps { baseTheme: string; value?: IButtonCellValue; showDataExplorer(targetVariable: IJupyterVariable, numberOfColumns: number): void; - isWeb: () => boolean; } export class VariableExplorerButtonCellFormatter extends React.Component { @@ -32,7 +31,7 @@ export class VariableExplorerButtonCellFormatter extends React.Component Date: Tue, 19 Jul 2022 13:08:40 -0700 Subject: [PATCH 3/8] Add descriptions to classes in IW folder (#10854) * Add descriptions to all classes and consolidate * Add some more commenting --- .../commands/commandRegistry.ts | 437 +++++++++++++++- .../interactiveWindowDebugger.node.ts | 3 + .../debugger/jupyter/debugCellControllers.ts | 4 + .../debugger/jupyter/kernelDebugAdapter.ts | 6 +- .../editor-integration/cellMatcher.ts | 3 + .../codeGeneratorFactory.ts | 3 + .../editor-integration/codelensprovider.ts | 5 + .../editor-integration/codewatcher.ts | 4 + .../editor-integration/decorator.ts | 3 + .../generatedCodeStorage.ts | 6 + .../generatedCodeStorageFactory.ts | 3 + .../editor-integration/hoverProvider.ts | 4 + .../generatedCodeStoreManager.ts | 3 + src/interactive-window/interactiveWindow.ts | 4 + .../interactiveWindowCommandListener.ts | 483 ------------------ .../interactiveWindowProvider.ts | 3 + .../outputs/tracebackFormatter.ts | 5 + .../serviceRegistry.node.ts | 7 +- src/interactive-window/serviceRegistry.web.ts | 6 - 19 files changed, 488 insertions(+), 504 deletions(-) delete mode 100644 src/interactive-window/interactiveWindowCommandListener.ts diff --git a/src/interactive-window/commands/commandRegistry.ts b/src/interactive-window/commands/commandRegistry.ts index 2cc0d669793a..3bc241160bde 100644 --- a/src/interactive-window/commands/commandRegistry.ts +++ b/src/interactive-window/commands/commandRegistry.ts @@ -4,14 +4,31 @@ 'use strict'; import { inject, injectable, optional } from 'inversify'; -import { CodeLens, ConfigurationTarget, env, Range, Uri, commands } from 'vscode'; -import { IKernelProvider } from '../../kernels/types'; +import { + CodeLens, + ConfigurationTarget, + env, + Range, + Uri, + commands, + NotebookCell, + NotebookEdit, + NotebookRange, + Selection, + Position, + ViewColumn, + workspace, + WorkspaceEdit +} from 'vscode'; +import { IKernelProvider, KernelConnectionMetadata } from '../../kernels/types'; import { ICommandNameArgumentTypeMapping } from '../../commands'; import { IApplicationShell, + IClipboard, ICommandManager, IDebugService, IDocumentManager, + IVSCodeNotebook, IWorkspaceService } from '../../platform/common/application/types'; @@ -19,18 +36,39 @@ import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../p import { DataScience } from '../../platform/common/utils/localize'; import { isUri, noop } from '../../platform/common/utils/misc'; import { captureTelemetry } from '../../telemetry'; -import { Commands, Telemetry } from '../../platform/common/constants'; +import { Commands, JVSC_EXTENSION_ID, PYTHON_LANGUAGE, Telemetry } from '../../platform/common/constants'; import { IDataScienceCodeLensProvider, ICodeWatcher } from '../editor-integration/types'; import { IInteractiveWindowProvider } from '../types'; import * as urlPath from '../../platform/vscode-path/resources'; -import { getFilePath } from '../../platform/common/platform/fs-paths'; +import { getDisplayPath, getFilePath } from '../../platform/common/platform/fs-paths'; import { IExtensionSingleActivationService } from '../../platform/activation/types'; - +import { chainWithPendingUpdates } from '../../kernels/execution/notebookUpdater'; +import { ExportFormat, IExportDialog, IFileConverter } from '../../notebooks/export/types'; +import { openAndShowNotebook } from '../../platform/common/utils/notebooks'; +import { JupyterInstallError } from '../../platform/errors/jupyterInstallError'; +import { traceError, traceInfo } from '../../platform/logging'; +import { CommandSource } from '../../platform/testing/common/constants'; +import { generateCellsFromDocument } from '../editor-integration/cellFactory'; +import { IDataScienceErrorHandler } from '../../kernels/errors/types'; +import { INotebookEditorProvider } from '../../notebooks/types'; +import { INotebookExporter, IJupyterExecution } from '../../kernels/jupyter/types'; +import { IFileSystem } from '../../platform/common/platform/types'; +import { IControllerPreferredService } from '../../notebooks/controllers/types'; +import { IStatusProvider } from '../../platform/progress/types'; + +/** + * Class that registers command handlers for interactive window commands. + */ @injectable() export class CommandRegistry implements IDisposable, IExtensionSingleActivationService { constructor( @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(INotebookExporter) @optional() private jupyterExporter: INotebookExporter | undefined, + @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IApplicationShell) private applicationShell: IApplicationShell, + @inject(IFileSystem) private fileSystem: IFileSystem, + @inject(IConfigurationService) private configuration: IConfigurationService, @inject(IDataScienceCodeLensProvider) @optional() private dataScienceCodeLensProvider: IDataScienceCodeLensProvider | undefined, @@ -40,9 +78,16 @@ export class CommandRegistry implements IDisposable, IExtensionSingleActivationS @inject(IApplicationShell) private appShell: IApplicationShell, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IInteractiveWindowProvider) - @optional() - private readonly interactiveWindowProvider: IInteractiveWindowProvider | undefined, - @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider + private readonly interactiveWindowProvider: IInteractiveWindowProvider, + @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, + @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, + @inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider, + @inject(IFileConverter) private fileConverter: IFileConverter, + @inject(IExportDialog) private exportDialog: IExportDialog, + @inject(IClipboard) private clipboard: IClipboard, + @inject(IVSCodeNotebook) private notebook: IVSCodeNotebook, + @inject(IControllerPreferredService) private controllerPreferredService: IControllerPreferredService, + @inject(IStatusProvider) private statusProvider: IStatusProvider ) { if (!this.workspace.isTrusted) { this.workspace.onDidGrantWorkspaceTrust(this.registerCommandsIfTrusted, this, this.disposables); @@ -73,6 +118,85 @@ export class CommandRegistry implements IDisposable, IExtensionSingleActivationS Commands.EnableLoadingWidgetsFrom3rdPartySource, this.enableLoadingWidgetScriptsFromThirdParty ); + this.registerCommand(Commands.CreateNewInteractive, (connection?: KernelConnectionMetadata) => + this.createNewInteractiveWindow(connection) + ); + this.registerCommand( + Commands.ImportNotebook, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.importNotebookOnFile(file); + } else { + return this.importNotebook(); + } + }); + } + ); + this.registerCommand( + Commands.ImportNotebookFile, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.importNotebookOnFile(file); + } else { + return this.importNotebook(); + } + }); + } + ); + this.commandManager.registerCommand( + Commands.ExportFileAsNotebook, + (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.exportFile(file); + } else { + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { + return this.exportFile(activeEditor.document.uri); + } + } + + return Promise.resolve(); + }); + } + ); + this.registerCommand( + Commands.ExportFileAndOutputAsNotebook, + (file: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { + return this.listenForErrors(() => { + if (file) { + return this.exportFileAndOutput(file); + } else { + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { + return this.exportFileAndOutput(activeEditor.document.uri); + } + } + return Promise.resolve(); + }); + } + ); + this.registerCommand(Commands.ExpandAllCells, async (context?: { notebookEditor: { notebookUri: Uri } }) => + this.expandAllCells(context?.notebookEditor?.notebookUri) + ); + this.registerCommand(Commands.CollapseAllCells, async (context?: { notebookEditor: { notebookUri: Uri } }) => + this.collapseAllCells(context?.notebookEditor?.notebookUri) + ); + this.registerCommand(Commands.ExportOutputAsNotebook, () => this.exportCells()); + this.registerCommand( + Commands.InteractiveExportAsNotebook, + (context?: { notebookEditor: { notebookUri: Uri } }) => this.export(context?.notebookEditor?.notebookUri) + ); + this.registerCommand(Commands.InteractiveExportAs, (context?: { notebookEditor: { notebookUri: Uri } }) => + this.exportAs(context?.notebookEditor?.notebookUri) + ); + this.registerCommand(Commands.ScrollToCell, (file: Uri, id: string) => this.scrollToCell(file, id)); + this.registerCommand(Commands.InteractiveClearAll, this.clearAllCellsInInteractiveWindow); + this.registerCommand(Commands.InteractiveRemoveCell, this.removeCellInInteractiveWindow); + this.registerCommand(Commands.InteractiveGoToCode, this.goToCodeInInteractiveWindow); + this.commandManager.registerCommand(Commands.InteractiveCopyCell, this.copyCellInInteractiveWindow); } public dispose() { this.disposables.forEach((d) => d.dispose()); @@ -484,4 +608,301 @@ export class CommandRegistry implements IDisposable, IExtensionSingleActivationS private async openOutlineView(): Promise { return this.commandManager.executeCommand('outline.focus'); } + + /* eslint-disable @typescript-eslint/no-explicit-any */ + private async listenForErrors(promise: () => Promise): Promise { + let result: any; + try { + result = await promise(); + return result; + } catch (err) { + traceError('listenForErrors', err as any); + this.dataScienceErrorHandler.handleError(err).then(noop, noop); + } + return result; + } + + @captureTelemetry(Telemetry.ExportPythonFileInteractive, undefined, false) + private async exportFile(file: Uri): Promise { + const filePath = getFilePath(file); + if (filePath && filePath.length > 0 && this.jupyterExporter) { + // If the current file is the active editor, then generate cells from the document. + const activeEditor = this.documentManager.activeTextEditor; + if (activeEditor && this.fileSystem.arePathsSame(activeEditor.document.uri, file)) { + const cells = generateCellsFromDocument( + activeEditor.document, + this.configuration.getSettings(activeEditor.document.uri) + ); + if (cells) { + // Bring up the export dialog box + const uri = await this.exportDialog.showDialog(ExportFormat.ipynb, file); + await this.waitForStatus( + async () => { + if (uri) { + const notebook = await this.jupyterExporter?.translateToNotebook(cells); + await this.fileSystem.writeFile(uri, JSON.stringify(notebook)); + } + }, + DataScience.exportingFormat(), + getDisplayPath(file) + ); + // When all done, show a notice that it completed. + if (uri && filePath) { + const openQuestion1 = DataScience.exportOpenQuestion1(); + const selection = await this.applicationShell.showInformationMessage( + DataScience.exportDialogComplete().format(getDisplayPath(file)), + openQuestion1 + ); + if (selection === openQuestion1) { + await openAndShowNotebook(uri); + } + } + } + } + } + } + + @captureTelemetry(Telemetry.ExportPythonFileAndOutputInteractive, undefined, false) + private async exportFileAndOutput(file: Uri): Promise { + const filePath = getFilePath(file); + if ( + filePath && + filePath.length > 0 && + this.jupyterExporter && + (await this.jupyterExecution.isNotebookSupported()) + ) { + // If the current file is the active editor, then generate cells from the document. + const activeEditor = this.documentManager.activeTextEditor; + if ( + activeEditor && + activeEditor.document && + this.fileSystem.arePathsSame(activeEditor.document.uri, file) + ) { + const cells = generateCellsFromDocument( + activeEditor.document, + this.configuration.getSettings(activeEditor.document.uri) + ); + if (cells) { + // Bring up the export dialog box + const uri = await this.exportDialog.showDialog(ExportFormat.ipynb, file); + if (!uri) { + return; + } + await this.waitForStatus( + async () => { + if (uri) { + const notebook = await this.jupyterExporter?.translateToNotebook(cells); + await this.fileSystem.writeFile(uri, JSON.stringify(notebook)); + } + }, + DataScience.exportingFormat(), + getDisplayPath(file) + ); + // Next open this notebook & execute it. + const editor = await this.notebook + .openNotebookDocument(uri) + .then((document) => this.notebook.showNotebookDocument(document)); + const { controller } = await this.controllerPreferredService.computePreferred(editor.notebook); + if (controller) { + await this.commandManager.executeCommand('notebook.selectKernel', { + id: controller.id, + extension: JVSC_EXTENSION_ID + }); + } + await this.commandManager.executeCommand('notebook.execute'); + return uri; + } + } + } else { + await this.dataScienceErrorHandler.handleError( + new JupyterInstallError( + DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()) + ) + ); + } + } + + private async expandAllCells(uri?: Uri) { + const interactiveWindow = this.getTargetInteractiveWindow(uri); + traceInfo(`Expanding all cells in interactive window with uri ${interactiveWindow?.notebookUri}`); + if (interactiveWindow) { + await interactiveWindow.expandAllCells(); + } + } + + private async collapseAllCells(uri?: Uri) { + const interactiveWindow = this.getTargetInteractiveWindow(uri); + traceInfo(`Collapsing all cells in interactive window with uri ${interactiveWindow?.notebookUri}`); + if (interactiveWindow) { + await interactiveWindow.collapseAllCells(); + } + } + + private exportCells() { + const interactiveWindow = this.interactiveWindowProvider?.activeWindow; + if (interactiveWindow) { + interactiveWindow.export(); + } + } + + private exportAs(uri?: Uri) { + const interactiveWindow = this.getTargetInteractiveWindow(uri); + if (interactiveWindow) { + interactiveWindow.exportAs(); + } + } + + private export(uri?: Uri) { + const interactiveWindow = this.getTargetInteractiveWindow(uri); + if (interactiveWindow) { + interactiveWindow.export(); + } + } + + @captureTelemetry(Telemetry.CreateNewInteractive, undefined, false) + private async createNewInteractiveWindow(connection?: KernelConnectionMetadata): Promise { + await this.interactiveWindowProvider?.getOrCreate(undefined, connection); + } + + private waitForStatus( + promise: () => Promise, + format: string, + file?: string, + canceled?: () => void + ): Promise { + const message = file ? format.format(file) : format; + return this.statusProvider.waitWithStatus(promise, message, undefined, canceled); + } + + @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) + private async importNotebook(): Promise { + const filtersKey = DataScience.importDialogFilter(); + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['ipynb']; + + const uris = await this.applicationShell.showOpenDialog({ + openLabel: DataScience.importDialogTitle(), + filters: filtersObject + }); + + if (uris && uris.length > 0) { + // Don't call the other overload as we'll end up with double telemetry. + await this.waitForStatus( + async () => { + await this.fileConverter.importIpynb(uris[0]); + }, + DataScience.importingFormat(), + getDisplayPath(uris[0]) + ); + } + } + + @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) + private async importNotebookOnFile(file: Uri): Promise { + const filepath = getFilePath(file); + if (filepath && filepath.length > 0) { + await this.waitForStatus( + async () => { + await this.fileConverter.importIpynb(file); + }, + DataScience.importingFormat(), + getDisplayPath(file) + ); + } + } + + private async scrollToCell(file: Uri, id: string): Promise { + if (id && file) { + // Find the interactive windows that have this file as a submitter + const possibles = this.interactiveWindowProvider.windows.filter( + (w) => w.submitters.findIndex((s) => this.fileSystem.arePathsSame(s, file)) >= 0 + ); + + // Scroll to cell in the one that has the cell. We need this so + // we don't activate all of them. + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < possibles.length; i += 1) { + if (await possibles[i].hasCell(id)) { + possibles[i].scrollToCell(id); + break; + } + } + } + } + + private async clearAllCellsInInteractiveWindow(context?: { notebookEditor: { notebookUri: Uri } }): Promise { + const uri = this.getTargetInteractiveWindow(context?.notebookEditor?.notebookUri)?.notebookUri; + if (!uri) { + return; + } + + // Look for the matching notebook document to add cells to + const document = workspace.notebookDocuments.find((document) => document.uri.toString() === uri.toString()); + if (!document) { + return; + } + + // Remove the cells from the matching notebook document + const edit = new WorkspaceEdit(); + const nbEdit = NotebookEdit.deleteCells(new NotebookRange(0, document.cellCount)); + edit.set(document.uri, [nbEdit]); + await workspace.applyEdit(edit); + } + + private async removeCellInInteractiveWindow(context?: NotebookCell) { + const interactiveWindow = this.interactiveWindowProvider.getActiveOrAssociatedInteractiveWindow(); + const ranges = + context === undefined + ? interactiveWindow?.notebookEditor?.selections + : [new NotebookRange(context.index, context.index + 1)]; + const document = context === undefined ? interactiveWindow?.notebookEditor?.notebook : context.notebook; + + if (ranges !== undefined && document !== undefined) { + await chainWithPendingUpdates(document, (edit) => { + ranges.forEach((range) => { + const nbEdit = NotebookEdit.deleteCells(range); + edit.set(document.uri, [nbEdit]); + }); + }); + } + } + + private async goToCodeInInteractiveWindow(context?: NotebookCell) { + if (context && context.metadata?.interactive) { + const uri = Uri.parse(context.metadata.interactive.uristring); + const line = context.metadata.interactive.lineIndex; + + const editor = await this.documentManager.showTextDocument(uri, { viewColumn: ViewColumn.One }); + + // If we found the editor change its selection + if (editor) { + editor.revealRange(new Range(line, 0, line, 0)); + editor.selection = new Selection(new Position(line, 0), new Position(line, 0)); + } + } + } + + private async copyCellInInteractiveWindow(context?: NotebookCell) { + if (context) { + const settings = this.configuration.getSettings(context.notebook.uri); + const source = [ + // Prepend cell marker to code + context.metadata.interactiveWindowCellMarker ?? settings.defaultCellMarker, + context.document.getText() + ].join('\n'); + await this.clipboard.writeText(source); + } + } + + private getTargetInteractiveWindow(notebookUri: Uri | undefined) { + let targetInteractiveWindow; + if (notebookUri !== undefined) { + targetInteractiveWindow = this.interactiveWindowProvider.windows.find( + (w) => w.notebookUri?.toString() === notebookUri.toString() + ); + } else { + targetInteractiveWindow = this.interactiveWindowProvider.getActiveOrAssociatedInteractiveWindow(); + } + return targetInteractiveWindow; + } } diff --git a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts index e3688e42cb42..234365e992d7 100644 --- a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts +++ b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts @@ -20,6 +20,9 @@ import { IJupyterDebugService } from '../../kernels/debugger/types'; import { executeSilently, getAssociatedNotebookDocument } from '../../kernels/helpers'; import { buildSourceMap } from './helper'; +/** + * Public API to begin debugging in the interactive window + */ @injectable() export class InteractiveWindowDebugger implements IInteractiveWindowDebugger { private configs: WeakMap = new WeakMap< diff --git a/src/interactive-window/debugger/jupyter/debugCellControllers.ts b/src/interactive-window/debugger/jupyter/debugCellControllers.ts index bcdeb2fd90b0..cf22fa5376fb 100644 --- a/src/interactive-window/debugger/jupyter/debugCellControllers.ts +++ b/src/interactive-window/debugger/jupyter/debugCellControllers.ts @@ -11,6 +11,10 @@ import { createDeferred } from '../../../platform/common/utils/async'; import { sendTelemetryEvent } from '../../../telemetry'; import { getInteractiveCellMetadata } from '../../helpers'; +/** + * Class that handles keeping track of whether or not a cell needs to be dumped before debugging. + * Dumping a cell is how the IPython kernel determines the file path of a cell + */ export class DebugCellController implements IDebuggingDelegate { private readonly _ready = createDeferred(); public readonly ready = this._ready.promise; diff --git a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts index 4d56fe2f4962..832584174d3f 100644 --- a/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts +++ b/src/interactive-window/debugger/jupyter/kernelDebugAdapter.ts @@ -15,7 +15,11 @@ import { getInteractiveCellMetadata } from '../../../interactive-window/helpers' import { KernelDebugAdapterBase } from '../../../kernels/debugger/kernelDebugAdapterBase'; import { InteractiveCellMetadata } from '../../editor-integration/types'; import { IDebugService } from '../../../platform/common/application/types'; - +/** + * KernelDebugAdapter listens to debug messages in order to translate file requests into real files + * (Interactive Window generally executes against a real file) + * Files from the kernel are pointing to cells, whereas the user is looking at a real file. + */ export class KernelDebugAdapter extends KernelDebugAdapterBase { private readonly debugLocationTracker?: DebugAdapterTracker; private readonly cellToDebugFileSortedInReverseOrderByLineNumber: { diff --git a/src/interactive-window/editor-integration/cellMatcher.ts b/src/interactive-window/editor-integration/cellMatcher.ts index 8767b36bc951..ec8ad0b07ba6 100644 --- a/src/interactive-window/editor-integration/cellMatcher.ts +++ b/src/interactive-window/editor-integration/cellMatcher.ts @@ -7,6 +7,9 @@ import { IJupyterSettings } from '../../platform/common/types'; import { noop } from '../../platform/common/utils/misc'; +/** + * CellMatcher is used to match either markdown or code cells using the regex's provided in the settings. + */ export class CellMatcher { public codeExecRegEx: RegExp; public markdownExecRegEx: RegExp; diff --git a/src/interactive-window/editor-integration/codeGeneratorFactory.ts b/src/interactive-window/editor-integration/codeGeneratorFactory.ts index 206dc26b8ff5..00dbbfbf816b 100644 --- a/src/interactive-window/editor-integration/codeGeneratorFactory.ts +++ b/src/interactive-window/editor-integration/codeGeneratorFactory.ts @@ -9,6 +9,9 @@ import { ICodeGeneratorFactory, IGeneratedCodeStorageFactory, IInteractiveWindow import { NotebookDocument } from 'vscode'; import { IExtensionSyncActivationService } from '../../platform/activation/types'; +/** + * The CodeGeneratorFactory creates CodeGenerators for a given notebook document. + */ @injectable() export class CodeGeneratorFactory implements ICodeGeneratorFactory, IExtensionSyncActivationService { private readonly codeGenerators = new WeakMap(); diff --git a/src/interactive-window/editor-integration/codelensprovider.ts b/src/interactive-window/editor-integration/codelensprovider.ts index f7d856625740..1b43504f1e35 100644 --- a/src/interactive-window/editor-integration/codelensprovider.ts +++ b/src/interactive-window/editor-integration/codelensprovider.ts @@ -30,6 +30,11 @@ import { IDataScienceCodeLensProvider, ICodeWatcher } from './types'; import * as urlPath from '../../platform/vscode-path/resources'; import { IDebugLocationTracker } from '../../kernels/debugger/types'; +/** + * Implementation of the VS code CodeLensProvider that provides code lenses for the Interactive Window. + * Uses a CodeWatcher to get the code lenses. + * + */ @injectable() export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider, IDisposable { private totalExecutionTimeInMs: number = 0; diff --git a/src/interactive-window/editor-integration/codewatcher.ts b/src/interactive-window/editor-integration/codewatcher.ts index bea934265054..ff4993674f15 100644 --- a/src/interactive-window/editor-integration/codewatcher.ts +++ b/src/interactive-window/editor-integration/codewatcher.ts @@ -53,6 +53,10 @@ function getIndex(index: number, length: number): number { } } +/** + * CodeWatchers watch source files, provide code lenses in them, and handle the execution of + * the code lenses. Code lenses are provided by a CodeLensFactory. + */ @injectable() export class CodeWatcher implements ICodeWatcher { private static sentExecuteCellTelemetry: boolean = false; diff --git a/src/interactive-window/editor-integration/decorator.ts b/src/interactive-window/editor-integration/decorator.ts index 34aafdc826e6..e723c0987906 100644 --- a/src/interactive-window/editor-integration/decorator.ts +++ b/src/interactive-window/editor-integration/decorator.ts @@ -12,6 +12,9 @@ import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../p import { getAssociatedJupyterNotebook } from '../../platform/common/utils'; import { generateCellRangesFromDocument } from './cellFactory'; +/** + * Provides the lines that show up between cells in the editor. + */ @injectable() export class Decorator implements IExtensionSingleActivationService, IDisposable { private currentCellTop: vscode.TextEditorDecorationType | undefined; diff --git a/src/interactive-window/editor-integration/generatedCodeStorage.ts b/src/interactive-window/editor-integration/generatedCodeStorage.ts index 71689ca599f1..e9331e4bfc4e 100644 --- a/src/interactive-window/editor-integration/generatedCodeStorage.ts +++ b/src/interactive-window/editor-integration/generatedCodeStorage.ts @@ -5,6 +5,12 @@ import { Uri } from 'vscode'; import { ResourceMap } from '../../platform/vscode-path/map'; import { IGeneratedCode, IFileGeneratedCodes, IGeneratedCodeStore } from './types'; +/** + * Stores an IGeneratedCode for each file that is sent to the Interactive Window. + * IGeneratedCode is a struct describing: + * - line and file numbers for the code + * - the real code sent to jupyter (minus cell markers) + */ export class GeneratedCodeStorage implements IGeneratedCodeStore { private codeGeneratorsByFile = new ResourceMap(); clear(): void { diff --git a/src/interactive-window/editor-integration/generatedCodeStorageFactory.ts b/src/interactive-window/editor-integration/generatedCodeStorageFactory.ts index 2abb62c24928..04cd1fcf0528 100644 --- a/src/interactive-window/editor-integration/generatedCodeStorageFactory.ts +++ b/src/interactive-window/editor-integration/generatedCodeStorageFactory.ts @@ -7,6 +7,9 @@ import { IVSCodeNotebook } from '../../platform/common/application/types'; import { GeneratedCodeStorage } from './generatedCodeStorage'; import { IGeneratedCodeStore, IGeneratedCodeStorageFactory } from './types'; +/** + * Creates GeneratedCodeStorages for a given notebook document. + */ @injectable() export class GeneratedCodeStorageFactory implements IGeneratedCodeStorageFactory { private readonly storages = new WeakMap(); diff --git a/src/interactive-window/editor-integration/hoverProvider.ts b/src/interactive-window/editor-integration/hoverProvider.ts index 8173163e1a0c..24d898b8666b 100644 --- a/src/interactive-window/editor-integration/hoverProvider.ts +++ b/src/interactive-window/editor-integration/hoverProvider.ts @@ -20,6 +20,10 @@ import { IInteractiveWindowProvider } from '../types'; import { getInteractiveCellMetadata } from '../helpers'; import * as urlPath from '../../platform/vscode-path/resources'; +/** + * Provides hover support in python files based on the state of a jupyter kernel. Files that are + * sent to the Interactive Window have hover support added when hovering over variables. + */ @injectable() export class HoverProvider implements IExtensionSyncActivationService, vscode.HoverProvider { private runFiles = new Set(); diff --git a/src/interactive-window/generatedCodeStoreManager.ts b/src/interactive-window/generatedCodeStoreManager.ts index 56070508190a..f67fbb07c725 100644 --- a/src/interactive-window/generatedCodeStoreManager.ts +++ b/src/interactive-window/generatedCodeStoreManager.ts @@ -12,6 +12,9 @@ import { disposeAllDisposables } from '../platform/common/helpers'; import { IDisposable, IDisposableRegistry } from '../platform/common/types'; import { ICodeGeneratorFactory, IGeneratedCodeStorageFactory } from './editor-integration/types'; +/** + * Responsible for updating the GenerateCodeStorage when kernels reload + */ @injectable() export class GeneratedCodeStorageManager implements IExtensionSyncActivationService { private readonly disposables: IDisposable[] = []; diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index e460a1682b27..ff0bd34e7e3b 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -72,6 +72,10 @@ import { chainWithPendingUpdates } from '../kernels/execution/notebookUpdater'; import { initializeInteractiveOrNotebookTelemetryBasedOnUserAction } from '../kernels/telemetry/helper'; import { generateMarkdownFromCodeLines, parseForComments } from '../platform/common/utils'; +/** + * ViewModel for an interactive window from the Jupyter extension's point of view. + * Methods for talking to an Interactive Window are exposed here, but the actual UI is part of VS code core. + */ export class InteractiveWindow implements IInteractiveWindowLoadable { public get onDidChangeViewState(): Event { return this._onDidChangeViewState.event; diff --git a/src/interactive-window/interactiveWindowCommandListener.ts b/src/interactive-window/interactiveWindowCommandListener.ts deleted file mode 100644 index 100725c02cb7..000000000000 --- a/src/interactive-window/interactiveWindowCommandListener.ts +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../platform/common/extensions'; - -import { inject, injectable, optional } from 'inversify'; -import { - NotebookCell, - NotebookEdit, - NotebookRange, - Position, - Range, - Selection, - Uri, - ViewColumn, - workspace, - WorkspaceEdit -} from 'vscode'; -import { - IApplicationShell, - IClipboard, - ICommandManager, - IDocumentManager, - IVSCodeNotebook -} from '../platform/common/application/types'; -import { Commands, JVSC_EXTENSION_ID, PYTHON_LANGUAGE, Telemetry } from '../platform/common/constants'; -import { traceError, traceInfo } from '../platform/logging'; -import { IFileSystem } from '../platform/common/platform/types'; -import { IConfigurationService, IDataScienceCommandListener, IDisposableRegistry } from '../platform/common/types'; -import * as localize from '../platform/common/utils/localize'; -import { captureTelemetry } from '../telemetry'; -import { CommandSource } from '../platform/testing/common/constants'; -import { JupyterInstallError } from '../platform/errors/jupyterInstallError'; -import { INotebookEditorProvider } from '../notebooks/types'; -import { KernelConnectionMetadata } from '../kernels/types'; -import { INotebookExporter, IJupyterExecution } from '../kernels/jupyter/types'; -import { IDataScienceErrorHandler } from '../kernels/errors/types'; -import { IFileConverter, IExportDialog, ExportFormat } from '../notebooks/export/types'; -import { IStatusProvider } from '../platform/progress/types'; -import { generateCellsFromDocument } from './editor-integration/cellFactory'; -import { IInteractiveWindowProvider } from './types'; -import { getDisplayPath, getFilePath } from '../platform/common/platform/fs-paths'; -import { chainWithPendingUpdates } from '../kernels/execution/notebookUpdater'; -import { openAndShowNotebook } from '../platform/common/utils/notebooks'; -import { noop } from '../platform/common/utils/misc'; -import { IControllerPreferredService } from '../notebooks/controllers/types'; - -@injectable() -export class InteractiveWindowCommandListener implements IDataScienceCommandListener { - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, - @inject(INotebookExporter) @optional() private jupyterExporter: INotebookExporter | undefined, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IStatusProvider) private statusProvider: IStatusProvider, - @inject(IDataScienceErrorHandler) private dataScienceErrorHandler: IDataScienceErrorHandler, - @inject(INotebookEditorProvider) protected ipynbProvider: INotebookEditorProvider, - @inject(IFileConverter) private fileConverter: IFileConverter, - @inject(IExportDialog) private exportDialog: IExportDialog, - @inject(IClipboard) private clipboard: IClipboard, - @inject(IVSCodeNotebook) private notebook: IVSCodeNotebook, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IControllerPreferredService) private controllerPreferredService: IControllerPreferredService - ) {} - - public register(commandManager: ICommandManager): void { - let disposable = commandManager.registerCommand( - Commands.CreateNewInteractive, - (connection?: KernelConnectionMetadata) => this.createNewInteractiveWindow(connection) - ); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand( - Commands.ImportNotebook, - (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file) { - return this.importNotebookOnFile(file); - } else { - return this.importNotebook(); - } - }); - } - ); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand( - Commands.ImportNotebookFile, - (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file) { - return this.importNotebookOnFile(file); - } else { - return this.importNotebook(); - } - }); - } - ); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand( - Commands.ExportFileAsNotebook, - (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file) { - return this.exportFile(file); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - return this.exportFile(activeEditor.document.uri); - } - } - - return Promise.resolve(); - }); - } - ); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand( - Commands.ExportFileAndOutputAsNotebook, - (file: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file) { - return this.exportFileAndOutput(file); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - return this.exportFileAndOutput(activeEditor.document.uri); - } - } - return Promise.resolve(); - }); - } - ); - this.disposableRegistry.push(disposable); - this.disposableRegistry.push( - commandManager.registerCommand( - Commands.ExpandAllCells, - async (context?: { notebookEditor: { notebookUri: Uri } }) => - this.expandAllCells(context?.notebookEditor?.notebookUri) - ) - ); - this.disposableRegistry.push( - commandManager.registerCommand( - Commands.CollapseAllCells, - async (context?: { notebookEditor: { notebookUri: Uri } }) => - this.collapseAllCells(context?.notebookEditor?.notebookUri) - ) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.ExportOutputAsNotebook, () => this.exportCells()) - ); - this.disposableRegistry.push( - commandManager.registerCommand( - Commands.InteractiveExportAsNotebook, - (context?: { notebookEditor: { notebookUri: Uri } }) => - this.export(context?.notebookEditor?.notebookUri) - ) - ); - this.disposableRegistry.push( - commandManager.registerCommand( - Commands.InteractiveExportAs, - (context?: { notebookEditor: { notebookUri: Uri } }) => - this.exportAs(context?.notebookEditor?.notebookUri) - ) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.ScrollToCell, (file: Uri, id: string) => - this.scrollToCell(file, id) - ) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.InteractiveClearAll, this.clearAllCellsInInteractiveWindow, this) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.InteractiveRemoveCell, this.removeCellInInteractiveWindow, this) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.InteractiveGoToCode, this.goToCodeInInteractiveWindow, this) - ); - this.disposableRegistry.push( - commandManager.registerCommand(Commands.InteractiveCopyCell, this.copyCellInInteractiveWindow, this) - ); - } - - /* eslint-disable @typescript-eslint/no-explicit-any */ - private async listenForErrors(promise: () => Promise): Promise { - let result: any; - try { - result = await promise(); - return result; - } catch (err) { - traceError('listenForErrors', err as any); - this.dataScienceErrorHandler.handleError(err).then(noop, noop); - } - return result; - } - - @captureTelemetry(Telemetry.ExportPythonFileInteractive, undefined, false) - private async exportFile(file: Uri): Promise { - const filePath = getFilePath(file); - if (filePath && filePath.length > 0 && this.jupyterExporter) { - // If the current file is the active editor, then generate cells from the document. - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && this.fileSystem.arePathsSame(activeEditor.document.uri, file)) { - const cells = generateCellsFromDocument( - activeEditor.document, - this.configuration.getSettings(activeEditor.document.uri) - ); - if (cells) { - // Bring up the export dialog box - const uri = await this.exportDialog.showDialog(ExportFormat.ipynb, file); - await this.waitForStatus( - async () => { - if (uri) { - const notebook = await this.jupyterExporter?.translateToNotebook(cells); - await this.fileSystem.writeFile(uri, JSON.stringify(notebook)); - } - }, - localize.DataScience.exportingFormat(), - getDisplayPath(file) - ); - // When all done, show a notice that it completed. - if (uri && filePath) { - const openQuestion1 = localize.DataScience.exportOpenQuestion1(); - const selection = await this.applicationShell.showInformationMessage( - localize.DataScience.exportDialogComplete().format(getDisplayPath(file)), - openQuestion1 - ); - if (selection === openQuestion1) { - await openAndShowNotebook(uri); - } - } - } - } - } - } - - @captureTelemetry(Telemetry.ExportPythonFileAndOutputInteractive, undefined, false) - private async exportFileAndOutput(file: Uri): Promise { - const filePath = getFilePath(file); - if ( - filePath && - filePath.length > 0 && - this.jupyterExporter && - (await this.jupyterExecution.isNotebookSupported()) - ) { - // If the current file is the active editor, then generate cells from the document. - const activeEditor = this.documentManager.activeTextEditor; - if ( - activeEditor && - activeEditor.document && - this.fileSystem.arePathsSame(activeEditor.document.uri, file) - ) { - const cells = generateCellsFromDocument( - activeEditor.document, - this.configuration.getSettings(activeEditor.document.uri) - ); - if (cells) { - // Bring up the export dialog box - const uri = await this.exportDialog.showDialog(ExportFormat.ipynb, file); - if (!uri) { - return; - } - await this.waitForStatus( - async () => { - if (uri) { - const notebook = await this.jupyterExporter?.translateToNotebook(cells); - await this.fileSystem.writeFile(uri, JSON.stringify(notebook)); - } - }, - localize.DataScience.exportingFormat(), - getDisplayPath(file) - ); - // Next open this notebook & execute it. - const editor = await this.notebook - .openNotebookDocument(uri) - .then((document) => this.notebook.showNotebookDocument(document)); - const { controller } = await this.controllerPreferredService.computePreferred(editor.notebook); - if (controller) { - await this.commandManager.executeCommand('notebook.selectKernel', { - id: controller.id, - extension: JVSC_EXTENSION_ID - }); - } - await this.commandManager.executeCommand('notebook.execute'); - return uri; - } - } - } else { - await this.dataScienceErrorHandler.handleError( - new JupyterInstallError( - localize.DataScience.jupyterNotSupported().format(await this.jupyterExecution.getNotebookError()) - ) - ); - } - } - - private async expandAllCells(uri?: Uri) { - const interactiveWindow = this.getTargetInteractiveWindow(uri); - traceInfo(`Expanding all cells in interactive window with uri ${interactiveWindow?.notebookUri}`); - if (interactiveWindow) { - await interactiveWindow.expandAllCells(); - } - } - - private async collapseAllCells(uri?: Uri) { - const interactiveWindow = this.getTargetInteractiveWindow(uri); - traceInfo(`Collapsing all cells in interactive window with uri ${interactiveWindow?.notebookUri}`); - if (interactiveWindow) { - await interactiveWindow.collapseAllCells(); - } - } - - private exportCells() { - const interactiveWindow = this.interactiveWindowProvider.activeWindow; - if (interactiveWindow) { - interactiveWindow.export(); - } - } - - private exportAs(uri?: Uri) { - const interactiveWindow = this.getTargetInteractiveWindow(uri); - if (interactiveWindow) { - interactiveWindow.exportAs(); - } - } - - private export(uri?: Uri) { - const interactiveWindow = this.getTargetInteractiveWindow(uri); - if (interactiveWindow) { - interactiveWindow.export(); - } - } - - @captureTelemetry(Telemetry.CreateNewInteractive, undefined, false) - private async createNewInteractiveWindow(connection?: KernelConnectionMetadata): Promise { - await this.interactiveWindowProvider.getOrCreate(undefined, connection); - } - - private waitForStatus( - promise: () => Promise, - format: string, - file?: string, - canceled?: () => void - ): Promise { - const message = file ? format.format(file) : format; - return this.statusProvider.waitWithStatus(promise, message, undefined, canceled); - } - - @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) - private async importNotebook(): Promise { - const filtersKey = localize.DataScience.importDialogFilter(); - const filtersObject: { [name: string]: string[] } = {}; - filtersObject[filtersKey] = ['ipynb']; - - const uris = await this.applicationShell.showOpenDialog({ - openLabel: localize.DataScience.importDialogTitle(), - filters: filtersObject - }); - - if (uris && uris.length > 0) { - // Don't call the other overload as we'll end up with double telemetry. - await this.waitForStatus( - async () => { - await this.fileConverter.importIpynb(uris[0]); - }, - localize.DataScience.importingFormat(), - getDisplayPath(uris[0]) - ); - } - } - - @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) - private async importNotebookOnFile(file: Uri): Promise { - const filepath = getFilePath(file); - if (filepath && filepath.length > 0) { - await this.waitForStatus( - async () => { - await this.fileConverter.importIpynb(file); - }, - localize.DataScience.importingFormat(), - getDisplayPath(file) - ); - } - } - - private async scrollToCell(file: Uri, id: string): Promise { - if (id && file) { - // Find the interactive windows that have this file as a submitter - const possibles = this.interactiveWindowProvider.windows.filter( - (w) => w.submitters.findIndex((s) => this.fileSystem.arePathsSame(s, file)) >= 0 - ); - - // Scroll to cell in the one that has the cell. We need this so - // we don't activate all of them. - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < possibles.length; i += 1) { - if (await possibles[i].hasCell(id)) { - possibles[i].scrollToCell(id); - break; - } - } - } - } - - private async clearAllCellsInInteractiveWindow(context?: { notebookEditor: { notebookUri: Uri } }): Promise { - const uri = this.getTargetInteractiveWindow(context?.notebookEditor?.notebookUri)?.notebookUri; - if (!uri) { - return; - } - - // Look for the matching notebook document to add cells to - const document = workspace.notebookDocuments.find((document) => document.uri.toString() === uri.toString()); - if (!document) { - return; - } - - // Remove the cells from the matching notebook document - const edit = new WorkspaceEdit(); - const nbEdit = NotebookEdit.deleteCells(new NotebookRange(0, document.cellCount)); - edit.set(document.uri, [nbEdit]); - await workspace.applyEdit(edit); - } - - private async removeCellInInteractiveWindow(context?: NotebookCell) { - const interactiveWindow = this.interactiveWindowProvider.getActiveOrAssociatedInteractiveWindow(); - const ranges = - context === undefined - ? interactiveWindow?.notebookEditor?.selections - : [new NotebookRange(context.index, context.index + 1)]; - const document = context === undefined ? interactiveWindow?.notebookEditor?.notebook : context.notebook; - - if (ranges !== undefined && document !== undefined) { - await chainWithPendingUpdates(document, (edit) => { - ranges.forEach((range) => { - const nbEdit = NotebookEdit.deleteCells(range); - edit.set(document.uri, [nbEdit]); - }); - }); - } - } - - private async goToCodeInInteractiveWindow(context?: NotebookCell) { - if (context && context.metadata?.interactive) { - const uri = Uri.parse(context.metadata.interactive.uristring); - const line = context.metadata.interactive.lineIndex; - - const editor = await this.documentManager.showTextDocument(uri, { viewColumn: ViewColumn.One }); - - // If we found the editor change its selection - if (editor) { - editor.revealRange(new Range(line, 0, line, 0)); - editor.selection = new Selection(new Position(line, 0), new Position(line, 0)); - } - } - } - - private async copyCellInInteractiveWindow(context?: NotebookCell) { - if (context) { - const settings = this.configuration.getSettings(context.notebook.uri); - const source = [ - // Prepend cell marker to code - context.metadata.interactiveWindowCellMarker ?? settings.defaultCellMarker, - context.document.getText() - ].join('\n'); - await this.clipboard.writeText(source); - } - } - - private getTargetInteractiveWindow(notebookUri: Uri | undefined) { - let targetInteractiveWindow; - if (notebookUri !== undefined) { - targetInteractiveWindow = this.interactiveWindowProvider.windows.find( - (w) => w.notebookUri?.toString() === notebookUri.toString() - ); - } else { - targetInteractiveWindow = this.interactiveWindowProvider.getActiveOrAssociatedInteractiveWindow(); - } - return targetInteractiveWindow; - } -} diff --git a/src/interactive-window/interactiveWindowProvider.ts b/src/interactive-window/interactiveWindowProvider.ts index ddd3f7af9a16..27885be50442 100644 --- a/src/interactive-window/interactiveWindowProvider.ts +++ b/src/interactive-window/interactiveWindowProvider.ts @@ -59,6 +59,9 @@ import { isInteractiveInputTab } from './helpers'; export const AskedForPerFileSettingKey = 'ds_asked_per_file_interactive'; export const InteractiveWindowCacheKey = 'ds_interactive_window_cache'; +/** + * Factory for InteractiveWindow + */ @injectable() export class InteractiveWindowProvider implements IInteractiveWindowProvider, IEmbedNotebookEditorProvider, IAsyncDisposable diff --git a/src/interactive-window/outputs/tracebackFormatter.ts b/src/interactive-window/outputs/tracebackFormatter.ts index 647db994bdeb..3ddf852aecb6 100644 --- a/src/interactive-window/outputs/tracebackFormatter.ts +++ b/src/interactive-window/outputs/tracebackFormatter.ts @@ -16,6 +16,11 @@ import { InteractiveWindowView } from '../../platform/common/constants'; const _escapeRegExp = require('lodash/escapeRegExp') as typeof import('lodash/escapeRegExp'); // NOSONAR const LineNumberMatchRegex = /(;32m[ ->]*?)(\d+)(.*)/g; +/** + * Modifies error tracebacks from running cells in the Interactive Window. Modification can include: + * - Providing links to files + * - Removing ANSI escape sequences + */ @injectable() export class InteractiveWindowTracebackFormatter implements ITracebackFormatter { constructor( diff --git a/src/interactive-window/serviceRegistry.node.ts b/src/interactive-window/serviceRegistry.node.ts index 4070c3d7d223..74ad1b16a290 100644 --- a/src/interactive-window/serviceRegistry.node.ts +++ b/src/interactive-window/serviceRegistry.node.ts @@ -4,7 +4,7 @@ import { ITracebackFormatter } from '../kernels/types'; import { IExtensionSyncActivationService, IExtensionSingleActivationService } from '../platform/activation/types'; -import { IDataScienceCommandListener, IJupyterExtensionBanner } from '../platform/common/types'; +import { IJupyterExtensionBanner } from '../platform/common/types'; import { IServiceManager } from '../platform/ioc/types'; import { CommandRegistry } from './commands/commandRegistry'; import { CodeGeneratorFactory } from './editor-integration/codeGeneratorFactory'; @@ -14,7 +14,6 @@ import { CodeWatcher } from './editor-integration/codewatcher'; import { Decorator } from './editor-integration/decorator'; import { GeneratedCodeStorageFactory } from './editor-integration/generatedCodeStorageFactory'; import { HoverProvider } from './editor-integration/hoverProvider'; -import { InteractiveWindowCommandListener } from './interactiveWindowCommandListener'; import { InteractiveWindowProvider } from './interactiveWindowProvider'; import { ICodeWatcher, @@ -32,10 +31,6 @@ import { BANNER_NAME_INTERACTIVE_SHIFTENTER, InteractiveShiftEnterBanner } from export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); - serviceManager.addSingleton( - IDataScienceCommandListener, - InteractiveWindowCommandListener - ); serviceManager.addSingleton(IExtensionSingleActivationService, CommandRegistry); serviceManager.addSingleton(IExtensionSyncActivationService, HoverProvider); serviceManager.add(ICodeWatcher, CodeWatcher); diff --git a/src/interactive-window/serviceRegistry.web.ts b/src/interactive-window/serviceRegistry.web.ts index 34f06762196d..3c85221e480f 100644 --- a/src/interactive-window/serviceRegistry.web.ts +++ b/src/interactive-window/serviceRegistry.web.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. 'use strict'; -import { IDataScienceCommandListener } from '../platform/common/types'; import { ITracebackFormatter } from '../kernels/types'; import { IExtensionSingleActivationService, IExtensionSyncActivationService } from '../platform/activation/types'; import { IServiceManager } from '../platform/ioc/types'; @@ -17,7 +16,6 @@ import { IDataScienceCodeLensProvider, ICodeGeneratorFactory } from './editor-integration/types'; -import { InteractiveWindowCommandListener } from './interactiveWindowCommandListener'; import { InteractiveWindowProvider } from './interactiveWindowProvider'; import { IInteractiveWindowDebuggingManager, IInteractiveWindowProvider } from './types'; import { CodeGeneratorFactory } from './editor-integration/codeGeneratorFactory'; @@ -29,10 +27,6 @@ import { InteractiveWindowDebuggingManager } from './debugger/jupyter/debuggingM export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); - serviceManager.addSingleton( - IDataScienceCommandListener, - InteractiveWindowCommandListener - ); serviceManager.addSingleton(IExtensionSingleActivationService, CommandRegistry); serviceManager.add(ICodeWatcher, CodeWatcher); serviceManager.addSingleton(ICodeLensFactory, CodeLensFactory); From eb4348cdef3a83c3ebb2ee2a37f9d9701537b3c4 Mon Sep 17 00:00:00 2001 From: Rich Chiodo Date: Tue, 19 Jul 2022 14:32:06 -0700 Subject: [PATCH 4/8] Make some changes to contributing.md (#10862) --- CONTRIBUTING.md | 64 ++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4b5d7a540ba..331bdb7a75d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,12 +46,12 @@ python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python -- ### Incremental Build -Run the `Compile` and `Compile Web Views` build Tasks from the [Run Build Task...](https://code.visualstudio.com/docs/editor/tasks) command picker (short cut `CTRL+SHIFT+B` or `⇧⌘B`). This will leave build tasks running in the background and which will re-run as files are edited and saved. You can see the output from either task in the Terminal panel (use the selector to choose which output to look at). +Run the `Compile`, `Compile Web Views`, and `Compile Web Extension` build Tasks from the [Run Build Task...](https://code.visualstudio.com/docs/editor/tasks) command picker (short cut `CTRL+SHIFT+B` or `⇧⌘B`). This will leave build tasks running in the background and which will re-run as files are edited and saved. You can see the output from either task in the Terminal panel (use the selector to choose which output to look at). You can also compile from the command-line. For a full compile you can use: ```shell -npx gulp prePublishNonBundle +npx gulp prePublishNonBundleNLS ``` For incremental builds you can use the following commands depending on your needs: @@ -103,25 +103,14 @@ Alter the `launch.json` file in the `"Debug Unit Tests"` section by setting the ...this will only run the suite with the tests you care about during a test run (be sure to set the debugger to run the `Debug Unit Tests` launcher). -### Running Functional Tests - -Functional tests are those in files with extension `.functional.test.ts`. -These tests are similar to integration tests in scope, but are run like unit tests. - -You can run functional tests in a similar way to that for unit tests: - -- via the "Functional Tests" launch option, or -- on the command line via `npm run test:functional` - ### Running Integration Tests (with VS Code) Note: Integration tests are those in files with extension `*.vscode.test*.ts`. 1. Make sure you have compiled all code (done automatically when using incremental building) -1. Ensure you have disabled breaking into 'Uncaught Exceptions' when running the Unit Tests +1. Some of the tests require specific virtual environments. Run the 'src/test/datascience/setupTestEnvs.cmd` (or equivalent) to create them. 1. For the linters and formatters tests to pass successfully, you will need to have those corresponding Python libraries installed locally by using the `./requirements.txt` and `build/test-requirements.txt` files 1. Run the tests via `npm run` or the Debugger launch options (you can "Start Without Debugging"). -1. **Note** you will be running tests under the default Python interpreter for the system. You can also run the tests from the command-line (after compiling): @@ -228,28 +217,31 @@ smoothly, but it allows you to help out by noticing when a step is missed or to learn in case someday you become a project maintainer as well! -### Architecture - -At a high level we have a bunch of folders. Each high level is described below; - -- src\extension.node.ts = entry point for node extension -- src\extension.web.ts = entry point for web extension -- src\kernels = code related to executing jupyter kernels -- src\notebooks = code related to vscode notebook UI -- src\interactive-window = interactive window -- src\platform = grab bag of utilities common to rest of code -- src\web-views = code related to web views we create. Variable view, data viewer, and plot viewer are all here. Both extension and UI side code. -- src\intellisense = code related to starting pylance and combining pylance completions with kernel completions -- src\test = all test related code -- src\telemetry = all code related to gathering and sending telemetry -- build = build scripts and config files -- .github = github workflows and issue templates -- news = markdown files used to generate the next release's changelog -- pythonFiles = python files used to do things like view data in the data viewer -- typings = .d.ts files used to provide types for node_modules without any typings -- types = .ts files used to provide types for node_modules -- images = gifs and jpgs for markdown files -- docs = generated dependency graph files (obsolete at the moment) +### Folder Structure + +At a high level we have a bunch of folders. Each high level is described in this wiki [page](https://github.com/microsoft/vscode-jupyter/wiki/Source-Code-Organization) + +### Typical workflow + +Here's an example of a typical workflow: + +1. Sync to main (get your fork's main to match vscode-jupyter's main) +1. Create branch +1. `npm ci` +1. `npm run clean` +1. Start VS code Insiders root +1. CTRL+SHIFT+B and build `Compile Web Views` and `Compile` +1. Make code changes +1. Write and [run](https://github.com/microsoft/vscode-jupyter/blob/29c4be79f64df1858692321b43c3079bb77bdd69/.vscode/launch.json#L252) unit tests if appropriate +1. Test with [`Extension`](https://github.com/microsoft/vscode-jupyter/blob/29c4be79f64df1858692321b43c3079bb77bdd69/.vscode/launch.json#L6) launch task +1. Repeat until works in normal extension +1. Test with [`Extension (web)`](https://github.com/microsoft/vscode-jupyter/blob/29c4be79f64df1858692321b43c3079bb77bdd69/.vscode/launch.json#L34) launch task +1. Run [jupyter notebook server](https://github.com/microsoft/vscode-jupyter/wiki/Connecting-to-a-remote-Jupyter-server-from-vscode.dev) to use in web testing +1. Repeat until works in web extension +1. Write integration tests and [run](https://github.com/microsoft/vscode-jupyter/blob/29c4be79f64df1858692321b43c3079bb77bdd69/.vscode/launch.json#L216) locally. +1. Submit PR +1. Check PR output to make sure tests don't fail. +1. Debug [CI test failures](https://github.com/microsoft/vscode-jupyter/wiki/Tests) ### Helping others From 7308db89073d2f3a67c03d778a0b8a19e091328b Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 20 Jul 2022 10:45:36 +1000 Subject: [PATCH 5/8] Fixes related to loading of env variables (#10843) * Fixes * Misc * Misc * Fixed tests * Fix tests * Misc --- TELEMETRY.md | 10 +- .../jupyter/jupyterKernelService.node.ts | 86 ++----- .../raw/launcher/kernelEnvVarsService.node.ts | 42 ++-- .../environmentActivationService.node.ts | 80 ------ src/platform/common/variables/types.ts | 6 + .../kernelEnvVarsService.unit.test.ts | 235 ++++++++++++------ .../kernels/jupyterKernelService.unit.test.ts | 50 ++-- 7 files changed, 237 insertions(+), 272 deletions(-) diff --git a/TELEMETRY.md b/TELEMETRY.md index 7e6b78d7c3b7..b05c76880aaf 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -5589,13 +5589,13 @@ No properties for event [src/kernels/jupyter/jupyterKernelService.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/jupyter/jupyterKernelService.node.ts) ```typescript - await this.fs.writeFile(Uri.file(kernelSpecFilePath), JSON.stringify(specModel, undefined, 2)); - } catch (ex) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendTelemetryEvent(Telemetry.FailedToUpdateKernelSpec, undefined, undefined, ex as any, true); - throw ex; + return; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendTelemetryEvent(Telemetry.FailedToUpdateKernelSpec, undefined, undefined, ex as any, true); + throw ex; } + ```
diff --git a/src/kernels/jupyter/jupyterKernelService.node.ts b/src/kernels/jupyter/jupyterKernelService.node.ts index 39c00aee307c..d1d61305fcc1 100644 --- a/src/kernels/jupyter/jupyterKernelService.node.ts +++ b/src/kernels/jupyter/jupyterKernelService.node.ts @@ -20,14 +20,11 @@ import { } from '../../platform/logging'; import { getDisplayPath, getFilePath } from '../../platform/common/platform/fs-paths'; import { IFileSystemNode } from '../../platform/common/platform/types.node'; -import { Resource, ReadWrite, IDisplayOptions, IConfigurationService } from '../../platform/common/types'; -import { noop } from '../../platform/common/utils/misc'; -import { IEnvironmentVariablesService } from '../../platform/common/variables/types'; -import { IEnvironmentActivationService } from '../../platform/interpreter/activation/types'; +import { Resource, ReadWrite, IDisplayOptions } from '../../platform/common/types'; import { PythonEnvironment } from '../../platform/pythonEnvironments/info'; import { captureTelemetry, sendTelemetryEvent, Telemetry } from '../../telemetry'; import { JupyterKernelDependencyError } from '../errors/jupyterKernelDependencyError'; -import { getKernelRegistrationInfo, cleanEnvironment } from '../helpers'; +import { cleanEnvironment } from '../helpers'; import { JupyterPaths } from '../raw/finder/jupyterPaths.node'; import { IJupyterKernelSpec, @@ -40,6 +37,7 @@ import { JupyterKernelSpec } from './jupyterKernelSpec'; import { serializePythonEnvironment } from '../../platform/api/pythonApi'; import { IJupyterKernelService } from './types'; import { arePathsSame } from '../../platform/common/platform/fileUtils'; +import { KernelEnvironmentVariablesService } from '../raw/launcher/kernelEnvVarsService.node'; /** * Responsible for registering and updating kernels in a non ZMQ situation (kernel specs) @@ -52,10 +50,8 @@ export class JupyterKernelService implements IJupyterKernelService { constructor( @inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService, @inject(IFileSystemNode) private readonly fs: IFileSystemNode, - @inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService, - @inject(IEnvironmentVariablesService) private readonly envVarsService: IEnvironmentVariablesService, @inject(JupyterPaths) private readonly jupyterPaths: JupyterPaths, - @inject(IConfigurationService) private readonly configService: IConfigurationService + @inject(KernelEnvironmentVariablesService) private readonly kernelEnvVars: KernelEnvironmentVariablesService ) {} /** @@ -261,7 +257,9 @@ export class JupyterKernelService implements IJupyterKernelService { let specModel: ReadWrite = JSON.parse( await this.fs.readFile(Uri.file(kernelSpecFilePath)) ); - let shouldUpdate = false; + if (Cancellation.isCanceled(cancelToken)) { + return; + } // Make sure the specmodel has an interpreter or already in the metadata or we // may overwrite a kernel created by the user @@ -276,47 +274,6 @@ export class JupyterKernelService implements IJupyterKernelService { ); specModel.argv[0] = interpreter.uri.fsPath; } - // Get the activated environment variables (as a work around for `conda run` and similar). - // This ensures the code runs within the context of an activated environment. - const interpreterEnv = await this.activationHelper - .getActivatedEnvironmentVariables(resource, interpreter, true) - .catch(noop) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .then((env) => (env || {}) as any); - - // Ensure we inherit env variables from the original kernelspec file. - let envInKernelSpecJson = - getKernelRegistrationInfo(kernel) === 'registeredByNewVersionOfExtForCustomKernelSpec' - ? specModel.env || {} - : {}; - - // Give preferences to variables in the env file (except for `PATH`). - envInKernelSpecJson = Object.assign({ ...interpreterEnv }, envInKernelSpecJson); - if (interpreterEnv['PATH']) { - envInKernelSpecJson['PATH'] = interpreterEnv['PATH']; - } - if (interpreterEnv['Path']) { - envInKernelSpecJson['Path'] = interpreterEnv['Path']; - } - specModel.env = Object.assign(envInKernelSpecJson, specModel.env); - - // Ensure the python env folder is always at the top of the PATH, this way all executables from that env are used. - // This way shell commands such as `!pip`, `!python` end up pointing to the right executables. - // Also applies to `!java` where java could be an executable in the conda bin directory. - if (specModel.env) { - this.envVarsService.prependPath(specModel.env as {}, path.dirname(interpreter.uri.fsPath)); - } - - // If user asks us to, set PYTHONNOUSERSITE - // For more details see here https://github.com/microsoft/vscode-jupyter/issues/8553#issuecomment-997144591 - // https://docs.python.org/3/library/site.html#site.ENABLE_USER_SITE - if (this.configService.getSettings(undefined).excludeUserSitePackages) { - specModel.env.PYTHONNOUSERSITE = 'True'; - } - - if (Cancellation.isCanceled(cancelToken)) { - return; - } // Ensure we update the metadata to include interpreter stuff as well (we'll use this to search kernels that match an interpreter). // We'll need information such as interpreter type, display name, path, etc... @@ -324,27 +281,28 @@ export class JupyterKernelService implements IJupyterKernelService { specModel.metadata = specModel.metadata || {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any specModel.metadata.interpreter = interpreter as any; - - // Indicate we need to write - shouldUpdate = true; } + specModel.env = await this.kernelEnvVars.getEnvironmentVariables(resource, interpreter, specedKernel); + // Scrub the environment of the specmodel to make sure it has allowed values (they all must be strings) // See this issue here: https://github.com/microsoft/vscode-python/issues/11749 - if (specModel.env) { - specModel = cleanEnvironment(specModel); - shouldUpdate = true; - } + specModel = cleanEnvironment(specModel); // Update the kernel.json with our new stuff. - if (shouldUpdate) { - try { - await this.fs.writeFile(Uri.file(kernelSpecFilePath), JSON.stringify(specModel, undefined, 2)); - } catch (ex) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendTelemetryEvent(Telemetry.FailedToUpdateKernelSpec, undefined, undefined, ex as any, true); - throw ex; + try { + await this.fs.writeFile(Uri.file(kernelSpecFilePath), JSON.stringify(specModel, undefined, 2)); + } catch (ex) { + if (Cancellation.isCanceled(cancelToken)) { + return; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendTelemetryEvent(Telemetry.FailedToUpdateKernelSpec, undefined, undefined, ex as any, true); + throw ex; + } + + if (Cancellation.isCanceled(cancelToken)) { + return; } // Always update the metadata for the original kernel. diff --git a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts index 96c79778a163..1b867e94361f 100644 --- a/src/kernels/raw/launcher/kernelEnvVarsService.node.ts +++ b/src/kernels/raw/launcher/kernelEnvVarsService.node.ts @@ -27,6 +27,13 @@ export class KernelEnvironmentVariablesService { @inject(IConfigurationService) private readonly configService: IConfigurationService ) {} /** + * Generates the environment variables for the kernel. + * + * Merge env variables from the following 3 sources: + * 1. kernelspec.json file + * 2. process.env + * 3. .env file in workspace folder + * * If the kernel belongs to a conda environment, then use the env variables of the conda environment and merge that with the env variables of the kernel spec. * In the case of some kernels such as java, the kernel spec contains the cli as such `argv = ['java', 'xyz']`. * The first item in the kernelspec argv is an kernel executable, and it might not in the current path (e.g. `java`). @@ -40,13 +47,7 @@ export class KernelEnvironmentVariablesService { let kernelEnv = kernelSpec.env && Object.keys(kernelSpec.env).length > 0 ? kernelSpec.env : undefined; // If an interpreter was not explicitly passed in, check for an interpreter path in the kernelspec to use - if (!interpreter) { - if (!kernelSpec.interpreterPath) { - traceInfo( - `No custom variables for Kernel as interpreter path is not defined for kernel ${kernelSpec.display_name}` - ); - return kernelEnv; - } + if (!interpreter && kernelSpec.interpreterPath) { interpreter = await this.interpreterService .getInterpreterDetails(Uri.file(kernelSpec.interpreterPath)) .catch((ex) => { @@ -55,7 +56,7 @@ export class KernelEnvironmentVariablesService { }); } - let [customEditVars, interpreterEnv] = await Promise.all([ + let [customEnvVars, interpreterEnv] = await Promise.all([ this.customEnvVars.getCustomEnvironmentVariables(resource).catch(noop), interpreter ? this.envActivation @@ -66,26 +67,17 @@ export class KernelEnvironmentVariablesService { }) : undefined ]); - if (!interpreterEnv && Object.keys(customEditVars || {}).length === 0) { + if (!interpreterEnv && Object.keys(customEnvVars || {}).length === 0) { traceInfo('No custom variables nor do we have a conda environment'); - // Ensure the python env folder is always at the top of the PATH, this way all executables from that env are used. - // This way shell commands such as `!pip`, `!python` end up pointing to the right executables. - // Also applies to `!java` where java could be an executable in the conda bin directory. - if (interpreter) { - const env = kernelEnv || process.env; - this.envVarsService.prependPath(env, path.dirname(interpreter.uri.fsPath)); - return env; - } - return kernelEnv; } // Merge the env variables with that of the kernel env. interpreterEnv = interpreterEnv || {}; const mergedVars = { ...process.env }; kernelEnv = kernelEnv || {}; - customEditVars = customEditVars || {}; + customEnvVars = customEnvVars || {}; this.envVarsService.mergeVariables(interpreterEnv, mergedVars); // interpreter vars win over proc. this.envVarsService.mergeVariables(kernelEnv, mergedVars); // kernels vars win over interpreter. - this.envVarsService.mergeVariables(customEditVars, mergedVars); // custom vars win over all. + this.envVarsService.mergeVariables(customEnvVars, mergedVars); // custom vars win over all. // Reinitialize the PATH variables. // The values in `PATH` found in the interpreter trumps everything else. // If we have more PATHS, they need to be appended to this PATH. @@ -99,16 +91,16 @@ export class KernelEnvironmentVariablesService { if (interpreterEnv['PYTHONPATH']) { mergedVars['PYTHONPATH'] = interpreterEnv['PYTHONPATH']; } - otherEnvPathKey = Object.keys(customEditVars).find((k) => k.toLowerCase() == 'path'); - if (otherEnvPathKey && customEditVars[otherEnvPathKey]) { - this.envVarsService.appendPath(mergedVars, customEditVars[otherEnvPathKey]!); + otherEnvPathKey = Object.keys(customEnvVars).find((k) => k.toLowerCase() == 'path'); + if (otherEnvPathKey && customEnvVars[otherEnvPathKey]) { + this.envVarsService.appendPath(mergedVars, customEnvVars[otherEnvPathKey]!); } otherEnvPathKey = Object.keys(kernelEnv).find((k) => k.toLowerCase() == 'path'); if (otherEnvPathKey && kernelEnv[otherEnvPathKey]) { this.envVarsService.appendPath(mergedVars, kernelEnv[otherEnvPathKey]!); } - if (customEditVars.PYTHONPATH) { - this.envVarsService.appendPythonPath(mergedVars, customEditVars.PYTHONPATH); + if (customEnvVars.PYTHONPATH) { + this.envVarsService.appendPythonPath(mergedVars, customEnvVars.PYTHONPATH); } if (kernelEnv.PYTHONPATH) { this.envVarsService.appendPythonPath(mergedVars, kernelEnv.PYTHONPATH); diff --git a/src/platform/common/process/environmentActivationService.node.ts b/src/platform/common/process/environmentActivationService.node.ts index 3fbbe34468a8..493e2d4f7f47 100644 --- a/src/platform/common/process/environmentActivationService.node.ts +++ b/src/platform/common/process/environmentActivationService.node.ts @@ -318,29 +318,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi return; } - // If this is a conda environment that supports conda run, then we don't need conda activation commands. - const [activationCommands, customEnvVars] = await Promise.all([ - interpreter.envType === EnvironmentType.Conda - ? Promise.resolve([]) - : this.getActivationCommands(resource, interpreter), - this.envVarsService.getCustomEnvironmentVariables(resource) - ]); - - // Check cache. - const customEnvVariablesHash = getTelemetrySafeHashedString(JSON.stringify(customEnvVars)); - const cachedVariables = this.getActivatedEnvVariablesFromCache( - resource, - interpreter, - customEnvVariablesHash, - activationCommands - ); - if (cachedVariables) { - traceVerbose(`Got activation Env Vars from cache`); - return cachedVariables && customEnvVars - ? { ...cachedVariables, ...customEnvVars } - : cachedVariables || customEnvVars; - } - if (this.activatedEnvVariablesCache.has(key)) { traceVerbose(`Got activation Env Vars from cached promise with key ${key}`); return this.activatedEnvVariablesCache.get(key); @@ -529,53 +506,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi ); } - /** - * We cache activated environment variables. - * When activating environments, all activation scripts update environment variables, nothing else (after all they don't start a process). - * The env variables can change based on the activation script, current env variables on the machine & python interpreter information. - * If any of these change, then the env variables are invalidated. - */ - private getActivatedEnvVariablesFromCache( - resource: Resource, - @logValue('uri') interpreter: PythonEnvironment, - customEnvVariablesHash: string, - activationCommandsForNonCondaEnvironments: string[] = [] - ) { - const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); - const key = ENVIRONMENT_ACTIVATED_ENV_VARS_KEY_PREFIX.format( - `${workspaceKey}_${interpreter && getInterpreterHash(interpreter)}` - ); - const interpreterVersion = `${interpreter.sysVersion || ''}#${interpreter.version?.raw || ''}`; - const cachedData = this.memento.get(key); - if (!cachedData || !cachedData.activatedEnvVariables) { - return; - } - if ( - cachedData.interpreterVersion !== interpreterVersion || - cachedData.customEnvVariablesHash !== customEnvVariablesHash - ) { - return; - } - // We're interested in activation commands only for non-conda environments. - // For conda environments, we don't care about the activation commands (as we activate either using conda activation commands - // Or use conda run). - // Hence for purposes of caching we don't care about the commands. - if ( - interpreter.envType !== EnvironmentType.Conda && - cachedData.activationCommands.join(',').toLowerCase() !== - (activationCommandsForNonCondaEnvironments || []).join(',').toLowerCase() - ) { - return; - } - if ( - cachedData.originalProcEnvVariablesHash !== - getTelemetrySafeHashedString(JSON.stringify(this.sanitizedCurrentProcessEnvVars)) - ) { - return; - } - this.updateWithLatestVSCodeVariables(cachedData.activatedEnvVariables); - return cachedData.activatedEnvVariables; - } private async storeActivatedEnvVariablesInCache( resource: Resource, @logValue('uri') interpreter: PythonEnvironment, @@ -619,16 +549,6 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi }); return vars; } - private updateWithLatestVSCodeVariables(envVars: EnvironmentVariables) { - // Restore the env vars we removed. - const vars = JSON.parse(JSON.stringify(this.currentProcess.env)); - Object.keys(vars).forEach((key) => { - if (key.startsWith('VSCODE_')) { - envVars[key] = vars[key]; - } - }); - } - @traceDecoratorVerbose('getCondaEnvVariables', TraceOptions.BeforeCall) public async getCondaEnvVariables( resource: Resource, diff --git a/src/platform/common/variables/types.ts b/src/platform/common/variables/types.ts index eb6ac238e302..728894cb4c36 100644 --- a/src/platform/common/variables/types.ts +++ b/src/platform/common/variables/types.ts @@ -53,6 +53,12 @@ export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvid export interface IEnvironmentVariablesProvider { onDidEnvironmentVariablesChange: Event; + /** + * Gets merged result of process.env and env variables defined in the .env file. + */ getEnvironmentVariables(resource?: Uri): Promise; + /** + * Gets the env variables defined in the .env file. + */ getCustomEnvironmentVariables(resource?: Uri): Promise; } diff --git a/src/test/common/variables/kernelEnvVarsService.unit.test.ts b/src/test/common/variables/kernelEnvVarsService.unit.test.ts index f45a89724982..873cec2b0ef1 100644 --- a/src/test/common/variables/kernelEnvVarsService.unit.test.ts +++ b/src/test/common/variables/kernelEnvVarsService.unit.test.ts @@ -32,21 +32,23 @@ suite('Kernel Environment Variables Service', () => { let interpreterService: IInterpreterService; let configService: IConfigurationService; let settings: IWatchableJupyterSettings; - const pathFile = Uri.file('foobar'); + const pathFile = Uri.joinPath(Uri.file('foobar'), 'bar'); const interpreter: PythonEnvironment = { envType: EnvironmentType.Conda, uri: pathFile, sysPrefix: '0' }; - const kernelSpec: IJupyterKernelSpec = { - name: 'kernel', - executable: pathFile.fsPath, - display_name: 'kernel', - interpreterPath: pathFile.fsPath, - argv: [] - }; - + let kernelSpec: IJupyterKernelSpec; + const originalEnvVars = Object.assign({}, process.env); + const processPath = Object.keys(process.env).find((k) => k.toLowerCase() == 'path'); setup(() => { + kernelSpec = { + name: 'kernel', + executable: pathFile.fsPath, + display_name: 'kernel', + interpreterPath: pathFile.fsPath, + argv: [] + }; fs = mock(); envActivation = mock(); customVariablesService = mock(); @@ -63,91 +65,160 @@ suite('Kernel Environment Variables Service', () => { instance(configService) ); }); + teardown(() => Object.assign(process.env, originalEnvVars)); - suite(`getEnvironmentVariables()`, () => { - test('Interpreter path trumps process', async () => { - when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ - PATH: 'foobar' - }); - when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve(); + test('Interpreter path trumps process', async () => { + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'foobar' + }); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve(); - const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); - const processPath = Object.keys(process.env).find((k) => k.toLowerCase() == 'path'); - assert.isOk(processPath); - assert.isOk(vars); - assert.strictEqual(vars![processPath!], `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar`); + assert.isOk(processPath); + assert.strictEqual(vars![processPath!], `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar`); + }); + test('Interpreter env variable trumps process', async () => { + process.env['HELLO_VAR'] = 'process'; + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + HELLO_VAR: 'new' }); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve(); + + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); - test('Paths are merged', async () => { - when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ - PATH: 'foobar' - }); - when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); - - const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); - const processPath = Object.keys(process.env).find((k) => k.toLowerCase() == 'path'); - assert.isOk(processPath); - assert.isOk(vars); - assert.strictEqual( - vars![processPath!], - `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar${path.delimiter}foobaz` - ); + assert.strictEqual(vars['HELLO_VAR'], 'new'); + // Compare ignoring the PATH variable. + assert.deepEqual( + Object.assign(vars, { PATH: '', Path: '' }), + Object.assign({}, process.env, { HELLO_VAR: 'new' }, { PATH: '', Path: '' }) + ); + }); + + test('Custom env variable trumps process and interpreter envs', async () => { + process.env['HELLO_VAR'] = 'process'; + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + HELLO_VAR: 'interpreter' }); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ HELLO_VAR: 'new' }); + + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); + + assert.strictEqual(vars['HELLO_VAR'], 'new'); + // Compare ignoring the PATH variable. + assert.deepEqual( + Object.assign(vars, { PATH: '', Path: '' }), + Object.assign({}, process.env, { HELLO_VAR: 'new' }, { PATH: '', Path: '' }) + ); + }); + + test('Custom env variable trumps process (non-python)', async () => { + process.env['HELLO_VAR'] = 'very old'; + delete kernelSpec.interpreterPath; + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({}); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ HELLO_VAR: 'new' }); + + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + + assert.strictEqual(vars['HELLO_VAR'], 'new'); + // Compare ignoring the PATH variable. + assert.deepEqual( + Object.assign(vars, { PATH: '', Path: '' }), + Object.assign({}, process.env, { HELLO_VAR: 'new' }, { PATH: '', Path: '' }) + ); + }); + + test('Returns process.env vars if no interpreter and no kernelspec.env', async () => { + delete kernelSpec.interpreterPath; + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve(); + + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + + assert.deepEqual(vars, process.env); + }); + + test('Returns process.env vars if unable to get activated vars for interpreter and no kernelspec.env', async () => { + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve(); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve(); + + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); + + // Compare ignoring the PATH variable. + assert.deepEqual( + Object.assign({}, vars, { PATH: '', Path: '' }), + Object.assign({}, process.env, { PATH: '', Path: '' }) + ); + assert.strictEqual( + vars![processPath!], + `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}${process.env[processPath!]}` + ); + }); - test('KernelSpec interpreterPath used if interpreter is undefined', async () => { - when(interpreterService.getInterpreterDetails(anything())).thenResolve({ - envType: EnvironmentType.Conda, - uri: Uri.file('foopath'), - sysPrefix: 'foosysprefix' - }); - when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ - PATH: 'foobar' - }); - when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); - - // undefined for interpreter here, interpreterPath from the spec should be used - const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); - const processPath = Object.keys(process.env).find((k) => k.toLowerCase() == 'path'); - assert.isOk(processPath); - assert.isOk(vars); - assert.strictEqual( - vars![processPath!], - `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar${path.delimiter}foobaz` - ); + test('Paths are merged', async () => { + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'foobar' }); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); - async function testPYTHONNOUSERSITE(envType: EnvironmentType, shouldBeSet: boolean) { - when(interpreterService.getInterpreterDetails(anything())).thenResolve({ - envType, - uri: Uri.file('foopath'), - sysPrefix: 'foosysprefix' - }); - when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ - PATH: 'foobar' - }); - when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); - when(settings.excludeUserSitePackages).thenReturn(shouldBeSet); - - // undefined for interpreter here, interpreterPath from the spec should be used - const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); - assert.isOk(vars); - - if (shouldBeSet) { - assert.isOk(vars!['PYTHONNOUSERSITE'], 'PYTHONNOUSERSITE should be set'); - } else { - assert.isUndefined(vars!['PYTHONNOUSERSITE'], 'PYTHONNOUSERSITE should not be set'); - } - } + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, interpreter, kernelSpec); + assert.isOk(processPath); + assert.strictEqual( + vars![processPath!], + `${path.dirname(interpreter.uri.fsPath)}${path.delimiter}foobar${path.delimiter}foobaz` + ); + }); - test('PYTHONNOUSERSITE should not be set for Global Interpreters', async () => { - await testPYTHONNOUSERSITE(EnvironmentType.Global, false); + test('KernelSpec interpreterPath used if interpreter is undefined', async () => { + when(interpreterService.getInterpreterDetails(anything())).thenResolve({ + envType: EnvironmentType.Conda, + uri: Uri.joinPath(Uri.file('env'), 'foopath'), + sysPrefix: 'foosysprefix' }); - test('PYTHONNOUSERSITE should be set for Conda Env', async () => { - await testPYTHONNOUSERSITE(EnvironmentType.Conda, true); + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'pathInInterpreterEnv' }); - test('PYTHONNOUSERSITE should be set for Virtual Env', async () => { - await testPYTHONNOUSERSITE(EnvironmentType.VirtualEnv, true); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); + + // undefined for interpreter here, interpreterPath from the spec should be used + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + assert.isOk(processPath); + assert.strictEqual( + vars![processPath!], + `${path.dirname(Uri.joinPath(Uri.file('env'), 'foopath').fsPath)}${path.delimiter}pathInInterpreterEnv${ + path.delimiter + }foobaz` + ); + }); + + async function testPYTHONNOUSERSITE(envType: EnvironmentType, shouldBeSet: boolean) { + when(interpreterService.getInterpreterDetails(anything())).thenResolve({ + envType, + uri: Uri.file('foopath'), + sysPrefix: 'foosysprefix' }); + when(envActivation.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ + PATH: 'foobar' + }); + when(customVariablesService.getCustomEnvironmentVariables(anything())).thenResolve({ PATH: 'foobaz' }); + when(settings.excludeUserSitePackages).thenReturn(shouldBeSet); + + // undefined for interpreter here, interpreterPath from the spec should be used + const vars = await kernelVariablesService.getEnvironmentVariables(undefined, undefined, kernelSpec); + + if (shouldBeSet) { + assert.isOk(vars!['PYTHONNOUSERSITE'], 'PYTHONNOUSERSITE should be set'); + } else { + assert.isUndefined(vars!['PYTHONNOUSERSITE'], 'PYTHONNOUSERSITE should not be set'); + } + } + + test('PYTHONNOUSERSITE should not be set for Global Interpreters', async () => { + await testPYTHONNOUSERSITE(EnvironmentType.Global, false); + }); + test('PYTHONNOUSERSITE should be set for Conda Env', async () => { + await testPYTHONNOUSERSITE(EnvironmentType.Conda, true); + }); + test('PYTHONNOUSERSITE should be set for Virtual Env', async () => { + await testPYTHONNOUSERSITE(EnvironmentType.VirtualEnv, true); }); }); diff --git a/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts b/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts index 026fbb6024d1..7302f17cf8d3 100644 --- a/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts +++ b/src/test/datascience/jupyter/kernels/jupyterKernelService.unit.test.ts @@ -9,31 +9,34 @@ import { FileSystem } from '../../../../platform/common/platform/fileSystem.node import { IFileSystemNode } from '../../../../platform/common/platform/types.node'; import { KernelDependencyService } from '../../../../platform/../kernels/kernelDependencyService.node'; import { IKernelDependencyService, LocalKernelConnectionMetadata } from '../../../../platform/../kernels/types'; -import { IEnvironmentActivationService } from '../../../../platform/interpreter/activation/types'; import { EnvironmentType } from '../../../../platform/pythonEnvironments/info'; import { EXTENSION_ROOT_DIR } from '../../../../platform/constants.node'; import * as path from '../../../../platform/vscode-path/path'; import { CancellationTokenSource, Uri } from 'vscode'; -import { EnvironmentVariablesService } from '../../../../platform/common/variables/environment.node'; import { JupyterKernelService } from '../../../../kernels/jupyter/jupyterKernelService.node'; import { JupyterPaths } from '../../../../kernels/raw/finder/jupyterPaths.node'; import { DisplayOptions } from '../../../../kernels/displayOptions'; -import { getOSType, OSType } from '../../../../platform/common/utils/platform'; -import { IConfigurationService, IWatchableJupyterSettings } from '../../../../platform/common/types'; +import { IWatchableJupyterSettings } from '../../../../platform/common/types'; import { ConfigurationService } from '../../../../platform/common/configuration/service.node'; import { JupyterSettings } from '../../../../platform/common/configSettings'; import { uriEquals } from '../../helpers'; +import { KernelEnvironmentVariablesService } from '../../../../kernels/raw/launcher/kernelEnvVarsService.node'; +import { IInterpreterService } from '../../../../platform/interpreter/contracts'; +import { IEnvironmentActivationService } from '../../../../platform/interpreter/activation/types'; +import { IEnvironmentVariablesProvider } from '../../../../platform/common/variables/types'; +import { EnvironmentVariablesService } from '../../../../platform/common/variables/environment.node'; +import { isWeb } from '../../../../platform/common/utils/misc'; // eslint-disable-next-line suite('DataScience - JupyterKernelService', () => { let kernelService: JupyterKernelService; let kernelDependencyService: IKernelDependencyService; - let configService: IConfigurationService; + let appEnv: IEnvironmentActivationService; let settings: IWatchableJupyterSettings; let fs: IFileSystemNode; - let appEnv: IEnvironmentActivationService; let testWorkspaceFolder: Uri; - const pathVariable = getOSType() === OSType.Windows ? 'PATH' : 'Path'; + // eslint-disable-next-line local-rules/dont-use-process + const pathVariable = Object.keys(process.env).find((k) => k.toLowerCase() == 'path')!; // Set of kernels. Generated this by running the localKernelFinder unit test and stringifying // the results returned. @@ -361,6 +364,11 @@ suite('DataScience - JupyterKernelService', () => { id: '14' } ]; + suiteSetup(function () { + if (isWeb()) { + return this.skip(); + } + }); setup(() => { kernelDependencyService = mock(KernelDependencyService); fs = mock(FileSystem); @@ -379,21 +387,31 @@ suite('DataScience - JupyterKernelService', () => { return Promise.reject('Invalid file'); }); when(fs.searchLocal(anything(), anything())).thenResolve([]); + const interpreterService = mock(); appEnv = mock(); when(appEnv.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({}); + const variablesService = new EnvironmentVariablesService(instance(fs)); + const customEnvVars = mock(); + when(customEnvVars.getCustomEnvironmentVariables(anything())).thenResolve(); + settings = mock(JupyterSettings); + const configService = mock(ConfigurationService); + settings = mock(JupyterSettings); + when(configService.getSettings(anything())).thenReturn(instance(settings)); + const kernelEnvService = new KernelEnvironmentVariablesService( + instance(interpreterService), + instance(appEnv), + variablesService, + instance(customEnvVars), + instance(configService) + ); testWorkspaceFolder = Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')); const jupyterPaths = mock(); when(jupyterPaths.getKernelSpecTempRegistrationFolder()).thenResolve(testWorkspaceFolder); - configService = mock(ConfigurationService); - settings = mock(JupyterSettings); - when(configService.getSettings(anything())).thenReturn(instance(settings)); kernelService = new JupyterKernelService( instance(kernelDependencyService), instance(fs), - instance(appEnv), - new EnvironmentVariablesService(instance(fs)), instance(jupyterPaths), - instance(configService) + kernelEnvService ); }); test('Dependencies checked on all kernels with interpreters', async () => { @@ -565,11 +583,11 @@ suite('DataScience - JupyterKernelService', () => { ); // capture(fs.localFileExists) }); - test('Kernel environment not updated when not custom interpreter', async () => { + test('Kernel environment should be updated even when there is no interpreter', async () => { const kernelsWithoutInterpreters = kernels.filter((k) => k.interpreter && !k.kernelSpec?.metadata?.interpreter); let updateCount = 0; - when(appEnv.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ foo: 'bar' }); when(fs.exists(anything())).thenResolve(true); + when(appEnv.getActivatedEnvironmentVariables(anything(), anything(), anything())).thenResolve({ foo: 'bar' }); when(fs.writeFile(anything(), anything())).thenCall((f: Uri, c) => { if (f.fsPath.endsWith('.json')) { const obj = JSON.parse(c); @@ -586,6 +604,6 @@ suite('DataScience - JupyterKernelService', () => { }) ); token.dispose(); - assert.equal(updateCount, 0, 'Should not have updated spec files when no interpreter metadata'); + assert.equal(updateCount, kernelsWithoutInterpreters.length); }); }); From 87225e5fa1be6ce2a43814d1df8218862821a5b6 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 20 Jul 2022 12:11:05 +1000 Subject: [PATCH 6/8] Cache kernels for a specific version of extension (#10852) * Cache kernels for a specific version of extension * Comments --- src/kernels/kernelFinder.base.ts | 59 ++++++++++++++++--- src/kernels/kernelFinder.node.ts | 7 ++- src/kernels/kernelFinder.web.ts | 7 ++- .../applicationEnvironment.base.ts | 4 ++ src/platform/common/application/types.ts | 6 ++ .../localKernelFinder.unit.test.ts | 6 +- .../remoteKernelFinder.unit.test.ts | 41 +++++++++++-- 7 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/kernels/kernelFinder.base.ts b/src/kernels/kernelFinder.base.ts index 79c60999e5e3..1c18cc2897d0 100644 --- a/src/kernels/kernelFinder.base.ts +++ b/src/kernels/kernelFinder.base.ts @@ -25,10 +25,11 @@ import { isLocalConnection, KernelConnectionMetadata } from './types'; +import { IApplicationEnvironment } from '../platform/common/application/types'; // Two cache keys so we can get local and remote separately (exported for tests) -export const LocalKernelSpecsCacheKey = 'JUPYTER_LOCAL_KERNELSPECS_V3'; -export const RemoteKernelSpecsCacheKey = 'JUPYTER_REMOTE_KERNELSPECS_V3'; +export const LocalKernelSpecsCacheKey = 'JUPYTER_LOCAL_KERNELSPECS_V4'; +export const RemoteKernelSpecsCacheKey = 'JUPYTER_REMOTE_KERNELSPECS_V4'; /** * Generic class for finding kernels (both remote and local). Handles all of the caching of the results. @@ -45,7 +46,8 @@ export abstract class BaseKernelFinder implements IKernelFinder { private readonly remoteKernelFinder: IRemoteKernelFinder | undefined, private readonly globalState: Memento, protected readonly serverUriStorage: IJupyterServerUriStorage, - protected readonly serverConnectionType: IServerConnectionType + protected readonly serverConnectionType: IServerConnectionType, + private readonly env: IApplicationEnvironment ) {} @traceDecoratorVerbose('Rank Kernels', TraceOptions.BeforeCall | TraceOptions.Arguments) @@ -274,14 +276,26 @@ export abstract class BaseKernelFinder implements IKernelFinder { cancelToken?: CancellationToken ): Promise { let results: KernelConnectionMetadata[] = this.cache.get(kind) || []; - const key = kind === 'local' ? LocalKernelSpecsCacheKey : RemoteKernelSpecsCacheKey; + const key = this.getCacheKey(kind); // If not in memory, check memento if (!results || results.length === 0) { // Check memento too - const values = this.globalState.get(key, []); - if (values && isArray(values)) { - results = values.map(deserializeKernelConnection); + const values = this.globalState.get<{ kernels: KernelConnectionMetadata[]; extensionVersion: string }>( + key, + { kernels: [], extensionVersion: '' } + ); + + /** + * The cached list of raw kernels is pointing to kernelSpec.json files in the extensions directory. + * Assume you have version 1 of extension installed. + * Now you update to version 2, at this point the cache still points to version 1 and the kernelSpec.json files are in the directory version 1. + * Those files in directory for version 1 could get deleted by VS Code at any point in time, as thats an old version of the extension and user has now installed version 2. + * Hence its wrong and buggy to use those files. + * To ensure we don't run into weird issues with the use of cached kernelSpec.json files, we ensure the cache is tied to each version of the extension. + */ + if (values && isArray(values.kernels) && values.extensionVersion === this.env.extensionVersion) { + results = values.kernels.map(deserializeKernelConnection); this.cache.set(kind, results); } } @@ -307,9 +321,36 @@ export abstract class BaseKernelFinder implements IKernelFinder { } protected async writeToCache(kind: 'local' | 'remote', values: KernelConnectionMetadata[]) { - const key = kind === 'local' ? LocalKernelSpecsCacheKey : RemoteKernelSpecsCacheKey; + const key = this.getCacheKey(kind); this.cache.set(kind, values); const serialized = values.map(serializeKernelConnection); - return this.globalState.update(key, serialized); + await Promise.all([ + this.removeOldCachedItems(), + , + this.globalState.update(key, { kernels: serialized, extensionVersion: this.env.extensionVersion }) + ]); + } + /** + * The old cached items can be quite large and we should clear them if we no longer need them. + */ + private async removeOldCachedItems(): Promise { + await Promise.all( + [ + 'JUPYTER_LOCAL_KERNELSPECS', + 'JUPYTER_LOCAL_KERNELSPECS_V1', + 'JUPYTER_LOCAL_KERNELSPECS_V2', + 'JUPYTER_LOCAL_KERNELSPECS_V3', + 'JUPYTER_REMOTE_KERNELSPECS', + 'JUPYTER_REMOTE_KERNELSPECS_V1', + 'JUPYTER_REMOTE_KERNELSPECS_V2', + 'JUPYTER_REMOTE_KERNELSPECS_V3' + ] + .filter((key) => LocalKernelSpecsCacheKey !== key && RemoteKernelSpecsCacheKey !== key) // Exclude latest cache key + .filter((key) => this.globalState.get(key, undefined) !== undefined) + .map((key) => this.globalState.update(key, undefined).then(noop, noop)) + ); + } + private getCacheKey(kind: 'local' | 'remote') { + return kind === 'local' ? LocalKernelSpecsCacheKey : RemoteKernelSpecsCacheKey; } } diff --git a/src/kernels/kernelFinder.node.ts b/src/kernels/kernelFinder.node.ts index 2e52bd08eac2..632374343918 100644 --- a/src/kernels/kernelFinder.node.ts +++ b/src/kernels/kernelFinder.node.ts @@ -9,6 +9,7 @@ import { PreferredRemoteKernelIdProvider } from './jupyter/preferredRemoteKernel import { ILocalKernelFinder, IRemoteKernelFinder } from './raw/types'; import { INotebookProvider, KernelConnectionMetadata } from './types'; import { IFileSystem } from '../platform/common/platform/types'; +import { IApplicationEnvironment } from '../platform/common/application/types'; /** * Node version of a KernelFinder. Node has different ways to validate than web. @@ -25,7 +26,8 @@ export class KernelFinder extends BaseKernelFinder { @inject(IJupyterServerUriStorage) serverUriStorage: IJupyterServerUriStorage, @inject(IServerConnectionType) serverConnectionType: IServerConnectionType, @inject(IJupyterRemoteCachedKernelValidator) - protected readonly cachedRemoteKernelValidator: IJupyterRemoteCachedKernelValidator + protected readonly cachedRemoteKernelValidator: IJupyterRemoteCachedKernelValidator, + @inject(IApplicationEnvironment) env: IApplicationEnvironment ) { super( preferredRemoteFinder, @@ -34,7 +36,8 @@ export class KernelFinder extends BaseKernelFinder { remoteKernelFinder, globalState, serverUriStorage, - serverConnectionType + serverConnectionType, + env ); } diff --git a/src/kernels/kernelFinder.web.ts b/src/kernels/kernelFinder.web.ts index 43f823868a8d..e0d4a009ff93 100644 --- a/src/kernels/kernelFinder.web.ts +++ b/src/kernels/kernelFinder.web.ts @@ -8,6 +8,7 @@ import { BaseKernelFinder } from './kernelFinder.base'; import { PreferredRemoteKernelIdProvider } from './jupyter/preferredRemoteKernelIdProvider'; import { IRemoteKernelFinder } from './raw/types'; import { INotebookProvider, KernelConnectionMetadata } from './types'; +import { IApplicationEnvironment } from '../platform/common/application/types'; /** * Web version of a KernelFinder. Web has different ways to validate than node. @@ -22,7 +23,8 @@ export class KernelFinder extends BaseKernelFinder { @inject(IJupyterServerUriStorage) serverUriStorage: IJupyterServerUriStorage, @inject(IServerConnectionType) serverConnectionType: IServerConnectionType, @inject(IJupyterRemoteCachedKernelValidator) - protected readonly cachedRemoteKernelValidator: IJupyterRemoteCachedKernelValidator + protected readonly cachedRemoteKernelValidator: IJupyterRemoteCachedKernelValidator, + @inject(IApplicationEnvironment) env: IApplicationEnvironment ) { super( preferredRemoteFinder, @@ -31,7 +33,8 @@ export class KernelFinder extends BaseKernelFinder { remoteKernelFinder, globalState, serverUriStorage, - serverConnectionType + serverConnectionType, + env ); } protected async isValidCachedKernel(kernel: KernelConnectionMetadata): Promise { diff --git a/src/platform/common/application/applicationEnvironment.base.ts b/src/platform/common/application/applicationEnvironment.base.ts index 8b66cc76d7d1..062e7862fcda 100644 --- a/src/platform/common/application/applicationEnvironment.base.ts +++ b/src/platform/common/application/applicationEnvironment.base.ts @@ -34,6 +34,10 @@ export abstract class BaseApplicationEnvironment implements IApplicationEnvironm // eslint-disable-next-line return this.packageJson.displayName; } + public get extensionVersion(): string { + // eslint-disable-next-line + return this.packageJson.version; + } /** * At the time of writing this API, the vscode.env.shell isn't officially released in stable version of VS Code. * Using this in stable version seems to throw errors in VSC with messages being displayed to the user about use of diff --git a/src/platform/common/application/types.ts b/src/platform/common/application/types.ts index 4c7e49eaf49e..70a110f7a150 100644 --- a/src/platform/common/application/types.ts +++ b/src/platform/common/application/types.ts @@ -963,6 +963,12 @@ export interface IApplicationEnvironment { * @readonly */ readonly extensionName: string; + /** + * The extension name. + * + * @readonly + */ + readonly extensionVersion: string; /** * The application root folder from which the editor is running. diff --git a/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts b/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts index e27fdc461462..9921410debe5 100644 --- a/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts +++ b/src/test/datascience/kernel-launcher/localKernelFinder.unit.test.ts @@ -58,6 +58,7 @@ import { IJupyterRemoteCachedKernelValidator, IServerConnectionType } from '../. import { uriEquals } from '../helpers'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../../platform/common/process/types.node'; import { getUserHomeDir } from '../../../platform/common/utils/platform.node'; +import { IApplicationEnvironment } from '../../../platform/common/application/types'; [false, true].forEach((isWindows) => { suite(`Local Kernel Finder ${isWindows ? 'Windows' : 'Unix'}`, () => { @@ -133,6 +134,8 @@ import { getUserHomeDir } from '../../../platform/common/utils/platform.node'; fs = mock(FileSystem); when(fs.delete(anything())).thenResolve(); when(fs.exists(anything())).thenResolve(true); + const env = mock(); + when(env.extensionVersion).thenReturn(''); const workspaceService = mock(WorkspaceService); const testWorkspaceFolder = Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')); @@ -264,7 +267,8 @@ import { getUserHomeDir } from '../../../platform/common/utils/platform.node'; instance(fs), instance(serverUriStorage), instance(connectionType), - instance(cachedRemoteKernelValidator) + instance(cachedRemoteKernelValidator), + instance(env) ); } teardown(() => { diff --git a/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts b/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts index df5bd46ec0a3..9aa787e41bc5 100644 --- a/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts +++ b/src/test/datascience/kernel-launcher/remoteKernelFinder.unit.test.ts @@ -42,6 +42,7 @@ import { JupyterServerUriStorage } from '../../../kernels/jupyter/launcher/serve import { FileSystem } from '../../../platform/common/platform/fileSystem.node'; import { takeTopRankKernel } from './localKernelFinder.unit.test'; import { LocalKernelSpecsCacheKey, RemoteKernelSpecsCacheKey } from '../../../kernels/kernelFinder.base'; +import { IApplicationEnvironment } from '../../../platform/common/application/types'; suite(`Remote Kernel Finder`, () => { let disposables: Disposable[] = []; @@ -120,7 +121,12 @@ suite(`Remote Kernel Finder`, () => { setup(() => { memento = mock(); - when(memento.get(ActiveKernelIdList, anything())).thenReturn([]); + when(memento.get(anything(), anything())).thenCall((key: string, defaultValue: unknown) => { + if (key === ActiveKernelIdList) { + return []; + } + return defaultValue; + }); const crypto = mock(CryptoUtils); when(crypto.createHash(anything(), anything())).thenCall((d, _c) => { return d.toLowerCase(); @@ -157,6 +163,8 @@ suite(`Remote Kernel Finder`, () => { when(connectionType.onDidChange).thenReturn(onDidChangeEvent.event); cachedRemoteKernelValidator = mock(); when(cachedRemoteKernelValidator.isValid(anything())).thenResolve(true); + const env = mock(); + when(env.extensionVersion).thenReturn(''); kernelFinder = new KernelFinder( instance(localKernelFinder), remoteKernelFinder, @@ -166,7 +174,8 @@ suite(`Remote Kernel Finder`, () => { instance(fs), instance(serverUriStorage), instance(connectionType), - instance(cachedRemoteKernelValidator) + instance(cachedRemoteKernelValidator), + instance(env) ); }); teardown(() => { @@ -316,8 +325,18 @@ suite(`Remote Kernel Finder`, () => { liveRemoteKernel ]; when(cachedRemoteKernelValidator.isValid(anything())).thenResolve(false); - when(memento.get(LocalKernelSpecsCacheKey, anything())).thenReturn([]); - when(memento.get(RemoteKernelSpecsCacheKey, anything())).thenReturn(cachedKernels); + when( + memento.get<{ kernels: KernelConnectionMetadata[]; extensionVersion: string }>( + LocalKernelSpecsCacheKey, + anything() + ) + ).thenReturn({ kernels: [], extensionVersion: '' }); + when( + memento.get<{ kernels: KernelConnectionMetadata[]; extensionVersion: string }>( + RemoteKernelSpecsCacheKey, + anything() + ) + ).thenReturn({ kernels: cachedKernels, extensionVersion: '' }); when(jupyterSessionManager.getRunningKernels()).thenResolve([]); when(jupyterSessionManager.getRunningSessions()).thenResolve([]); when(jupyterSessionManager.getKernelSpecs()).thenResolve([]); @@ -366,8 +385,18 @@ suite(`Remote Kernel Finder`, () => { ]; when(cachedRemoteKernelValidator.isValid(anything())).thenResolve(false); when(cachedRemoteKernelValidator.isValid(liveRemoteKernel)).thenResolve(true); - when(memento.get(LocalKernelSpecsCacheKey, anything())).thenReturn([]); - when(memento.get(RemoteKernelSpecsCacheKey, anything())).thenReturn(cachedKernels); + when( + memento.get<{ kernels: KernelConnectionMetadata[]; extensionVersion: string }>( + LocalKernelSpecsCacheKey, + anything() + ) + ).thenReturn({ kernels: [], extensionVersion: '' }); + when( + memento.get<{ kernels: KernelConnectionMetadata[]; extensionVersion: string }>( + RemoteKernelSpecsCacheKey, + anything() + ) + ).thenReturn({ kernels: cachedKernels, extensionVersion: '' }); when(jupyterSessionManager.getRunningKernels()).thenResolve([]); when(jupyterSessionManager.getRunningSessions()).thenResolve([]); when(jupyterSessionManager.getKernelSpecs()).thenResolve([]); From 4910e9b85c4308167349f558ea0d6534724785ad Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 19 Jul 2022 20:34:05 -0700 Subject: [PATCH 7/8] Strict IKernel with notebook document linked in core. (#10817) * Strict IKernel with notebook document linked in core. * Update src/interactive-window/generatedCodeStoreManager.ts Co-authored-by: Don Jayamanne * make notebook property readonly * update telemetry * no casting for notebookKernel types * rename IBaseKernel * update telemetry * separate kernel from jupyter and 3rd party * test IKernel. * Reduce IBaseKernel use * telemetry update * Cleanr up kernelprovider base * :lipstick: * Support get by uri * Update kernel provider work * update injectable * telemetry * fix unit test * telemetry :lipstick: * fix kernel provider event Co-authored-by: Don Jayamanne --- TELEMETRY.md | 164 +++++--------- .../commands/commandRegistry.ts | 2 +- .../interactiveWindowDebugger.node.ts | 11 +- .../editor-integration/codeLensFactory.ts | 6 +- .../editor-integration/hoverProvider.ts | 2 +- .../generatedCodeStoreManager.ts | 5 +- src/interactive-window/interactiveWindow.ts | 2 +- src/kernels/execution/kernelExecution.ts | 13 +- src/kernels/helpers.node.ts | 9 +- src/kernels/helpers.ts | 11 +- .../ipywidgets/ipyWidgetMessageDispatcher.ts | 5 +- .../ipyWidgetMessageDispatcherFactory.ts | 11 +- .../ipywidgets/ipyWidgetScriptSource.ts | 5 +- src/kernels/kernel.base.ts | 8 +- src/kernels/kernel.node.ts | 24 ++- src/kernels/kernel.web.ts | 7 +- src/kernels/kernelProvider.base.ts | 201 ++++++++++++------ src/kernels/kernelProvider.node.ts | 83 +++++++- src/kernels/kernelProvider.web.ts | 83 +++++++- src/kernels/serviceRegistry.node.ts | 5 +- src/kernels/serviceRegistry.web.ts | 5 +- src/kernels/types.ts | 44 +++- src/kernels/variables/debuggerVariables.ts | 5 +- .../variables/pythonVariableRequester.ts | 4 +- src/notebooks/controllers/kernelConnector.ts | 109 +++++++--- .../remoteKernelConnectionHandler.ts | 1 + .../controllers/vscodeNotebookController.ts | 10 +- .../debugger/debuggingManagerBase.ts | 8 +- src/notebooks/export/exportBase.web.ts | 2 +- src/notebooks/notebookCommandListener.ts | 10 +- src/standalone/api/kernelApi.ts | 38 +++- src/standalone/api/kernelConnectionWrapper.ts | 4 +- src/standalone/context/activeEditorContext.ts | 12 +- .../pythonKernelCompletionProvider.ts | 2 +- src/test/client/api.vscode.test.ts | 2 +- .../interactiveWindow.vscode.common.test.ts | 2 +- ...yWidgetScriptManager.vscode.common.test.ts | 2 +- .../notebook/interruptRestart.vscode.test.ts | 4 +- .../notebook/kernelCrashes.vscode.test.ts | 14 +- .../kernelEvents.vscode.common.test.ts | 10 +- .../notebook/kernelSelection.vscode.test.ts | 4 +- .../standardWidgets.vscode.common.test.ts | 4 +- .../thirdpartyWidgets.vscode.common.test.ts | 2 +- .../generatedCodeStorageManager.unit.test.ts | 1 + .../kernels/kernelProvider.node.unit.test.ts | 73 +++++-- .../variablesView/notebookWatcher.ts | 16 +- 46 files changed, 643 insertions(+), 402 deletions(-) diff --git a/TELEMETRY.md b/TELEMETRY.md index b05c76880aaf..572b4f21c12e 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -147,14 +147,14 @@ No properties for event ## Locations Used -[src/interactive-window/interactiveWindowCommandListener.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/interactiveWindowCommandListener.ts) +[src/interactive-window/commands/commandRegistry.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/commands/commandRegistry.ts) ```typescript } } @captureTelemetry(Telemetry.CreateNewInteractive, undefined, false) private async createNewInteractiveWindow(connection?: KernelConnectionMetadata): Promise { - await this.interactiveWindowProvider.getOrCreate(undefined, connection); + await this.interactiveWindowProvider?.getOrCreate(undefined, connection); } ``` @@ -1163,7 +1163,7 @@ No properties for event ## Locations Used -[src/interactive-window/interactiveWindowCommandListener.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/interactiveWindowCommandListener.ts) +[src/interactive-window/commands/commandRegistry.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/commands/commandRegistry.ts) ```typescript return result; } @@ -1191,7 +1191,7 @@ No properties for event ## Locations Used -[src/interactive-window/interactiveWindowCommandListener.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/interactiveWindowCommandListener.ts) +[src/interactive-window/commands/commandRegistry.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/commands/commandRegistry.ts) ```typescript } } @@ -1447,19 +1447,19 @@ No description provided ## Locations Used -[src/interactive-window/interactiveWindowCommandListener.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/interactiveWindowCommandListener.ts) +[src/interactive-window/commands/commandRegistry.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/commands/commandRegistry.ts) ```typescript return this.statusProvider.waitWithStatus(promise, message, undefined, canceled); } @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) private async importNotebook(): Promise { - const filtersKey = localize.DataScience.importDialogFilter(); + const filtersKey = DataScience.importDialogFilter(); const filtersObject: { [name: string]: string[] } = {}; ``` -[src/interactive-window/interactiveWindowCommandListener.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/interactiveWindowCommandListener.ts) +[src/interactive-window/commands/commandRegistry.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/interactive-window/commands/commandRegistry.ts) ```typescript } } @@ -3231,7 +3231,7 @@ No properties for event } sendTelemetryEvent(Telemetry.RestartKernelCommand); - const kernel = this.kernelProvider.get(document.uri); + const kernel = this.kernelProvider.get(document); if (kernel) { ``` @@ -4596,27 +4596,15 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript - sendTelemetryEvent(Telemetry.UserInstalledPandas); - } - } else { - sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); - throw new Error( - DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) - ); -``` - - -[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) -```typescript - throw new Error(DataScience.failedToInstallPandas()); - } + if (selection === Common.install()) { + await this._doInstall(executer, tokenSource); } else { sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); - throw new Error( - DataScience.pandasRequiredForViewing().format(pandasMinimumVersionSupportedByVariableViewer) - ); + throw new Error(message); + } + } ``` @@ -4690,37 +4678,37 @@ No properties for event [src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) ```typescript - cancellationPromise - ]); - if (response === InstallerResponse.Installed) { - sendTelemetryEvent(Telemetry.UserInstalledPandas); - } - } else { - sendTelemetryEvent(Telemetry.UserDidNotInstallPandas); + cancellationPromise + ]); + if (response === InstallerResponse.Installed) { + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } + } + ``` [src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) ```typescript - if (await this.promptInstall()) { - try { - await this.execute(command, kernel); - sendTelemetryEvent(Telemetry.UserInstalledPandas); - } catch (e) { - sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); - throw new Error(DataScience.failedToInstallPandas()); + + try { + await this.execute(command, kernel); + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } catch (e) { + sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); + throw new Error(DataScience.failedToInstallPandas()); ``` [src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) ```typescript - await this.execute(command, kernel); - sendTelemetryEvent(Telemetry.UserInstalledPandas); - } catch (e) { - sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); - throw new Error(DataScience.failedToInstallPandas()); - } - } else { + await this.execute(command, kernel); + sendTelemetryEvent(Telemetry.UserInstalledPandas); + } catch (e) { + sendTelemetryEvent(Telemetry.UserInstalledPandas, undefined, undefined, e); + throw new Error(DataScience.failedToInstallPandas()); + } + } ``` @@ -4892,8 +4880,8 @@ No properties for event public async checkAndInstallMissingDependencies(interpreter: PythonEnvironment): Promise { sendTelemetryEvent(Telemetry.DataViewerUsingInterpreter); - const tokenSource = new CancellationTokenSource(); - try { + await this.checkOrInstall(interpreter); + } ``` @@ -7758,27 +7746,15 @@ No description provided ``` -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) -```typescript - interpreter: PythonEnvironment, - tokenSource: CancellationTokenSource - ): Promise { - sendTelemetryEvent(Telemetry.PythonModuleInstall, undefined, { - action: 'displayed', - moduleName: ProductNames.get(Product.pandas)!, - pythonEnvType: interpreter?.envType -``` - - -[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript + } } - private async installMissingDependencies(kernel: IKernelWithSession): Promise { - sendTelemetryEvent(Telemetry.PythonModuleInstall, undefined, { - action: 'displayed', - moduleName: ProductNames.get(Product.pandas)! - }); + @captureTelemetry(Telemetry.PythonModuleInstall, { + action: 'displayed', + moduleName: ProductNames.get(Product.pandas)! + }) ``` @@ -8669,29 +8645,17 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript - throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); + const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; + await this.promptInstall(executer, tokenSource, versionStr); } - sendTelemetryEvent(Telemetry.PandasNotInstalled); - await this.installMissingDependencies(interpreter, tokenSource); + await this.promptInstall(executer, tokenSource); } finally { tokenSource.dispose(); ``` - -[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) -```typescript - throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); - } - - sendTelemetryEvent(Telemetry.PandasNotInstalled); - - await this.installMissingDependencies(kernel); - } -``` -
DS_INTERNAL.SHOW_DATA_PANDAS_INSTALL_CANCELED @@ -8707,9 +8671,9 @@ No description provided ## Locations Used -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript - const pandasVersion = await this.getVersion(interpreter, tokenSource.token); + const pandasVersion = await this.getVersion(executer, tokenSource.token); if (Cancellation.isCanceled(tokenSource.token)) { sendTelemetryEvent(Telemetry.PandasInstallCanceled); @@ -8735,7 +8699,7 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript if (pandasVersion) { @@ -8746,18 +8710,6 @@ No properties for event sendTelemetryEvent(Telemetry.PandasTooOld); ``` - -[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) -```typescript - - if (pandasVersion) { - if (pandasVersion.compare(pandasMinimumVersionSupportedByVariableViewer) > 0) { - sendTelemetryEvent(Telemetry.PandasOK); - return; - } - sendTelemetryEvent(Telemetry.PandasTooOld); -``` -
DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD @@ -8775,7 +8727,7 @@ No properties for event ## Locations Used -[src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/interpreterDataViewerDependencyImplementation.node.ts) +[src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/baseDataViewerDependencyImplementation.ts) ```typescript sendTelemetryEvent(Telemetry.PandasOK); return; @@ -8783,19 +8735,7 @@ No properties for event sendTelemetryEvent(Telemetry.PandasTooOld); // Warn user that we cannot start because pandas is too old. const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; - throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); -``` - - -[src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/webviews/extension-side/dataviewer/kernelDataViewerDependencyImplementation.ts) -```typescript - sendTelemetryEvent(Telemetry.PandasOK); - return; - } - sendTelemetryEvent(Telemetry.PandasTooOld); - // Warn user that we cannot start because pandas is too old. - const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; - throw new Error(DataScience.pandasTooOldForViewingFormat().format(versionStr)); + await this.promptInstall(executer, tokenSource, versionStr); ```
diff --git a/src/interactive-window/commands/commandRegistry.ts b/src/interactive-window/commands/commandRegistry.ts index 3bc241160bde..21e951125687 100644 --- a/src/interactive-window/commands/commandRegistry.ts +++ b/src/interactive-window/commands/commandRegistry.ts @@ -428,7 +428,7 @@ export class CommandRegistry implements IDisposable, IExtensionSingleActivationS // Attempt to get the interactive window for this file const iw = this.interactiveWindowProvider.windows.find((w) => w.owner?.toString() == uri.toString()); if (iw && iw.notebookDocument) { - const kernel = this.kernelProvider.get(iw.notebookDocument.uri); + const kernel = this.kernelProvider.get(iw.notebookDocument); if (kernel) { // If we have a matching iw, then stop current execution await kernel.interrupt(); diff --git a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts index 234365e992d7..d8eb6f0301ff 100644 --- a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts +++ b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts @@ -17,7 +17,7 @@ import { IKernel, isLocalConnection } from '../../kernels/types'; import { IInteractiveWindowDebugger } from '../types'; import { IFileGeneratedCodes } from '../editor-integration/types'; import { IJupyterDebugService } from '../../kernels/debugger/types'; -import { executeSilently, getAssociatedNotebookDocument } from '../../kernels/helpers'; +import { executeSilently } from '../../kernels/helpers'; import { buildSourceMap } from './helper'; /** @@ -68,10 +68,10 @@ export class InteractiveWindowDebugger implements IInteractiveWindowDebugger { } public async detach(kernel: IKernel): Promise { - const notebook = getAssociatedNotebookDocument(kernel); - if (!kernel.session || !notebook) { + if (!kernel.session) { return; } + const notebook = kernel.notebook; const config = this.configs.get(notebook); if (config) { traceInfo('stop debugging'); @@ -170,10 +170,7 @@ export class InteractiveWindowDebugger implements IInteractiveWindowDebugger { kernel: IKernel, extraConfig: Partial ): Promise { - const notebook = getAssociatedNotebookDocument(kernel); - if (!kernel || !notebook) { - return; - } + const notebook = kernel.notebook; // If we already have configuration, we're already attached, don't do it again. const key = notebook; let result = this.configs.get(key); diff --git a/src/interactive-window/editor-integration/codeLensFactory.ts b/src/interactive-window/editor-integration/codeLensFactory.ts index a72685bce088..d8ec191c3493 100644 --- a/src/interactive-window/editor-integration/codeLensFactory.ts +++ b/src/interactive-window/editor-integration/codeLensFactory.ts @@ -24,7 +24,6 @@ import { IKernelProvider } from '../../kernels/types'; import { CodeLensCommands, Commands, InteractiveWindowView } from '../../platform/common/constants'; import { generateCellRangesFromDocument } from './cellFactory'; import { ICodeLensFactory, IGeneratedCode, IGeneratedCodeStorageFactory } from './types'; -import { getAssociatedNotebookDocument } from '../../kernels/helpers'; type CodeLensCacheData = { cachedDocumentVersion: number | undefined; @@ -64,10 +63,7 @@ export class CodeLensFactory implements ICodeLensFactory { notebook.onDidChangeNotebookCellExecutionState(this.onDidChangeNotebookCellExecutionState, this, disposables); kernelProvider.onDidDisposeKernel( (kernel) => { - const notebook = getAssociatedNotebookDocument(kernel); - if (notebook) { - this.notebookData.delete(notebook.uri.toString()); - } + this.notebookData.delete(kernel.notebook.uri.toString()); }, this, disposables diff --git a/src/interactive-window/editor-integration/hoverProvider.ts b/src/interactive-window/editor-integration/hoverProvider.ts index 24d898b8666b..26e2d3c5223f 100644 --- a/src/interactive-window/editor-integration/hoverProvider.ts +++ b/src/interactive-window/editor-integration/hoverProvider.ts @@ -139,7 +139,7 @@ export class HoverProvider implements IExtensionSyncActivationService, vscode.Ho this.notebook.notebookDocuments .filter((item) => notebookUris.includes(item.uri.toString())) .forEach((item) => { - const kernel = this.kernelProvider.get(item.uri); + const kernel = this.kernelProvider.get(item); if (kernel) { kernels.add(kernel); } diff --git a/src/interactive-window/generatedCodeStoreManager.ts b/src/interactive-window/generatedCodeStoreManager.ts index f67fbb07c725..bb64c89ed0bb 100644 --- a/src/interactive-window/generatedCodeStoreManager.ts +++ b/src/interactive-window/generatedCodeStoreManager.ts @@ -3,7 +3,6 @@ import { inject, injectable } from 'inversify'; import { NotebookDocument } from 'vscode'; -import { getAssociatedNotebookDocument } from '../kernels/helpers'; import { IKernel, IKernelProvider } from '../kernels/types'; import { IControllerSelection } from '../notebooks/controllers/types'; import { IExtensionSyncActivationService } from '../platform/activation/types'; @@ -39,8 +38,8 @@ export class GeneratedCodeStorageManager implements IExtensionSyncActivationServ this.codeGeneratorFactory.get(notebook)?.reset(); } private onDidCreateKernel(kernel: IKernel) { - const notebook = getAssociatedNotebookDocument(kernel); - if (kernel.creator !== 'jupyterExtension' || notebook?.notebookType !== InteractiveWindowView) { + const notebook = kernel.notebook; + if (kernel.creator !== 'jupyterExtension' || notebook.notebookType !== InteractiveWindowView) { return; } // Possible we changed kernels for the same document. diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index ff0bd34e7e3b..dc09b6efdc04 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -256,7 +256,7 @@ export class InteractiveWindow implements IInteractiveWindowLoadable { }; // When connecting, we need to update the sys info message this.updateSysInfoMessage(this.getSysInfoMessage(metadata, SysInfoReason.Start), false, sysInfoCell); - const kernel = await KernelConnector.connectToKernel( + const kernel = await KernelConnector.connectToNotebookKernel( controller, metadata, this.serviceContainer, diff --git a/src/kernels/execution/kernelExecution.ts b/src/kernels/execution/kernelExecution.ts index 6e637a8af97b..68c4f1923bd0 100644 --- a/src/kernels/execution/kernelExecution.ts +++ b/src/kernels/execution/kernelExecution.ts @@ -18,7 +18,7 @@ import { captureTelemetry, Telemetry } from '../../telemetry'; import { CellOutputDisplayIdTracker } from './cellDisplayIdTracker'; import { IKernelConnectionSession, - IKernel, + IBaseKernel, InterruptResult, ITracebackFormatter, NotebookCellRunState @@ -26,7 +26,6 @@ import { import { traceCellMessage } from './helpers'; import { getDisplayPath } from '../../platform/common/platform/fs-paths'; import { CellExecutionMessageHandlerService } from './cellExecutionMessageHandlerService'; -import { getAssociatedNotebookDocument } from '../helpers'; import { noop } from '../../platform/common/utils/misc'; /** @@ -41,7 +40,7 @@ export class KernelExecution implements IDisposable { private _restartPromise?: Promise; private readonly _onPreExecute = new EventEmitter(); constructor( - private readonly kernel: IKernel, + private readonly kernel: IBaseKernel, appShell: IApplicationShell, private readonly interruptTimeout: number, outputTracker: CellOutputDisplayIdTracker, @@ -63,7 +62,7 @@ export class KernelExecution implements IDisposable { return this._onPreExecute.event; } public get queue() { - const notebook = getAssociatedNotebookDocument(this.kernel); + const notebook = this.kernel.notebook; return notebook ? this.documentExecutions.get(notebook)?.queue || [] : []; } public async executeCell( @@ -89,7 +88,7 @@ export class KernelExecution implements IDisposable { return result[0]; } public async cancel() { - const notebook = getAssociatedNotebookDocument(this.kernel); + const notebook = this.kernel.notebook; if (!notebook) { return; } @@ -105,7 +104,7 @@ export class KernelExecution implements IDisposable { */ public async interrupt(sessionPromise?: Promise): Promise { trackKernelResourceInformation(this.kernel.resourceUri, { interruptKernel: true }); - const notebook = getAssociatedNotebookDocument(this.kernel); + const notebook = this.kernel.notebook; const executionQueue = notebook ? this.documentExecutions.get(notebook) : undefined; if (notebook && !executionQueue && this.kernel.kernelConnectionMetadata.kind !== 'connectToLiveRemoteKernel') { return InterruptResult.Success; @@ -144,7 +143,7 @@ export class KernelExecution implements IDisposable { */ public async restart(sessionPromise?: Promise): Promise { trackKernelResourceInformation(this.kernel.resourceUri, { restartKernel: true }); - const notebook = getAssociatedNotebookDocument(this.kernel); + const notebook = this.kernel.notebook; const executionQueue = notebook ? this.documentExecutions.get(notebook) : undefined; // Possible we don't have a notebook. const session = sessionPromise ? await sessionPromise.catch(() => undefined) : undefined; diff --git a/src/kernels/helpers.node.ts b/src/kernels/helpers.node.ts index ffde7980702b..b533dede6952 100644 --- a/src/kernels/helpers.node.ts +++ b/src/kernels/helpers.node.ts @@ -4,7 +4,7 @@ 'use strict'; import * as nbformat from '@jupyterlab/nbformat'; -import { IKernel, KernelConnectionMetadata } from './types'; +import { IKernelConnectionSession, KernelConnectionMetadata } from './types'; import { Uri } from 'vscode'; import { traceError, traceVerbose } from '../platform/logging'; import { getDisplayPath } from '../platform/common/platform/fs-paths'; @@ -17,14 +17,11 @@ import { sendTelemetryEvent, Telemetry } from '../telemetry'; import { executeSilently, isPythonKernelConnection } from './helpers'; export async function sendTelemetryForPythonKernelExecutable( - kernel: IKernel, + session: IKernelConnectionSession, resource: Resource, kernelConnection: KernelConnectionMetadata, executionService: IPythonExecutionFactory ) { - if (!kernel.session) { - return; - } if (!kernelConnection.interpreter || !isPythonKernelConnection(kernelConnection)) { return; } @@ -36,7 +33,7 @@ export async function sendTelemetryForPythonKernelExecutable( } try { traceVerbose('Begin sendTelemetryForPythonKernelExecutable'); - const outputs = await executeSilently(kernel.session, 'import sys\nprint(sys.executable)'); + const outputs = await executeSilently(session, 'import sys\nprint(sys.executable)'); if (outputs.length === 0) { return; } diff --git a/src/kernels/helpers.ts b/src/kernels/helpers.ts index 9659b72c4dea..42e323ec8b20 100644 --- a/src/kernels/helpers.ts +++ b/src/kernels/helpers.ts @@ -16,10 +16,9 @@ import { LiveRemoteKernelConnectionMetadata, PythonKernelConnectionMetadata, IJupyterKernelSpec, - IKernel, IKernelConnectionSession } from './types'; -import { Uri, workspace } from 'vscode'; +import { Uri } from 'vscode'; import { IWorkspaceService } from '../platform/common/application/types'; import { isCI, PYTHON_LANGUAGE, Telemetry } from '../platform/common/constants'; import { traceError, traceInfo, traceInfoIfCI, traceWarning } from '../platform/logging'; @@ -1589,11 +1588,3 @@ export function deserializeKernelConnection(kernelConnection: any): KernelConnec } return kernelConnection; } - -export function getAssociatedNotebookDocument(kernel: IKernel | undefined) { - if (!kernel) { - return; - } - - return workspace.notebookDocuments.find((nb) => nb.uri.toString() === kernel.uri.toString()); -} diff --git a/src/kernels/ipywidgets/ipyWidgetMessageDispatcher.ts b/src/kernels/ipywidgets/ipyWidgetMessageDispatcher.ts index 26b45e09b4ba..53de6a1f8cc4 100644 --- a/src/kernels/ipywidgets/ipyWidgetMessageDispatcher.ts +++ b/src/kernels/ipywidgets/ipyWidgetMessageDispatcher.ts @@ -18,7 +18,6 @@ import { sendTelemetryEvent, Telemetry } from '../../telemetry'; import { IKernel, IKernelProvider, KernelSocketInformation } from '../types'; import { WIDGET_MIMETYPE } from './constants'; import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from './types'; -import { getAssociatedNotebookDocument } from '../helpers'; type PendingMessage = { resultPromise: Deferred; @@ -79,7 +78,7 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher { this.pendingTargetNames.add('jupyter.widget'); kernelProvider.onDidStartKernel( (e) => { - if (getAssociatedNotebookDocument(e) === document) { + if (e.notebook === document) { this.initialize(); } }, @@ -386,7 +385,7 @@ export class IPyWidgetMessageDispatcher implements IIPyWidgetMessageDispatcher { private getKernel(): IKernel | undefined { if (this.document && !this.kernel?.session) { - this.kernel = this.kernelProvider.get(this.document.uri); + this.kernel = this.kernelProvider.get(this.document); this.kernel?.onDisposed(() => (this.kernel = undefined)); } if (this.kernel && !this.kernelRestartHandlerAttached) { diff --git a/src/kernels/ipywidgets/ipyWidgetMessageDispatcherFactory.ts b/src/kernels/ipywidgets/ipyWidgetMessageDispatcherFactory.ts index b59819d213b8..cf84b57556ed 100644 --- a/src/kernels/ipywidgets/ipyWidgetMessageDispatcherFactory.ts +++ b/src/kernels/ipywidgets/ipyWidgetMessageDispatcherFactory.ts @@ -7,7 +7,6 @@ import { inject, injectable } from 'inversify'; import { Event, EventEmitter, NotebookDocument } from 'vscode'; import { IDisposable, IDisposableRegistry } from '../../platform/common/types'; import { IPyWidgetMessages } from '../../messageTypes'; -import { getAssociatedNotebookDocument } from '../helpers'; import { IKernel, IKernelProvider } from '../types'; import { IPyWidgetMessageDispatcher } from './ipyWidgetMessageDispatcher'; import { IIPyWidgetMessageDispatcher, IPyWidgetMessage } from './types'; @@ -122,12 +121,10 @@ export class IPyWidgetMessageDispatcherFactory implements IDisposable { if (this.disposed) { return; } - const notebook = getAssociatedNotebookDocument(kernel); - if (notebook) { - const item = this.messageDispatchers.get(notebook); - this.messageDispatchers.delete(notebook); - item?.dispose(); // NOSONAR - } + const notebook = kernel.notebook; + const item = this.messageDispatchers.get(notebook); + this.messageDispatchers.delete(notebook); + item?.dispose(); // NOSONAR } private onMessage(message: IPyWidgetMessage, document?: NotebookDocument) { diff --git a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts index b4b5d20127e2..a81e08318454 100644 --- a/src/kernels/ipywidgets/ipyWidgetScriptSource.ts +++ b/src/kernels/ipywidgets/ipyWidgetScriptSource.ts @@ -12,7 +12,6 @@ import { IKernel, IKernelProvider } from '../types'; import { IPyWidgetScriptSourceProvider } from './ipyWidgetScriptSourceProvider'; import { ILocalResourceUriConverter, IWidgetScriptSourceProviderFactory, WidgetScriptSource } from './types'; import { ConsoleForegroundColors } from '../../platform/logging/types'; -import { getAssociatedNotebookDocument } from '../helpers'; import { noop } from '../../platform/common/utils/misc'; import { createDeferred, Deferred } from '../../platform/common/utils/async'; import { ScriptUriConverter } from './scriptUriConverter'; @@ -70,7 +69,7 @@ export class IPyWidgetScriptSource { disposables.push(this); this.kernelProvider.onDidStartKernel( (e) => { - if (getAssociatedNotebookDocument(e) === this.document) { + if (e.notebook === this.document) { this.initialize(); } }, @@ -113,7 +112,7 @@ export class IPyWidgetScriptSource { } if (!this.kernel) { - this.kernel = this.kernelProvider.get(this.document.uri); + this.kernel = this.kernelProvider.get(this.document); } if (!this.kernel?.session) { return; diff --git a/src/kernels/kernel.base.ts b/src/kernels/kernel.base.ts index 9493d7300d71..224310913259 100644 --- a/src/kernels/kernel.base.ts +++ b/src/kernels/kernel.base.ts @@ -14,7 +14,8 @@ import { NotebookController, ColorThemeKind, Disposable, - Uri + Uri, + NotebookDocument } from 'vscode'; import { CodeSnippets, Identifiers } from '../platform/common/constants'; import { IApplicationShell, IWorkspaceService } from '../platform/common/application/types'; @@ -43,7 +44,7 @@ import { import { sendTelemetryEvent, Telemetry } from '../telemetry'; import { executeSilently, getDisplayNameOrNameOfKernelConnection, isPythonKernelConnection } from './helpers'; import { - IKernel, + IBaseKernel, IKernelConnectionSession, INotebookProvider, InterruptResult, @@ -66,7 +67,7 @@ import { KernelExecution } from './execution/kernelExecution'; /** * Represents an active kernel process running on the jupyter (or local) machine. */ -export abstract class BaseKernel implements IKernel { +export abstract class BaseKernel implements IBaseKernel { private readonly disposables: IDisposable[] = []; get onStatusChanged(): Event { return this._onStatusChanged.event; @@ -142,6 +143,7 @@ export abstract class BaseKernel implements IKernel { constructor( public readonly uri: Uri, public readonly resourceUri: Resource, + public readonly notebook: NotebookDocument | undefined, public readonly kernelConnectionMetadata: Readonly, private readonly notebookProvider: INotebookProvider, private readonly launchTimeout: number, diff --git a/src/kernels/kernel.node.ts b/src/kernels/kernel.node.ts index 9c17ea446660..60f4db377ea6 100644 --- a/src/kernels/kernel.node.ts +++ b/src/kernels/kernel.node.ts @@ -2,14 +2,14 @@ // Licensed under the MIT License. 'use strict'; -import { NotebookController, Uri } from 'vscode'; +import { NotebookController, NotebookDocument, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../platform/common/application/types'; import { CodeSnippets, InteractiveWindowView } from '../platform/common/constants'; import { traceInfo, traceError } from '../platform/logging'; import { IPythonExecutionFactory } from '../platform/common/process/types.node'; import { Resource, IConfigurationService, IExtensionContext } from '../platform/common/types'; import { calculateWorkingDirectory } from '../platform/common/utils.node'; -import { getAssociatedNotebookDocument, isLocalHostConnection, isPythonKernelConnection } from './helpers'; +import { isLocalHostConnection, isPythonKernelConnection } from './helpers'; import { expandWorkingDir } from './jupyter/jupyterUtils'; import { INotebookProvider, @@ -33,6 +33,7 @@ export class Kernel extends BaseKernel { constructor( uri: Uri, resourceUri: Resource, + notebook: NotebookDocument | undefined, kernelConnectionMetadata: Readonly, notebookProvider: INotebookProvider, launchTimeout: number, @@ -52,6 +53,7 @@ export class Kernel extends BaseKernel { super( uri, resourceUri, + notebook, kernelConnectionMetadata, notebookProvider, launchTimeout, @@ -78,7 +80,7 @@ export class Kernel extends BaseKernel { } // Only do this for interactive windows. IPYKERNEL_CELL_NAME is set other ways in // notebooks - if (getAssociatedNotebookDocument(this)?.notebookType === InteractiveWindowView) { + if (this.notebook?.notebookType === InteractiveWindowView) { // If using ipykernel 6, we need to set the IPYKERNEL_CELL_NAME so that // debugging can work. However this code is harmless for IPYKERNEL 5 so just always do it const scriptPath = AddRunCellHook.getScriptPath(this.context); @@ -138,12 +140,14 @@ export class Kernel extends BaseKernel { return []; } - protected override sendTelemetryForPythonKernelExecutable() { - return sendTelemetryForPythonKernelExecutable( - this, - this.resourceUri, - this.kernelConnectionMetadata, - this.pythonExecutionFactory - ); + protected override async sendTelemetryForPythonKernelExecutable() { + if (this.session) { + return sendTelemetryForPythonKernelExecutable( + this.session, + this.resourceUri, + this.kernelConnectionMetadata, + this.pythonExecutionFactory + ); + } } } diff --git a/src/kernels/kernel.web.ts b/src/kernels/kernel.web.ts index 1c04743fe3b1..0b14d1a309c1 100644 --- a/src/kernels/kernel.web.ts +++ b/src/kernels/kernel.web.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { NotebookController, Uri } from 'vscode'; +import { NotebookController, NotebookDocument, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService } from '../platform/common/application/types'; import { Resource, IConfigurationService, IExtensionContext } from '../platform/common/types'; import { INotebookProvider, ITracebackFormatter, KernelActionSource, KernelConnectionMetadata } from './types'; @@ -10,7 +10,6 @@ import { BaseKernel } from './kernel.base'; import { IStatusProvider } from '../platform/progress/types'; import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; -import { getAssociatedNotebookDocument } from './helpers'; import { IFileSystem } from '../platform/common/platform/types'; /** @@ -22,6 +21,7 @@ export class Kernel extends BaseKernel { constructor( id: Uri, resourceUri: Resource, + notebook: NotebookDocument | undefined, kernelConnectionMetadata: Readonly, notebookProvider: INotebookProvider, launchTimeout: number, @@ -40,6 +40,7 @@ export class Kernel extends BaseKernel { super( id, resourceUri, + notebook, kernelConnectionMetadata, notebookProvider, launchTimeout, @@ -57,7 +58,7 @@ export class Kernel extends BaseKernel { } protected async getDebugCellHook(): Promise { - if (getAssociatedNotebookDocument(this)?.notebookType === InteractiveWindowView) { + if (this.notebook?.notebookType === InteractiveWindowView) { // If using ipykernel 6, we need to set the IPYKERNEL_CELL_NAME so that // debugging can work. However this code is harmless for IPYKERNEL 5 so just always do it if (!this.addRunCellHookContents) { diff --git a/src/kernels/kernelProvider.base.ts b/src/kernels/kernelProvider.base.ts index 33e0e267885d..366bdd2db7a5 100644 --- a/src/kernels/kernelProvider.base.ts +++ b/src/kernels/kernelProvider.base.ts @@ -3,28 +3,23 @@ 'use strict'; import type { KernelMessage } from '@jupyterlab/services'; -import { Event, EventEmitter, NotebookDocument, Uri, workspace } from 'vscode'; +import { Event, EventEmitter, NotebookDocument, Uri } from 'vscode'; import { IVSCodeNotebook } from '../platform/common/application/types'; import { traceInfoIfCI, traceVerbose, traceWarning } from '../platform/logging'; import { getDisplayPath } from '../platform/common/platform/fs-paths'; import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposableRegistry } from '../platform/common/types'; -import { noop } from '../platform/common/utils/misc'; -import { IKernel, IKernelProvider, KernelOptions } from './types'; +import { isUri, noop } from '../platform/common/utils/misc'; +import { IBaseKernel, IKernelProvider, IKernel, KernelOptions, IThirdPartyKernelProvider } from './types'; /** * Provides kernels to the system. Generally backed by a URI or a notebook object. */ -export abstract class BaseKernelProvider implements IKernelProvider { +export abstract class BaseCoreKernelProvider implements IKernelProvider { /** * Use a separate dictionary to track kernels by Notebook, so that * the ref to kernel is lost when the notebook is closed. */ private readonly kernelsByNotebook = new WeakMap(); - /** - * The life time of kernels not tied to a notebook will be managed by callers of the API. - * Where as if a kernel is tied to a notebook, then the kernel dies along with notebooks. - */ - private readonly kernelsByUri = new Map(); private readonly pendingDisposables = new Set(); protected readonly _onDidRestartKernel = new EventEmitter(); protected readonly _onDidStartKernel = new EventEmitter(); @@ -35,12 +30,11 @@ export abstract class BaseKernelProvider implements IKernelProvider { public get kernels() { const kernels = new Set(); this.notebook.notebookDocuments.forEach((item) => { - const kernel = this.get(item.uri); + const kernel = this.get(item); if (kernel) { kernels.add(kernel); } }); - Array.from(this.kernelsByUri.values()).forEach((item) => kernels.add(item.kernel)); return Array.from(kernels); } constructor( @@ -49,7 +43,7 @@ export abstract class BaseKernelProvider implements IKernelProvider { private readonly notebook: IVSCodeNotebook ) { this.asyncDisposables.push(this); - this.notebook.onDidCloseNotebookDocument((e) => this.disposeOldKernel(e.uri), this, disposables); + this.notebook.onDidCloseNotebookDocument((e) => this.disposeOldKernel(e), this, disposables); disposables.push(this._onDidDisposeKernel); disposables.push(this._onDidRestartKernel); disposables.push(this._onKernelStatusChanged); @@ -60,7 +54,6 @@ export abstract class BaseKernelProvider implements IKernelProvider { public get onDidDisposeKernel(): Event { return this._onDidDisposeKernel.event; } - public get onDidRestartKernel(): Event { return this._onDidRestartKernel.event; } @@ -71,10 +64,26 @@ export abstract class BaseKernelProvider implements IKernelProvider { public get onDidCreateKernel(): Event { return this._onDidCreateKernel.event; } + public get(uriOrNotebook: Uri | NotebookDocument): IKernel | undefined { + if (isUri(uriOrNotebook)) { + const notebook = this.notebook.notebookDocuments.find( + (item) => item.uri.toString() === uriOrNotebook.toString() + ); + return notebook ? this.get(notebook) : undefined; + } else { + return this.kernelsByNotebook.get(uriOrNotebook)?.kernel; + } + } - public get(uri: Uri): IKernel | undefined { - return this.getInternal(uri)?.kernel; + public getInternal(notebook: NotebookDocument): + | { + options: KernelOptions; + kernel: IKernel; + } + | undefined { + return this.kernelsByNotebook.get(notebook); } + public async dispose() { traceInfoIfCI(`Disposing all kernels from kernel provider`); const items = Array.from(this.pendingDisposables.values()); @@ -82,39 +91,126 @@ export abstract class BaseKernelProvider implements IKernelProvider { await Promise.all(items); await Promise.all(this.kernels.map((k) => k.dispose())); } - public abstract getOrCreate(uri: Uri, options: KernelOptions): IKernel; + public abstract getOrCreate(notebook: NotebookDocument, options: KernelOptions): IKernel; + protected storeKernel(notebook: NotebookDocument, options: KernelOptions, kernel: IKernel) { + this.kernelsByNotebook.set(notebook, { options, kernel }); + this._onDidCreateKernel.fire(kernel); + } + /** + * If a kernel has been disposed, then remove the mapping of Uri + Kernel. + */ + protected deleteMappingIfKernelIsDisposed(uri: Uri, kernel: IKernel) { + kernel.onDisposed( + () => { + // If the same kernel is associated with this document & it was disposed, then delete it. + if (this.get(kernel.notebook) === kernel) { + this.kernelsByNotebook.delete(kernel.notebook); + traceVerbose( + `Kernel got disposed, hence there is no longer a kernel associated with ${getDisplayPath(uri)}` + ); + } + this.pendingDisposables.delete(kernel); + }, + this, + this.disposables + ); + } + protected disposeOldKernel(notebook: NotebookDocument) { + const kernelToDispose = this.kernelsByNotebook.get(notebook); + if (kernelToDispose) { + traceInfoIfCI( + `Disposing kernel associated with ${getDisplayPath(notebook.uri)}, isClosed=${notebook.isClosed}` + ); + this.pendingDisposables.add(kernelToDispose.kernel); + kernelToDispose.kernel + .dispose() + .catch((ex) => traceWarning('Failed to dispose old kernel', ex)) + .finally(() => this.pendingDisposables.delete(kernelToDispose.kernel)) + .catch(noop); + } + this.kernelsByNotebook.delete(notebook); + } +} + +export abstract class BaseThirdPartyKernelProvider implements IThirdPartyKernelProvider { + /** + * The life time of kernels not tied to a notebook will be managed by callers of the API. + * Where as if a kernel is tied to a notebook, then the kernel dies along with notebooks. + */ + private readonly kernelsByUri = new Map(); + private readonly pendingDisposables = new Set(); + protected readonly _onDidRestartKernel = new EventEmitter(); + protected readonly _onDidStartKernel = new EventEmitter(); + protected readonly _onDidCreateKernel = new EventEmitter(); + protected readonly _onDidDisposeKernel = new EventEmitter(); + protected readonly _onKernelStatusChanged = new EventEmitter<{ + status: KernelMessage.Status; + kernel: IBaseKernel; + }>(); + public readonly onKernelStatusChanged = this._onKernelStatusChanged.event; + public get kernels() { + return Array.from(this.kernelsByUri.values()).map((item) => item.kernel); + } + constructor( + protected asyncDisposables: IAsyncDisposableRegistry, + protected disposables: IDisposableRegistry, + private readonly notebook: IVSCodeNotebook + ) { + this.asyncDisposables.push(this); + this.notebook.onDidCloseNotebookDocument((e) => this.disposeOldKernel(e.uri), this, disposables); + disposables.push(this._onDidDisposeKernel); + disposables.push(this._onDidRestartKernel); + disposables.push(this._onKernelStatusChanged); + disposables.push(this._onDidStartKernel); + disposables.push(this._onDidCreateKernel); + } + + public get onDidDisposeKernel(): Event { + return this._onDidDisposeKernel.event; + } + public get onDidRestartKernel(): Event { + return this._onDidRestartKernel.event; + } + public get onDidStartKernel(): Event { + return this._onDidStartKernel.event; + } + public get onDidCreateKernel(): Event { + return this._onDidCreateKernel.event; + } + public get(uri: Uri): IBaseKernel | undefined { + return this.kernelsByUri.get(uri.toString())?.kernel; + } + public getInternal(uri: Uri): | { options: KernelOptions; - kernel: IKernel; + kernel: IBaseKernel; } | undefined { - const notebook = workspace.notebookDocuments.find((nb) => nb.uri.toString() === uri.toString()); - if (!notebook) { - return this.kernelsByUri.get(uri.toString()); - } - return notebook ? this.kernelsByNotebook.get(notebook) : undefined; + return this.kernelsByUri.get(uri.toString()); } - protected storeKernel(uri: Uri, notebook: NotebookDocument | undefined, options: KernelOptions, kernel: IKernel) { - if (notebook) { - this.kernelsByNotebook.set(notebook, { options, kernel }); - } else { - this.kernelsByUri.set(uri.toString(), { options, kernel }); - } + + public async dispose() { + traceInfoIfCI(`Disposing all kernels from kernel provider`); + const items = Array.from(this.pendingDisposables.values()); + this.pendingDisposables.clear(); + await Promise.all(items); + await Promise.all(this.kernels.map((k) => k.dispose())); + } + public abstract getOrCreate(uri: Uri, options: KernelOptions): IBaseKernel; + protected storeKernel(uri: Uri, options: KernelOptions, kernel: IBaseKernel) { + this.kernelsByUri.set(uri.toString(), { options, kernel }); this._onDidCreateKernel.fire(kernel); } + /** * If a kernel has been disposed, then remove the mapping of Uri + Kernel. */ - protected deleteMappingIfKernelIsDisposed(uri: Uri, kernel: IKernel) { + protected deleteMappingIfKernelIsDisposed(uri: Uri, kernel: IBaseKernel) { kernel.onDisposed( () => { // If the same kernel is associated with this document & it was disposed, then delete it. - if (this.getInternal(uri)?.kernel === kernel) { - const notebook = workspace.notebookDocuments.find((nb) => nb.uri.toString() === uri.toString()); - if (notebook) { - this.kernelsByNotebook.delete(notebook); - } + if (this.get(uri) === kernel) { this.kernelsByUri.delete(uri.toString()); traceVerbose( `Kernel got disposed, hence there is no longer a kernel associated with ${getDisplayPath(uri)}` @@ -127,33 +223,16 @@ export abstract class BaseKernelProvider implements IKernelProvider { ); } protected disposeOldKernel(uri: Uri) { - const notebook = workspace.notebookDocuments.find((nb) => nb.uri.toString() === uri.toString()); - if (notebook) { - const kernelToDispose = this.kernelsByNotebook.get(notebook); - if (kernelToDispose) { - traceInfoIfCI( - `Disposing kernel associated with ${getDisplayPath(notebook.uri)}, isClosed=${notebook.isClosed}` - ); - this.pendingDisposables.add(kernelToDispose.kernel); - kernelToDispose.kernel - .dispose() - .catch((ex) => traceWarning('Failed to dispose old kernel', ex)) - .finally(() => this.pendingDisposables.delete(kernelToDispose.kernel)) - .catch(noop); - } - this.kernelsByNotebook.delete(notebook); - } else { - const kernelToDispose = this.kernelsByUri.get(uri.toString()); - if (kernelToDispose) { - traceInfoIfCI(`Disposing kernel associated with ${getDisplayPath(uri)}`); - this.pendingDisposables.add(kernelToDispose.kernel); - kernelToDispose.kernel - .dispose() - .catch((ex) => traceWarning('Failed to dispose old kernel', ex)) - .finally(() => this.pendingDisposables.delete(kernelToDispose.kernel)) - .catch(noop); - } - this.kernelsByUri.delete(uri.toString()); + const kernelToDispose = this.kernelsByUri.get(uri.toString()); + if (kernelToDispose) { + traceInfoIfCI(`Disposing kernel associated with ${getDisplayPath(uri)}`); + this.pendingDisposables.add(kernelToDispose.kernel); + kernelToDispose.kernel + .dispose() + .catch((ex) => traceWarning('Failed to dispose old kernel', ex)) + .finally(() => this.pendingDisposables.delete(kernelToDispose.kernel)) + .catch(noop); } + this.kernelsByUri.delete(uri.toString()); } } diff --git a/src/kernels/kernelProvider.node.ts b/src/kernels/kernelProvider.node.ts index 2ab56531d9b2..885e76a254e0 100644 --- a/src/kernels/kernelProvider.node.ts +++ b/src/kernels/kernelProvider.node.ts @@ -3,7 +3,7 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { Uri, workspace } from 'vscode'; +import { NotebookDocument, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService, IVSCodeNotebook } from '../platform/common/application/types'; import { IPythonExecutionFactory } from '../platform/common/process/types.node'; import { @@ -13,9 +13,9 @@ import { IExtensionContext } from '../platform/common/types'; import { Kernel } from './kernel.node'; -import { IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; +import { IBaseKernel, IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; import { IStatusProvider } from '../platform/progress/types'; -import { BaseKernelProvider } from './kernelProvider.base'; +import { BaseCoreKernelProvider, BaseThirdPartyKernelProvider } from './kernelProvider.base'; import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { IFileSystem } from '../platform/common/platform/types'; @@ -24,7 +24,7 @@ import { IFileSystem } from '../platform/common/platform/types'; * Node version of a kernel provider. Needed in order to create the node version of a kernel. */ @injectable() -export class KernelProvider extends BaseKernelProvider { +export class KernelProvider extends BaseCoreKernelProvider { constructor( @inject(IAsyncDisposableRegistry) asyncDisposables: IAsyncDisposableRegistry, @inject(IDisposableRegistry) disposables: IDisposableRegistry, @@ -43,20 +43,87 @@ export class KernelProvider extends BaseKernelProvider { super(asyncDisposables, disposables, notebook); } - public getOrCreate(uri: Uri, options: KernelOptions): IKernel { + public getOrCreate(notebook: NotebookDocument, options: KernelOptions): IKernel { + const existingKernelInfo = this.getInternal(notebook); + if (existingKernelInfo && existingKernelInfo.options.metadata.id === options.metadata.id) { + return existingKernelInfo.kernel; + } + this.disposeOldKernel(notebook); + + const uri = notebook.uri; + const resourceUri = notebook.notebookType === InteractiveWindowView ? options.resourceUri : uri; + const waitForIdleTimeout = this.configService.getSettings(resourceUri).jupyterLaunchTimeout; + const interruptTimeout = this.configService.getSettings(resourceUri).jupyterInterruptTimeout; + const kernel = new Kernel( + uri, + resourceUri, + notebook, + options.metadata, + this.notebookProvider, + waitForIdleTimeout, + interruptTimeout, + this.appShell, + this.fs, + options.controller, + this.configService, + this.outputTracker, + this.workspaceService, + this.pythonExecutionFactory, + this.statusProvider, + options.creator, + this.context, + this.formatters + ) as IKernel; + kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); + kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); + kernel.onStarted(() => this._onDidStartKernel.fire(kernel), this, this.disposables); + kernel.onStatusChanged( + (status) => this._onKernelStatusChanged.fire({ kernel, status }), + this, + this.disposables + ); + this.asyncDisposables.push(kernel); + this.storeKernel(notebook, options, kernel); + this.deleteMappingIfKernelIsDisposed(uri, kernel); + return kernel; + } +} + +@injectable() +export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { + constructor( + @inject(IAsyncDisposableRegistry) asyncDisposables: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(CellOutputDisplayIdTracker) private readonly outputTracker: CellOutputDisplayIdTracker, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IVSCodeNotebook) notebook: IVSCodeNotebook, + @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, + @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[] + ) { + super(asyncDisposables, disposables, notebook); + } + + public getOrCreate(uri: Uri, options: KernelOptions): IBaseKernel { + // const notebook = this. const existingKernelInfo = this.getInternal(uri); - const notebook = workspace.notebookDocuments.find((nb) => nb.uri.toString() === uri.toString()); if (existingKernelInfo && existingKernelInfo.options.metadata.id === options.metadata.id) { return existingKernelInfo.kernel; } this.disposeOldKernel(uri); - const resourceUri = notebook?.notebookType === InteractiveWindowView ? options.resourceUri : uri; + const resourceUri = uri; const waitForIdleTimeout = this.configService.getSettings(resourceUri).jupyterLaunchTimeout; const interruptTimeout = this.configService.getSettings(resourceUri).jupyterInterruptTimeout; const kernel = new Kernel( uri, resourceUri, + undefined, options.metadata, this.notebookProvider, waitForIdleTimeout, @@ -82,7 +149,7 @@ export class KernelProvider extends BaseKernelProvider { this.disposables ); this.asyncDisposables.push(kernel); - this.storeKernel(uri, notebook, options, kernel); + this.storeKernel(uri, options, kernel); this.deleteMappingIfKernelIsDisposed(uri, kernel); return kernel; } diff --git a/src/kernels/kernelProvider.web.ts b/src/kernels/kernelProvider.web.ts index b74c837ed2d8..9471880ec97e 100644 --- a/src/kernels/kernelProvider.web.ts +++ b/src/kernels/kernelProvider.web.ts @@ -3,7 +3,7 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { Uri, workspace } from 'vscode'; +import { NotebookDocument, Uri } from 'vscode'; import { IApplicationShell, IWorkspaceService, IVSCodeNotebook } from '../platform/common/application/types'; import { IAsyncDisposableRegistry, @@ -12,8 +12,8 @@ import { IExtensionContext } from '../platform/common/types'; import { Kernel } from './kernel.web'; -import { IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; -import { BaseKernelProvider } from './kernelProvider.base'; +import { IBaseKernel, IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; +import { BaseCoreKernelProvider, BaseThirdPartyKernelProvider } from './kernelProvider.base'; import { IStatusProvider } from '../platform/progress/types'; import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; @@ -23,7 +23,7 @@ import { IFileSystem } from '../platform/common/platform/types'; * Web version of a kernel provider. Needed in order to create the web version of a kernel. */ @injectable() -export class KernelProvider extends BaseKernelProvider { +export class KernelProvider extends BaseCoreKernelProvider { constructor( @inject(IAsyncDisposableRegistry) asyncDisposables: IAsyncDisposableRegistry, @inject(IDisposableRegistry) disposables: IDisposableRegistry, @@ -41,20 +41,85 @@ export class KernelProvider extends BaseKernelProvider { super(asyncDisposables, disposables, notebook); } - public getOrCreate(uri: Uri, options: KernelOptions): IKernel { + public getOrCreate(notebook: NotebookDocument, options: KernelOptions): IKernel { + const uri = notebook.uri; + const existingKernelInfo = this.getInternal(notebook); + if (existingKernelInfo && existingKernelInfo.options.metadata.id === options.metadata.id) { + return existingKernelInfo.kernel; + } + this.disposeOldKernel(notebook); + + const resourceUri = notebook?.notebookType === InteractiveWindowView ? options.resourceUri : uri; + const waitForIdleTimeout = this.configService.getSettings(resourceUri).jupyterLaunchTimeout; + const interruptTimeout = this.configService.getSettings(resourceUri).jupyterInterruptTimeout; + const kernel = new Kernel( + uri, + resourceUri, + notebook, + options.metadata, + this.notebookProvider, + waitForIdleTimeout, + interruptTimeout, + this.appShell, + options.controller, + this.configService, + this.outputTracker, + this.workspaceService, + this.statusProvider, + options.creator, + this.context, + this.formatters, + this.fs + ) as IKernel; + kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); + kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); + kernel.onStarted(() => this._onDidStartKernel.fire(kernel), this, this.disposables); + kernel.onStatusChanged( + (status) => this._onKernelStatusChanged.fire({ kernel, status }), + this, + this.disposables + ); + this.asyncDisposables.push(kernel); + this.storeKernel(notebook, options, kernel); + + this.deleteMappingIfKernelIsDisposed(uri, kernel); + return kernel; + } +} + +@injectable() +export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { + constructor( + @inject(IAsyncDisposableRegistry) asyncDisposables: IAsyncDisposableRegistry, + @inject(IDisposableRegistry) disposables: IDisposableRegistry, + @inject(INotebookProvider) private notebookProvider: INotebookProvider, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(CellOutputDisplayIdTracker) private readonly outputTracker: CellOutputDisplayIdTracker, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IVSCodeNotebook) notebook: IVSCodeNotebook, + @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[], + @inject(IFileSystem) private readonly fs: IFileSystem + ) { + super(asyncDisposables, disposables, notebook); + } + + public getOrCreate(uri: Uri, options: KernelOptions): IBaseKernel { const existingKernelInfo = this.getInternal(uri); - const notebook = workspace.notebookDocuments.find((nb) => nb.uri.toString() === uri.toString()); if (existingKernelInfo && existingKernelInfo.options.metadata.id === options.metadata.id) { return existingKernelInfo.kernel; } this.disposeOldKernel(uri); - const resourceUri = notebook?.notebookType === InteractiveWindowView ? options.resourceUri : uri; + const resourceUri = uri; const waitForIdleTimeout = this.configService.getSettings(resourceUri).jupyterLaunchTimeout; const interruptTimeout = this.configService.getSettings(resourceUri).jupyterInterruptTimeout; const kernel = new Kernel( uri, resourceUri, + undefined, options.metadata, this.notebookProvider, waitForIdleTimeout, @@ -79,7 +144,9 @@ export class KernelProvider extends BaseKernelProvider { this.disposables ); this.asyncDisposables.push(kernel); - this.storeKernel(uri, notebook, options, kernel); + + this.storeKernel(uri, options, kernel); + this.deleteMappingIfKernelIsDisposed(uri, kernel); return kernel; } diff --git a/src/kernels/serviceRegistry.node.ts b/src/kernels/serviceRegistry.node.ts index 48923bd40373..3740136b3873 100644 --- a/src/kernels/serviceRegistry.node.ts +++ b/src/kernels/serviceRegistry.node.ts @@ -30,13 +30,13 @@ import { PreWarmActivatedJupyterEnvironmentVariables } from './variables/preWarm import { PythonVariablesRequester } from './variables/pythonVariableRequester'; import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; import { IDebugLocationTracker, IDebugLocationTrackerFactory, IJupyterDebugService } from './debugger/types'; -import { IKernelDependencyService, IKernelFinder, IKernelProvider } from './types'; +import { IKernelDependencyService, IKernelFinder, IKernelProvider, IThirdPartyKernelProvider } from './types'; import { IJupyterVariables, IKernelVariableRequester } from './variables/types'; import { KernelCrashMonitor } from './kernelCrashMonitor'; import { KernelAutoRestartMonitor } from './kernelAutoRestartMonitor.node'; import { registerTypes as registerWidgetTypes } from './ipywidgets/serviceRegistry.node'; import { registerTypes as registerJupyterTypes } from './jupyter/serviceRegistry.node'; -import { KernelProvider } from './kernelProvider.node'; +import { KernelProvider, ThirdPartyKernelProvider } from './kernelProvider.node'; import { KernelFinder } from './kernelFinder.node'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFactory'; @@ -107,6 +107,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea KernelAutoRestartMonitor ); serviceManager.addSingleton(IKernelProvider, KernelProvider); + serviceManager.addSingleton(IThirdPartyKernelProvider, ThirdPartyKernelProvider); serviceManager.addSingleton(IKernelFinder, KernelFinder); // Subdirectories diff --git a/src/kernels/serviceRegistry.web.ts b/src/kernels/serviceRegistry.web.ts index 78cb512da05d..495b9c3607e8 100644 --- a/src/kernels/serviceRegistry.web.ts +++ b/src/kernels/serviceRegistry.web.ts @@ -14,8 +14,8 @@ import { KernelCrashMonitor } from './kernelCrashMonitor'; import { registerTypes as registerWidgetTypes } from './ipywidgets/serviceRegistry.web'; import { registerTypes as registerJupyterTypes } from './jupyter/serviceRegistry.web'; import { injectable } from 'inversify'; -import { IKernelFinder, IKernelProvider } from './types'; -import { KernelProvider } from './kernelProvider.web'; +import { IKernelFinder, IKernelProvider, IThirdPartyKernelProvider } from './types'; +import { KernelProvider, ThirdPartyKernelProvider } from './kernelProvider.web'; import { KernelFinder } from './kernelFinder.web'; import { PreferredRemoteKernelIdProvider } from './jupyter/preferredRemoteKernelIdProvider'; import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; @@ -76,6 +76,7 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea serviceManager.addSingleton(IExtensionSyncActivationService, KernelCrashMonitor); serviceManager.addSingleton(IKernelProvider, KernelProvider); + serviceManager.addSingleton(IThirdPartyKernelProvider, ThirdPartyKernelProvider); serviceManager.addSingleton( PreferredRemoteKernelIdProvider, PreferredRemoteKernelIdProvider diff --git a/src/kernels/types.ts b/src/kernels/types.ts index effb8f311adb..167d695944c6 100644 --- a/src/kernels/types.ts +++ b/src/kernels/types.ts @@ -12,6 +12,7 @@ import type { Event, NotebookCell, NotebookController, + NotebookDocument, QuickPickItem, Uri } from 'vscode'; @@ -129,12 +130,17 @@ export function isRemoteConnection( return !isLocalConnection(kernelConnection); } -export interface IKernel extends IAsyncDisposable { +export interface IKernel extends IBaseKernel { + readonly notebook: NotebookDocument; +} + +export interface IBaseKernel extends IAsyncDisposable { /** * Total execution count on this kernel */ readonly executionCount: number; readonly uri: Uri; + readonly notebook: NotebookDocument | undefined; /** * In the case of Notebooks, this is the same as the Notebook Uri. * But in the case of Interactive Window, this is the Uri of the file (such as the Python file). @@ -219,22 +225,38 @@ export type KernelOptions = { creator: KernelActionSource; }; export const IKernelProvider = Symbol('IKernelProvider'); -export interface IKernelProvider extends IAsyncDisposable { - readonly kernels: Readonly; - onDidCreateKernel: Event; - onDidStartKernel: Event; - onDidRestartKernel: Event; - onDidDisposeKernel: Event; - onKernelStatusChanged: Event<{ status: KernelMessage.Status; kernel: IKernel }>; +export interface IKernelProvider extends IBaseKernelProvider { /** - * Get hold of the active kernel for a given Notebook. + * Get hold of the active kernel for a given notebook document. */ - get(uri: Uri): IKernel | undefined; + get(uriOrNotebook: Uri | NotebookDocument): IKernel | undefined; /** * Gets or creates a kernel for a given Notebook. * WARNING: If called with different options for same Notebook, old kernel associated with the Uri will be disposed. */ - getOrCreate(uri: Uri, options: KernelOptions): IKernel; + getOrCreate(notebook: NotebookDocument, options: KernelOptions): IKernel; +} + +export const IThirdPartyKernelProvider = Symbol('IThirdPartyKernelProvider'); +export interface IThirdPartyKernelProvider extends IBaseKernelProvider { + /** + * Get hold of the active kernel for a given resource uri. + */ + get(uri: Uri): IBaseKernel | undefined; + /** + * Gets or creates a kernel for a given resource uri. + * WARNING: If called with different options for same resource uri, old kernel associated with the Uri will be disposed. + */ + getOrCreate(uri: Uri, options: KernelOptions): IBaseKernel; +} + +export interface IBaseKernelProvider extends IAsyncDisposable { + readonly kernels: Readonly; + onDidCreateKernel: Event; + onDidStartKernel: Event; + onDidRestartKernel: Event; + onDidDisposeKernel: Event; + onKernelStatusChanged: Event<{ status: KernelMessage.Status; kernel: T }>; } export interface IRawConnection { diff --git a/src/kernels/variables/debuggerVariables.ts b/src/kernels/variables/debuggerVariables.ts index 0a86b2f1a38d..9e5722cf1f70 100644 --- a/src/kernels/variables/debuggerVariables.ts +++ b/src/kernels/variables/debuggerVariables.ts @@ -29,7 +29,6 @@ import { } from './types'; import { convertDebugProtocolVariableToIJupyterVariable, DataViewableTypes } from './helpers'; import { noop } from '../../platform/common/utils/misc'; -import { getAssociatedNotebookDocument } from '../helpers'; const KnownExcludedVariables = new Set(['In', 'Out', 'exit', 'quit']); const MaximumRowChunkSizeForDebugger = 100; @@ -170,7 +169,7 @@ export class DebuggerVariables frameId: (targetVariable as any).frameId }); - const notebook = getAssociatedNotebookDocument(kernel); + const notebook = kernel?.notebook; let fileName = notebook ? path.basename(notebook.uri.path) : ''; if (!fileName && this.debugLocation?.fileName) { fileName = path.basename(this.debugLocation.fileName); @@ -290,7 +289,7 @@ export class DebuggerVariables } private watchKernel(kernel: IKernel) { - const key = getAssociatedNotebookDocument(kernel)?.uri.toString(); + const key = kernel.notebook?.uri.toString(); if (key && !this.watchedNotebooks.has(key)) { const disposables: Disposable[] = []; disposables.push(kernel.onRestarted(this.resetImport.bind(this, key))); diff --git a/src/kernels/variables/pythonVariableRequester.ts b/src/kernels/variables/pythonVariableRequester.ts index bdfb0e172ca5..90e8681cd655 100644 --- a/src/kernels/variables/pythonVariableRequester.ts +++ b/src/kernels/variables/pythonVariableRequester.ts @@ -6,7 +6,7 @@ import { DataScience } from '../../platform/common/utils/localize'; import { stripAnsi } from '../../platform/common/utils/regexp'; import { JupyterDataRateLimitError } from '../../platform/errors/jupyterDataRateLimitError'; import { Telemetry } from '../../telemetry'; -import { executeSilently, getAssociatedNotebookDocument, SilentExecutionErrorOptions } from '../helpers'; +import { executeSilently, SilentExecutionErrorOptions } from '../helpers'; import { IKernel } from '../types'; import { IKernelVariableRequester, IJupyterVariable } from './types'; import { IDataFrameScriptGenerator, IVariableScriptGenerator } from '../../platform/common/types'; @@ -89,7 +89,7 @@ export class PythonVariablesRequester implements IKernelVariableRequester { } ); - const fileName = getAssociatedNotebookDocument(kernel)?.uri || kernel.resourceUri || kernel.uri; + const fileName = kernel.notebook?.uri || kernel.resourceUri || kernel.uri; // Combine with the original result (the call only returns the new fields) return { diff --git a/src/notebooks/controllers/kernelConnector.ts b/src/notebooks/controllers/kernelConnector.ts index bc7022e61db2..a99c0e20978a 100644 --- a/src/notebooks/controllers/kernelConnector.ts +++ b/src/notebooks/controllers/kernelConnector.ts @@ -4,13 +4,15 @@ 'use strict'; import { + IBaseKernel, IKernel, KernelConnectionMetadata, IKernelProvider, isLocalConnection, KernelInterpreterDependencyResponse, KernelAction, - KernelActionSource + KernelActionSource, + IThirdPartyKernelProvider } from '../../kernels/types'; import { Memento, NotebookDocument, NotebookController, Uri } from 'vscode'; import { ICommandManager, IApplicationShell } from '../../platform/common/application/types'; @@ -62,7 +64,7 @@ export class KernelConnector { } private static async notifyAndRestartDeadKernel( - kernel: IKernel, + kernel: IBaseKernel, serviceContainer: IServiceContainer ): Promise { const appShell = serviceContainer.get(IApplicationShell); @@ -103,7 +105,7 @@ export class KernelConnector { error: Error, errorContext: KernelAction, resource: Resource, - kernel: IKernel, + kernel: IBaseKernel, controller: NotebookController, metadata: KernelConnectionMetadata, actionSource: KernelActionSource @@ -168,13 +170,13 @@ export class KernelConnector { switch (currentContext) { case 'start': case 'execution': - return (k: IKernel) => k.start(options); + return (k: IBaseKernel) => k.start(options); case 'interrupt': - return (k: IKernel) => k.interrupt(); + return (k: IBaseKernel) => k.interrupt(); case 'restart': - return (k: IKernel) => k.restart(); + return (k: IBaseKernel) => k.restart(); } } @@ -192,7 +194,7 @@ export class KernelConnector { string, { kernel: Deferred<{ - kernel: IKernel; + kernel: IBaseKernel; deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted'; }>; options: IDisplayOptions; @@ -203,14 +205,19 @@ export class KernelConnector { serviceContainer: IServiceContainer, notebookResource: NotebookResource, options: IDisplayOptions, - promise: Promise<{ - kernel: IKernel; - deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted'; - }>, + promise: + | Promise<{ + kernel: IBaseKernel; + deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted'; + }> + | Promise<{ + kernel: IKernel; + deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted'; + }>, actionSource: KernelActionSource, - onAction: (action: KernelAction, kernel: IKernel) => void, + onAction: (action: KernelAction, kernel: IBaseKernel) => void, disposables: IDisposable[] - ): Promise { + ): Promise { const { kernel, deadKernelAction } = await promise; // Before returning, but without disposing the kernel, double check it's still valid // If a restart didn't happen, then we can't connect. Throw an error. @@ -249,8 +256,8 @@ export class KernelConnector { notebookResource: NotebookResource, options: IDisplayOptions, disposables: IDisposable[], - onAction: (action: KernelAction, kernel: IKernel) => void = () => noop() - ): Promise { + onAction: (action: KernelAction, kernel: IBaseKernel | IKernel) => void = () => noop() + ): Promise { traceVerbose(`${initialContext} the kernel, options.disableUI=${options.disableUI}`); let currentPromise = this.getKernelInfo(notebookResource); @@ -327,13 +334,19 @@ export class KernelConnector { private static setKernelInfo( notebookResource: NotebookResource, deferred: Deferred<{ - kernel: IKernel; + kernel: IBaseKernel; deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted' | undefined; }>, options: IDisplayOptions ) { if (notebookResource.notebook) { - KernelConnector.connectionsByNotebook.set(notebookResource.notebook, { kernel: deferred, options }); + KernelConnector.connectionsByNotebook.set(notebookResource.notebook, { + kernel: deferred as Deferred<{ + kernel: IKernel; + deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted' | undefined; + }>, + options + }); } else { KernelConnector.connectionsByUri.set(notebookResource.resource.toString(), { kernel: deferred, options }); } @@ -341,7 +354,7 @@ export class KernelConnector { private static deleteKernelInfo( notebookResource: NotebookResource, matchingKernelPromise?: Promise<{ - kernel: IKernel; + kernel: IBaseKernel; deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted' | undefined; }> ) { @@ -376,28 +389,33 @@ export class KernelConnector { notebookResource: NotebookResource, options: IDisplayOptions, actionSource: KernelActionSource, - onAction: (action: KernelAction, kernel: IKernel) => void + onAction: (action: KernelAction, kernel: IBaseKernel) => void ): Promise<{ - kernel: IKernel; + kernel: IBaseKernel | IKernel; deadKernelAction?: 'deadKernelWasRestarted' | 'deadKernelWasNoRestarted'; }> { const kernelProvider = serviceContainer.get(IKernelProvider); - let kernel: IKernel | undefined; + const thirdPartyKernelProvider = serviceContainer.get(IThirdPartyKernelProvider); + let kernel: IBaseKernel | undefined; let currentMethod = KernelConnector.convertContextToFunction(initialContext, options); let currentContext = initialContext; while (kernel === undefined) { // Try to create the kernel (possibly again) - kernel = kernelProvider.getOrCreate( - notebookResource.notebook ? notebookResource.notebook.uri : notebookResource.resource, - { - metadata, - controller, - resourceUri: notebookResource.resource, - creator: actionSource - } - ); - - const isKernelDead = (k: IKernel) => + kernel = notebookResource.notebook + ? kernelProvider.getOrCreate(notebookResource.notebook, { + metadata, + controller, + resourceUri: notebookResource.resource, + creator: actionSource + }) + : thirdPartyKernelProvider.getOrCreate(notebookResource.resource, { + metadata, + controller, + resourceUri: notebookResource.resource, + creator: actionSource + }); + + const isKernelDead = (k: IBaseKernel) => k.status === 'dead' || (k.status === 'terminating' && !k.disposed && !k.disposing); try { @@ -458,11 +476,11 @@ export class KernelConnector { return { kernel }; } - public static async connectToKernel( + public static async connectToNotebookKernel( controller: NotebookController, metadata: KernelConnectionMetadata, serviceContainer: IServiceContainer, - notebookResource: NotebookResource, + notebookResource: { resource: Resource; notebook: NotebookDocument }, options: IDisplayOptions, disposables: IDisposable[], actionSource: KernelActionSource = 'jupyterExtension', @@ -477,6 +495,29 @@ export class KernelConnector { notebookResource, options, disposables, + onAction as (action: KernelAction, kernel: IBaseKernel) => void + ) as Promise; + } + + public static async connectToKernel( + controller: NotebookController, + metadata: KernelConnectionMetadata, + serviceContainer: IServiceContainer, + resource: { resource: Uri; notebook: undefined }, + options: IDisplayOptions, + disposables: IDisposable[], + actionSource: KernelActionSource = 'jupyterExtension', + onAction: (action: KernelAction, kernel: IBaseKernel) => void = () => noop() + ): Promise { + return KernelConnector.wrapKernelMethod( + controller, + metadata, + 'start', + actionSource, + serviceContainer, + resource, + options, + disposables, onAction ); } diff --git a/src/notebooks/controllers/remoteKernelConnectionHandler.ts b/src/notebooks/controllers/remoteKernelConnectionHandler.ts index 2d1378db5f61..065436f06678 100644 --- a/src/notebooks/controllers/remoteKernelConnectionHandler.ts +++ b/src/notebooks/controllers/remoteKernelConnectionHandler.ts @@ -60,6 +60,7 @@ export class RemoteKernelConnectionHandler implements IExtensionSyncActivationSe } } private onDidStartKernel(kernel: IKernel) { + // TODO@rebornix, IKernel is already created by Jupyter Extension if (kernel.creator !== 'jupyterExtension' || !kernel.resourceUri) { return; } diff --git a/src/notebooks/controllers/vscodeNotebookController.ts b/src/notebooks/controllers/vscodeNotebookController.ts index 874cb83e4ac8..050254ef67ff 100644 --- a/src/notebooks/controllers/vscodeNotebookController.ts +++ b/src/notebooks/controllers/vscodeNotebookController.ts @@ -293,7 +293,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont if (!event.selected) { // If user has selected another controller, then kill the current kernel. // Possible user selected a controller that's not contributed by us at all. - const kernel = this.kernelProvider.get(event.notebook.uri); + const kernel = this.kernelProvider.get(event.notebook); if (kernel?.kernelConnectionMetadata.id === this.kernelConnection.id) { traceInfo( `Disposing kernel ${this.kernelConnection.id} for notebook ${getDisplayPath( @@ -499,8 +499,8 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont // Execution should be ended elsewhere } - private async connectToKernel(doc: NotebookDocument, options: IDisplayOptions) { - return KernelConnector.connectToKernel( + private async connectToKernel(doc: NotebookDocument, options: IDisplayOptions): Promise { + return KernelConnector.connectToNotebookKernel( this.controller, this.kernelConnection, this.serviceContainer, @@ -552,7 +552,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont } private async onDidSelectController(document: NotebookDocument) { const selectedKernelConnectionMetadata = this.connection; - const existingKernel = this.kernelProvider.get(document.uri); + const existingKernel = this.kernelProvider.get(document); if ( existingKernel && areKernelConnectionsEqual(existingKernel.kernelConnectionMetadata, selectedKernelConnectionMetadata) @@ -610,7 +610,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont // This will dispose any existing (older kernels) associated with this notebook. // This way other parts of extension have access to this kernel immediately after event is handled. // Unlike webview notebooks we cannot revert to old kernel if kernel switching fails. - const newKernel = this.kernelProvider.getOrCreate(document.uri, { + const newKernel = this.kernelProvider.getOrCreate(document, { metadata: selectedKernelConnectionMetadata, controller: this.controller, resourceUri: document.uri, // In the case of interactive window, we cannot pass the Uri of notebook, it must be the Py file or undefined. diff --git a/src/notebooks/debugger/debuggingManagerBase.ts b/src/notebooks/debugger/debuggingManagerBase.ts index 6ea4823601bc..e1d720f8aafb 100644 --- a/src/notebooks/debugger/debuggingManagerBase.ts +++ b/src/notebooks/debugger/debuggingManagerBase.ts @@ -146,9 +146,9 @@ export abstract class DebuggingManagerBase implements IDisposable { await this.notebookControllerLoader.loaded; const controller = this.notebookControllerSelection.getSelected(doc); - let kernel = this.kernelProvider.get(doc.uri); + let kernel = this.kernelProvider.get(doc); if (!kernel && controller) { - kernel = this.kernelProvider.getOrCreate(doc.uri, { + kernel = this.kernelProvider.getOrCreate(doc, { metadata: controller.connection, controller: controller?.controller, resourceUri: doc.uri, @@ -164,13 +164,13 @@ export abstract class DebuggingManagerBase implements IDisposable { protected async checkForIpykernel6(doc: NotebookDocument): Promise { try { - let kernel = this.kernelProvider.get(doc.uri); + let kernel = this.kernelProvider.get(doc); if (!kernel) { const controller = this.notebookControllerSelection.getSelected(doc); if (!controller) { return IpykernelCheckResult.ControllerNotSelected; } - kernel = this.kernelProvider.getOrCreate(doc.uri, { + kernel = this.kernelProvider.getOrCreate(doc, { metadata: controller.connection, controller: controller?.controller, resourceUri: doc.uri, diff --git a/src/notebooks/export/exportBase.web.ts b/src/notebooks/export/exportBase.web.ts index a93ff63c38f3..2412801fe3f2 100644 --- a/src/notebooks/export/exportBase.web.ts +++ b/src/notebooks/export/exportBase.web.ts @@ -44,7 +44,7 @@ export class ExportBase implements INbConvertExport, IExportBase { _interpreter: PythonEnvironment, _token: CancellationToken ): Promise { - const kernel = this.kernelProvider.get(sourceDocument.uri); + const kernel = this.kernelProvider.get(sourceDocument); if (!kernel) { // trace error return; diff --git a/src/notebooks/notebookCommandListener.ts b/src/notebooks/notebookCommandListener.ts index bbd8a0c540bd..36a935b6cc3e 100644 --- a/src/notebooks/notebookCommandListener.ts +++ b/src/notebooks/notebookCommandListener.ts @@ -30,7 +30,6 @@ import { INotebookEditorProvider } from './types'; import { IServiceContainer } from '../platform/ioc/types'; import { endCellAndDisplayErrorsInCell } from '../kernels/execution/helpers'; import { chainWithPendingUpdates } from '../kernels/execution/notebookUpdater'; -import { getAssociatedNotebookDocument } from '../kernels/helpers'; import { IDataScienceErrorHandler } from '../kernels/errors/types'; import { getNotebookMetadata } from '../platform/common/utils'; import { KernelConnector } from './controllers/kernelConnector'; @@ -189,7 +188,7 @@ export class NotebookCommandListener implements IDataScienceCommandListener { } traceInfoIfCI(`Interrupt kernel command handler for ${getDisplayPath(document.uri)}`); - const kernel = this.kernelProvider.get(document.uri); + const kernel = this.kernelProvider.get(document); if (!kernel) { traceInfo(`Interrupt requested & no kernel.`); return; @@ -206,7 +205,7 @@ export class NotebookCommandListener implements IDataScienceCommandListener { } sendTelemetryEvent(Telemetry.RestartKernelCommand); - const kernel = this.kernelProvider.get(document.uri); + const kernel = this.kernelProvider.get(document); if (kernel) { trackKernelResourceInformation(kernel.resourceUri, { restartKernel: true }); @@ -236,10 +235,7 @@ export class NotebookCommandListener implements IDataScienceCommandListener { private readonly pendingRestartInterrupt = new WeakMap>(); private async wrapKernelMethod(currentContext: 'interrupt' | 'restart', kernel: IKernel) { - const notebook = getAssociatedNotebookDocument(kernel); - if (!notebook) { - throw new Error('Unable to start a kernel that is not attached to a notebook document'); - } + const notebook = kernel.notebook; // We don't want to create multiple restarts/interrupt requests for the same kernel. const pendingPromise = this.pendingRestartInterrupt.get(kernel); if (pendingPromise) { diff --git a/src/standalone/api/kernelApi.ts b/src/standalone/api/kernelApi.ts index d0d7991c6d41..269ecf740964 100644 --- a/src/standalone/api/kernelApi.ts +++ b/src/standalone/api/kernelApi.ts @@ -6,8 +6,9 @@ import { Disposable, Event, EventEmitter, Uri } from 'vscode'; import { KernelConnectionWrapper } from './kernelConnectionWrapper'; import { IKernelProvider, - IKernel, - KernelConnectionMetadata as IKernelKernelConnectionMetadata + IBaseKernel, + KernelConnectionMetadata as IKernelKernelConnectionMetadata, + IThirdPartyKernelProvider } from '../../kernels/types'; import { disposeAllDisposables } from '../../platform/common/helpers'; import { traceInfo } from '../../platform/logging'; @@ -37,6 +38,7 @@ export class JupyterKernelServiceFactory implements IExportedKernelServiceFactor private readonly extensionApi = new Map(); constructor( @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, + @inject(IThirdPartyKernelProvider) private readonly thirdPartyKernelProvider: IThirdPartyKernelProvider, @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, @inject(IControllerRegistration) private readonly controllerRegistration: IControllerRegistration, @inject(IControllerLoader) private readonly controllerLoader: IControllerLoader, @@ -57,6 +59,7 @@ export class JupyterKernelServiceFactory implements IExportedKernelServiceFactor const service = new JupyterKernelService( accessInfo.extensionId, this.kernelProvider, + this.thirdPartyKernelProvider, this.disposables, this.controllerRegistration, this.controllerLoader, @@ -89,10 +92,11 @@ class JupyterKernelService implements IExportedKernelService { }); return this._onDidChangeKernels.event; } - private static readonly wrappedKernelConnections = new WeakMap(); + private static readonly wrappedKernelConnections = new WeakMap(); constructor( private readonly callingExtensionId: string, @inject(IKernelProvider) private readonly kernelProvider: IKernelProvider, + @inject(IThirdPartyKernelProvider) private readonly thirdPartyKernelProvider: IThirdPartyKernelProvider, @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, @inject(IControllerRegistration) private readonly controllerRegistration: IControllerRegistration, @inject(IControllerLoader) private readonly controllerLoader: IControllerLoader, @@ -100,6 +104,8 @@ class JupyterKernelService implements IExportedKernelService { ) { this.kernelProvider.onDidDisposeKernel(() => this._onDidChangeKernels.fire(), this, disposables); this.kernelProvider.onDidStartKernel(() => this._onDidChangeKernels.fire(), this, disposables); + this.thirdPartyKernelProvider.onDidDisposeKernel(() => this._onDidChangeKernels.fire(), this, disposables); + this.thirdPartyKernelProvider.onDidStartKernel(() => this._onDidChangeKernels.fire(), this, disposables); this.controllerLoader.refreshed(() => this._onDidChangeKernelSpecifications.fire(), this, disposables); } async getKernelSpecifications(refresh?: boolean): Promise { @@ -123,7 +129,25 @@ class JupyterKernelService implements IExportedKernelService { (item) => item.startedAtLeastOnce || item.kernelConnectionMetadata.kind === 'connectToLiveRemoteKernel' ) .forEach((item) => { - const kernel = this.kernelProvider.get(item.uri); + const kernel = this.kernelProvider.get(item.notebook); + // When returning list of active sessions, we don't want to return something thats + // associated with a controller. + // Note: In VS Code, a controller starts a kernel, however the controller only keeps track of the kernel spec. + // Hence when we return this connection, we're actually returning the controller's kernel spec & the uri. + if (kernel && kernel.session?.kernelId) { + kernelsAlreadyListed.add(kernel.session?.kernelId); + } + kernels.push({ + metadata: this.translateKernelConnectionMetadataToExportedType(item.kernelConnectionMetadata), + uri: item.uri + }); + }); + this.thirdPartyKernelProvider.kernels + .filter( + (item) => item.startedAtLeastOnce || item.kernelConnectionMetadata.kind === 'connectToLiveRemoteKernel' + ) + .forEach((item) => { + const kernel = this.thirdPartyKernelProvider.get(item.uri); // When returning list of active sessions, we don't want to return something thats // associated with a controller. // Note: In VS Code, a controller starts a kernel, however the controller only keeps track of the kernel spec. @@ -155,7 +179,7 @@ class JupyterKernelService implements IExportedKernelService { extensionId: this.callingExtensionId, pemUsed: 'getKernel' }); - const kernel = this.kernelProvider.get(uri); + const kernel = this.thirdPartyKernelProvider.get(uri) ?? this.kernelProvider.get(uri); if (kernel?.session?.kernel) { const connection = this.wrapKernelConnection(kernel); return { @@ -206,7 +230,7 @@ class JupyterKernelService implements IExportedKernelService { } return this.wrapKernelConnection(kernel); } - private wrapKernelConnection(kernel: IKernel): IKernelConnectionInfo { + private wrapKernelConnection(kernel: IBaseKernel): IKernelConnectionInfo { if (JupyterKernelService.wrappedKernelConnections.get(kernel)) { return JupyterKernelService.wrappedKernelConnections.get(kernel)!; } @@ -252,7 +276,7 @@ class KernelSocketWrapper implements IKernelSocket { public get onDidChange(): Event { return this._onDidSocketChange.event; } - constructor(kernel: IKernel) { + constructor(kernel: IBaseKernel) { const subscription = kernel.kernelSocket.subscribe((socket) => { this.removeHooks(); this.socket = socket?.socket; diff --git a/src/standalone/api/kernelConnectionWrapper.ts b/src/standalone/api/kernelConnectionWrapper.ts index a94092d658e8..a459c8fe104b 100644 --- a/src/standalone/api/kernelConnectionWrapper.ts +++ b/src/standalone/api/kernelConnectionWrapper.ts @@ -5,7 +5,7 @@ import type { Kernel } from '@jupyterlab/services'; import { IDisposable } from '../../platform/common/types'; import { noop } from '../../platform/common/utils/misc'; import { BaseKernelConnectionWrapper } from '../../kernels/jupyter/baseKernelConnectionWrapper'; -import { IKernel } from '../../kernels/types'; +import { IBaseKernel } from '../../kernels/types'; export class KernelConnectionWrapper extends BaseKernelConnectionWrapper { /** @@ -24,7 +24,7 @@ export class KernelConnectionWrapper extends BaseKernelConnectionWrapper { } } - constructor(readonly kernel: IKernel, disposables: IDisposable[]) { + constructor(readonly kernel: IBaseKernel, disposables: IDisposable[]) { super(kernel.session!.kernel!, disposables); const emiStatusChangeEvents = () => { this.statusChanged.emit(kernel.status); diff --git a/src/standalone/context/activeEditorContext.ts b/src/standalone/context/activeEditorContext.ts index 020fa8b4cafe..00119bd049bf 100644 --- a/src/standalone/context/activeEditorContext.ts +++ b/src/standalone/context/activeEditorContext.ts @@ -14,7 +14,7 @@ import { isNotebookCell, noop } from '../../platform/common/utils/misc'; import { InteractiveWindowView, JupyterNotebookView } from '../../platform/common/constants'; import { IInteractiveWindowProvider, IInteractiveWindow } from '../../interactive-window/types'; import { getNotebookMetadata, isJupyterNotebook } from '../../platform/common/utils'; -import { getAssociatedNotebookDocument, isPythonNotebook } from '../../kernels/helpers'; +import { isPythonNotebook } from '../../kernels/helpers'; import { IControllerSelection } from '../../notebooks/controllers/types'; @injectable() @@ -168,7 +168,7 @@ export class ActiveEditorContextService implements IExtensionSingleActivationSer private updateContextOfActiveNotebookKernel(activeEditor?: NotebookEditor) { const kernel = activeEditor && activeEditor.notebook.notebookType === JupyterNotebookView - ? this.kernelProvider.get(activeEditor.notebook.uri) + ? this.kernelProvider.get(activeEditor.notebook) : undefined; if (kernel) { const canStart = kernel.status !== 'unknown'; @@ -193,7 +193,7 @@ export class ActiveEditorContextService implements IExtensionSingleActivationSer } private updateContextOfActiveInteractiveWindowKernel() { const notebook = this.interactiveProvider?.getActiveOrAssociatedInteractiveWindow()?.notebookEditor?.notebook; - const kernel = notebook ? this.kernelProvider.get(notebook.uri) : undefined; + const kernel = notebook ? this.kernelProvider.get(notebook) : undefined; if (kernel) { const canStart = kernel.status !== 'unknown'; this.canRestartInteractiveWindowKernelContext.set(!!canStart).ignoreErrors(); @@ -206,11 +206,11 @@ export class ActiveEditorContextService implements IExtensionSingleActivationSer this.updateSelectedKernelContext(); } private onDidKernelStatusChange({ kernel }: { kernel: IKernel }) { - const notebook = getAssociatedNotebookDocument(kernel); - if (notebook?.notebookType === InteractiveWindowView) { + const notebook = kernel.notebook; + if (notebook.notebookType === InteractiveWindowView) { this.updateContextOfActiveInteractiveWindowKernel(); } else if ( - notebook?.notebookType === JupyterNotebookView && + notebook.notebookType === JupyterNotebookView && notebook === this.vscNotebook.activeNotebookEditor?.notebook ) { this.updateContextOfActiveNotebookKernel(this.vscNotebook.activeNotebookEditor); diff --git a/src/standalone/intellisense/pythonKernelCompletionProvider.ts b/src/standalone/intellisense/pythonKernelCompletionProvider.ts index cab4e8a6dea5..ced6c9955bfc 100644 --- a/src/standalone/intellisense/pythonKernelCompletionProvider.ts +++ b/src/standalone/intellisense/pythonKernelCompletionProvider.ts @@ -82,7 +82,7 @@ export class PythonKernelCompletionProvider implements CompletionItemProvider { return []; } - const kernel = this.kernelProvider.get(notebookDocument.uri); + const kernel = this.kernelProvider.get(notebookDocument); if (!kernel || !kernel.session) { traceError(`Live Notebook not available for ${getDisplayPath(notebookDocument.uri)}`); return []; diff --git a/src/test/client/api.vscode.test.ts b/src/test/client/api.vscode.test.ts index d9d92ebe0257..917626388995 100644 --- a/src/test/client/api.vscode.test.ts +++ b/src/test/client/api.vscode.test.ts @@ -21,7 +21,7 @@ import { IS_REMOTE_NATIVE_TEST } from '../constants.node'; import { workspace } from 'vscode'; // eslint-disable-next-line -suite('3rd Party Kernel Service API', function () { +suite.only('3rd Party Kernel Service API', function () { let api: IExtensionTestApi; let vscodeNotebook: IVSCodeNotebook; const disposables: IDisposable[] = []; diff --git a/src/test/datascience/interactiveWindow.vscode.common.test.ts b/src/test/datascience/interactiveWindow.vscode.common.test.ts index bb2e1e9c3e18..2cbf40a4420e 100644 --- a/src/test/datascience/interactiveWindow.vscode.common.test.ts +++ b/src/test/datascience/interactiveWindow.vscode.common.test.ts @@ -162,7 +162,7 @@ suite(`Interactive window execution`, async function () { // Restart kernel const kernelProvider = api.serviceContainer.get(IKernelProvider); - const kernel = kernelProvider.get(notebookDocument.uri); + const kernel = kernelProvider.get(notebookDocument); const handler = createEventHandler(kernel!, 'onRestarted', disposables); await vscode.commands.executeCommand('jupyter.restartkernel'); // Wait for restart to finish diff --git a/src/test/datascience/ipywidgets/ipyWidgetScriptManager.vscode.common.test.ts b/src/test/datascience/ipywidgets/ipyWidgetScriptManager.vscode.common.test.ts index b7836ee938f2..d88ca4ed5c09 100644 --- a/src/test/datascience/ipywidgets/ipyWidgetScriptManager.vscode.common.test.ts +++ b/src/test/datascience/ipywidgets/ipyWidgetScriptManager.vscode.common.test.ts @@ -69,7 +69,7 @@ suite('IPyWidget Script Manager', function () { await runCell(cell); await waitForCellExecutionToComplete(cell); - kernel = kernelProvider.get(notebook.uri)!; + kernel = kernelProvider.get(notebook)!; scriptManager = widgetScriptManagerFactory.getOrCreate(kernel); }); setup(async function () { diff --git a/src/test/datascience/notebook/interruptRestart.vscode.test.ts b/src/test/datascience/notebook/interruptRestart.vscode.test.ts index cd6241a2a0a0..0b372ae509e1 100644 --- a/src/test/datascience/notebook/interruptRestart.vscode.test.ts +++ b/src/test/datascience/notebook/interruptRestart.vscode.test.ts @@ -123,7 +123,7 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors (slow)', await Promise.all([runAllCellsInActiveNotebook(), waitForTextOutput(cell, '1', 0, false)]); // Restart the kernel & use event handler to check if it was restarted successfully. - const kernel = api.serviceContainer.get(IKernelProvider).get(cell.notebook.uri); + const kernel = api.serviceContainer.get(IKernelProvider).get(cell.notebook); if (!kernel) { throw new Error('Kernel not available'); } @@ -366,7 +366,7 @@ suite('DataScience - VSCode Notebook - Restart/Interrupt/Cancel/Errors (slow)', await sleep(500); // Restart the kernel & use event handler to check if it was restarted successfully. - const kernel = api.serviceContainer.get(IKernelProvider).get(cell1.notebook.uri); + const kernel = api.serviceContainer.get(IKernelProvider).get(cell1.notebook); if (!kernel) { throw new Error('Kernel not available'); } diff --git a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts index f8a7b25eb854..65444b919710 100644 --- a/src/test/datascience/notebook/kernelCrashes.vscode.test.ts +++ b/src/test/datascience/notebook/kernelCrashes.vscode.test.ts @@ -112,7 +112,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' const [cell1, cell2] = vscodeNotebook.activeNotebookEditor!.notebook.getCells(); await Promise.all([runCell(cell1), waitForExecutionCompletedSuccessfully(cell1)]); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const restartingEventFired = createDeferred(); const autoRestartingEventFired = createDeferred(); @@ -161,7 +161,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' const [cell1, cell2] = vscodeNotebook.activeNotebookEditor!.notebook.getCells(); await Promise.all([runCell(cell1), waitForExecutionCompletedSuccessfully(cell1)]); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const terminatingEventFired = createDeferred(); const deadEventFired = createDeferred(); const expectedErrorMessage = DataScience.kernelDiedWithoutError().format( @@ -215,7 +215,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' await runAndFailWithKernelCrash(); await insertCodeCell('print("123412341234")', { index: 2 }); const cell3 = vscodeNotebook.activeNotebookEditor!.notebook.cellAt(2); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) @@ -238,7 +238,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' await runAndFailWithKernelCrash(); await insertCodeCell('print("123412341234")', { index: 2 }); const cell3 = vscodeNotebook.activeNotebookEditor!.notebook.cellAt(2); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) @@ -279,7 +279,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' await runAndFailWithKernelCrash(); await insertCodeCell('print("123412341234")', { index: 2 }); const cell3 = vscodeNotebook.activeNotebookEditor!.notebook.cellAt(2); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) @@ -302,7 +302,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' await runAndFailWithKernelCrash(); await insertCodeCell('print("123412341234")', { index: 2 }); const cell3 = vscodeNotebook.activeNotebookEditor!.notebook.cellAt(2); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) @@ -323,7 +323,7 @@ suite('DataScience - VSCode Notebook Kernel Error Handling - (Execution) (slow)' test('Ensure we get only one prompt to restart kernel when running all cells against a dead kernel', async function () { await runAndFailWithKernelCrash(); await insertCodeCell('print("123412341234")', { index: 2 }); - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; const expectedErrorMessage = DataScience.cannotRunCellKernelIsDead().format( getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata) diff --git a/src/test/datascience/notebook/kernelEvents.vscode.common.test.ts b/src/test/datascience/notebook/kernelEvents.vscode.common.test.ts index 2e36beb57961..7a5cfeef638f 100644 --- a/src/test/datascience/notebook/kernelEvents.vscode.common.test.ts +++ b/src/test/datascience/notebook/kernelEvents.vscode.common.test.ts @@ -81,8 +81,8 @@ suite('Kernel Event', function () { const kernelStatusChanged = createEventHandler(kernelProvider, 'onKernelStatusChanged', disposables); const nb = await createEmptyPythonNotebook(disposables); - await waitForCondition(async () => !!kernelProvider.get(nb.uri), 5_000, 'Kernel not created'); - const kernel = kernelProvider.get(nb.uri)!; + await waitForCondition(async () => !!kernelProvider.get(nb), 5_000, 'Kernel not created'); + const kernel = kernelProvider.get(nb)!; const startedEvent = createEventHandler(kernel, 'onStarted', disposables); const onPreExecuteEvent = createEventHandler(kernel, 'onPreExecute', disposables); const onStatusChangeEvent = createEventHandler(kernel, 'onStatusChanged', disposables); @@ -117,14 +117,14 @@ suite('Kernel Event', function () { }); test('Kernel.IKernelConnection Events', async () => { const nb = await createEmptyPythonNotebook(disposables); - await waitForCondition(async () => !!kernelProvider.get(nb.uri), 5_000, 'Kernel not created'); - const kernel = kernelProvider.get(nb.uri)!; + await waitForCondition(async () => !!kernelProvider.get(nb), 5_000, 'Kernel not created'); + const kernel = kernelProvider.get(nb)!; const onPreExecuteEvent = createEventHandler(kernel, 'onPreExecute', disposables); const cell = await insertCodeCell('print("cell1")', { index: 0 }); await Promise.all([runCell(cell), waitForTextOutput(cell, 'cell1')]); - const kernelConnection = kernelProvider.get(nb.uri)!.session!.kernel!; + const kernelConnection = kernelProvider.get(nb)!.session!.kernel!; assert.strictEqual(onPreExecuteEvent.count, 1, 'Pre-execute should be fired once'); assert.equal(onPreExecuteEvent.first, cell, 'Incorrect cell'); diff --git a/src/test/datascience/notebook/kernelSelection.vscode.test.ts b/src/test/datascience/notebook/kernelSelection.vscode.test.ts index 8127833d908c..c4a20b88b3d9 100644 --- a/src/test/datascience/notebook/kernelSelection.vscode.test.ts +++ b/src/test/datascience/notebook/kernelSelection.vscode.test.ts @@ -316,7 +316,7 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { } // Very this kernel gets disposed when we switch the notebook kernel. - const kernel = kernelProvider.get(window.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(window.activeNotebookEditor!.notebook)!; assert.ok(kernel, 'Kernel is not defined'); const eventListener = createEventHandler(kernel, 'onDisposed'); @@ -338,7 +338,7 @@ suite('DataScience - VSCode Notebook - Kernel Selection', function () { // Verify the new kernel is not the same as the old. assert.isFalse( - kernel === kernelProvider.get(window.activeNotebookEditor!.notebook.uri), + kernel === kernelProvider.get(window.activeNotebookEditor!.notebook), 'Kernels should not be the same' ); }); diff --git a/src/test/datascience/widgets/standardWidgets.vscode.common.test.ts b/src/test/datascience/widgets/standardWidgets.vscode.common.test.ts index 69b471932036..6cd47a20f01c 100644 --- a/src/test/datascience/widgets/standardWidgets.vscode.common.test.ts +++ b/src/test/datascience/widgets/standardWidgets.vscode.common.test.ts @@ -248,7 +248,7 @@ suite('Standard IPyWidget Tests', function () { await assertOutputContainsHtml(cell, comms, ['66'], '.widget-readout'); // Restart the kernel. - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; await kernel.restart(); await executeCellAndWaitForOutput(cell, comms); await assertOutputContainsHtml(cell, comms, ['66'], '.widget-readout'); @@ -271,7 +271,7 @@ suite('Standard IPyWidget Tests', function () { await assertOutputContainsHtml(cell, comms, ['66'], '.widget-readout'); // Restart the kernel. - const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook.uri)!; + const kernel = kernelProvider.get(vscodeNotebook.activeNotebookEditor!.notebook)!; await kernel.interrupt(); await executeCellAndWaitForOutput(cell, comms); await assertOutputContainsHtml(cell, comms, ['66'], '.widget-readout'); diff --git a/src/test/datascience/widgets/thirdpartyWidgets.vscode.common.test.ts b/src/test/datascience/widgets/thirdpartyWidgets.vscode.common.test.ts index 950e43ffdfcb..685bbc4968e4 100644 --- a/src/test/datascience/widgets/thirdpartyWidgets.vscode.common.test.ts +++ b/src/test/datascience/widgets/thirdpartyWidgets.vscode.common.test.ts @@ -160,7 +160,7 @@ import { GlobalStateKeyToTrackIfUserConfiguredCDNAtLeastOnce } from '../../../ke }); const cell = vscodeNotebook.activeNotebookEditor!.notebook.cellAt(1); // ipyvolume fails in Python 3.10 due to a known issue. - const kernel = kernelProvider.get(cell.notebook.uri); + const kernel = kernelProvider.get(cell.notebook); if ( kernel && kernel.kernelConnectionMetadata.interpreter && diff --git a/src/test/interactive-winndow/generatedCodeStorageManager.unit.test.ts b/src/test/interactive-winndow/generatedCodeStorageManager.unit.test.ts index 065e2233c33e..a5c3479b36d3 100644 --- a/src/test/interactive-winndow/generatedCodeStorageManager.unit.test.ts +++ b/src/test/interactive-winndow/generatedCodeStorageManager.unit.test.ts @@ -92,6 +92,7 @@ suite('GeneratedCodeStorageManager', () => { const iwKernelRestart = new EventEmitter(); disposables.push(iwKernelRestart); when(kernel.onRestarted).thenReturn(iwKernelRestart.event); + when(kernel.notebook).thenReturn(iwNotebookInstance); when(kernel.uri).thenReturn(nbUri); when(kernel.creator).thenReturn('jupyterExtension'); when(iwNotebook.notebookType).thenReturn(InteractiveWindowView); diff --git a/src/test/kernels/kernelProvider.node.unit.test.ts b/src/test/kernels/kernelProvider.node.unit.test.ts index 62940a15e171..4f9d715fa3a8 100644 --- a/src/test/kernels/kernelProvider.node.unit.test.ts +++ b/src/test/kernels/kernelProvider.node.unit.test.ts @@ -5,8 +5,14 @@ import { assert } from 'chai'; import { anything, instance, mock, when } from 'ts-mockito'; import { EventEmitter, NotebookController, NotebookDocument, Uri } from 'vscode'; import { CellOutputDisplayIdTracker } from '../../kernels/execution/cellDisplayIdTracker'; -import { KernelProvider as NodeKernelProvider } from '../../kernels/kernelProvider.node'; -import { IKernelProvider, INotebookProvider, KernelConnectionMetadata, KernelOptions } from '../../kernels/types'; +import { KernelProvider, ThirdPartyKernelProvider } from '../../kernels/kernelProvider.node'; +import { + IThirdPartyKernelProvider, + INotebookProvider, + KernelConnectionMetadata, + KernelOptions, + IKernelProvider +} from '../../kernels/types'; import { IApplicationShell, IVSCodeNotebook, IWorkspaceService } from '../../platform/common/application/types'; import { AsyncDisposableRegistry } from '../../platform/common/asyncDisposableRegistry'; import { JupyterNotebookView } from '../../platform/common/constants'; @@ -27,6 +33,7 @@ suite('KernelProvider Node', () => { const disposables: IDisposable[] = []; let asyncDisposables: AsyncDisposableRegistry; let kernelProvider: IKernelProvider; + let thirdPartyKernelProvider: IThirdPartyKernelProvider; let notebookProvider: INotebookProvider; let configService: IConfigurationService; let appShell: IApplicationShell; @@ -81,7 +88,23 @@ suite('KernelProvider Node', () => { instance(sampleNotebook2), instance(sampleNotebook3) ]); - kernelProvider = new NodeKernelProvider( + + kernelProvider = new KernelProvider( + asyncDisposables, + disposables, + instance(notebookProvider), + instance(configService), + instance(appShell), + instance(fs), + instance(outputTracker), + instance(workspaceService), + instance(vscNotebook), + instance(pythonExecFactory), + instance(statusProvider), + instance(context), + [] + ); + thirdPartyKernelProvider = new ThirdPartyKernelProvider( asyncDisposables, disposables, instance(notebookProvider), @@ -118,7 +141,7 @@ suite('KernelProvider Node', () => { const onKernelCreated = createEventHandler(kernelProvider, 'onDidCreateKernel', disposables); const onKernelDisposed = createEventHandler(kernelProvider, 'onDidDisposeKernel', disposables); - const kernel = kernelProvider.getOrCreate(sampleUri1, options); + const kernel = kernelProvider.getOrCreate(instance(sampleNotebook1), options); asyncDisposables.push(kernel); assert.equal(kernel.uri, sampleUri1, 'Kernel id should match the uri'); @@ -128,7 +151,11 @@ suite('KernelProvider Node', () => { assert.equal(onKernelDisposed.count, 0, 'Should not have triggered the event'); assert.isOk(kernel, 'Should be an object'); assert.equal(kernel, kernelProvider.get(sampleUri1), 'Should return the same instance'); - assert.equal(kernel, kernelProvider.getOrCreate(sampleUri1, options), 'Should return the same instance'); + assert.equal( + kernel, + kernelProvider.getOrCreate(instance(sampleNotebook1), options), + 'Should return the same instance' + ); await kernel.dispose(); assert.isTrue(kernel.disposed, 'Kernel should be disposed'); @@ -150,33 +177,33 @@ suite('KernelProvider Node', () => { resourceUri: uri }; - assert.isUndefined(kernelProvider.get(uri), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri1), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri2), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri3), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(uri), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri1), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri2), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri3), 'Should not return an instance'); - const onKernelCreated = createEventHandler(kernelProvider, 'onDidCreateKernel', disposables); - const onKernelDisposed = createEventHandler(kernelProvider, 'onDidDisposeKernel', disposables); - const kernel = kernelProvider.getOrCreate(uri, options); + const onKernelCreated = createEventHandler(thirdPartyKernelProvider, 'onDidCreateKernel', disposables); + const onKernelDisposed = createEventHandler(thirdPartyKernelProvider, 'onDidDisposeKernel', disposables); + const kernel = thirdPartyKernelProvider.getOrCreate(uri, options); asyncDisposables.push(kernel); assert.equal(kernel.uri, uri, 'Kernel id should match the uri'); - assert.isUndefined(kernelProvider.get(sampleUri2), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri3), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri2), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri3), 'Should not return an instance'); assert.equal(onKernelCreated.count, 1, 'Should have triggered the event'); assert.equal(onKernelDisposed.count, 0, 'Should not have triggered the event'); assert.isOk(kernel, 'Should be an object'); - assert.equal(kernel, kernelProvider.get(uri), 'Should return the same instance'); - assert.equal(kernel, kernelProvider.getOrCreate(uri, options), 'Should return the same instance'); + assert.equal(kernel, thirdPartyKernelProvider.get(uri), 'Should return the same instance'); + assert.equal(kernel, thirdPartyKernelProvider.getOrCreate(uri, options), 'Should return the same instance'); await kernel.dispose(); assert.isTrue(kernel.disposed, 'Kernel should be disposed'); assert.equal(onKernelDisposed.count, 1, 'Should have triggered the disposed event'); assert.equal(onKernelDisposed.first, kernel, 'Incorrect disposed event arg'); - assert.isUndefined(kernelProvider.get(sampleUri1), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri2), 'Should not return an instance'); - assert.isUndefined(kernelProvider.get(sampleUri3), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri1), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri2), 'Should not return an instance'); + assert.isUndefined(thirdPartyKernelProvider.get(sampleUri3), 'Should not return an instance'); }); test('When kernel is disposed a new kernel should be returned when calling getOrCreate', async () => { const metadata = mock(); @@ -189,12 +216,12 @@ suite('KernelProvider Node', () => { }; // Dispose the first kernel - const kernel = kernelProvider.getOrCreate(sampleUri1, options); + const kernel = kernelProvider.getOrCreate(sampleNotebook1, options); await kernel.dispose(); assert.isTrue(kernel.disposed, 'Kernel should be disposed'); assert.isUndefined(kernelProvider.get(sampleUri1), 'Should not return an instance as kernel was disposed'); - const newKernel = kernelProvider.getOrCreate(sampleUri1, options); + const newKernel = kernelProvider.getOrCreate(sampleNotebook1, options); asyncDisposables.push(newKernel); assert.notEqual(kernel, newKernel, 'Should return a different instance'); }); @@ -208,7 +235,7 @@ suite('KernelProvider Node', () => { resourceUri: sampleUri1 }; - const kernel = kernelProvider.getOrCreate(sampleUri1, options); + const kernel = kernelProvider.getOrCreate(instance(sampleNotebook1), options); assert.isOk(kernel); const onKernelDisposed = createEventHandler(kernelProvider, 'onDidDisposeKernel', disposables); assert.isOk(kernelProvider.get(sampleUri1), 'Should return an instance'); @@ -220,7 +247,7 @@ suite('KernelProvider Node', () => { assert.isUndefined(kernelProvider.get(sampleUri1), 'Should not return an instance'); // Calling getOrCreate again will return a whole new instance. - const newKernel = kernelProvider.getOrCreate(sampleUri1, options); + const newKernel = kernelProvider.getOrCreate(instance(sampleNotebook1), options); asyncDisposables.push(newKernel); assert.notEqual(kernel, newKernel, 'Should return a different instance'); }); diff --git a/src/webviews/extension-side/variablesView/notebookWatcher.ts b/src/webviews/extension-side/variablesView/notebookWatcher.ts index 21c85a708bca..87383665f7bb 100644 --- a/src/webviews/extension-side/variablesView/notebookWatcher.ts +++ b/src/webviews/extension-side/variablesView/notebookWatcher.ts @@ -18,7 +18,6 @@ import { IInteractiveWindowProvider } from '../../../interactive-window/types'; import { IVSCodeNotebook } from '../../../platform/common/application/types'; import { IDisposableRegistry } from '../../../platform/common/types'; import { IDataViewerFactory } from '../dataviewer/types'; -import { getAssociatedNotebookDocument } from '../../../kernels/helpers'; import { JupyterNotebookView } from '../../../platform/common/constants'; import { isJupyterNotebook } from '../../../platform/common/utils'; @@ -49,16 +48,14 @@ export class NotebookWatcher implements INotebookWatcher { public get activeKernel(): IKernel | undefined { const activeNotebook = this.notebooks.activeNotebookEditor?.notebook; const activeJupyterNotebookKernel = - activeNotebook?.notebookType == JupyterNotebookView - ? this.kernelProvider.get(activeNotebook.uri) - : undefined; + activeNotebook?.notebookType == JupyterNotebookView ? this.kernelProvider.get(activeNotebook) : undefined; if (activeJupyterNotebookKernel) { return activeJupyterNotebookKernel; } const interactiveWindowDoc = this.getActiveInteractiveWindowDocument(); const activeInteractiveWindowKernel = interactiveWindowDoc - ? this.kernelProvider.get(interactiveWindowDoc.uri) + ? this.kernelProvider.get(interactiveWindowDoc) : undefined; if (activeInteractiveWindowKernel) { @@ -71,7 +68,7 @@ export class NotebookWatcher implements INotebookWatcher { } public get activeNotebookExecutionCount(): number | undefined { - const activeNotebook = getAssociatedNotebookDocument(this.activeKernel); + const activeNotebook = this.activeKernel?.notebook; return activeNotebook ? this._executionCountTracker.get(activeNotebook) : undefined; } @@ -96,10 +93,7 @@ export class NotebookWatcher implements INotebookWatcher { this.notebooks.onDidCloseNotebookDocument(this.notebookEditorClosed, this, this.disposables); this.kernelProvider.onDidRestartKernel( (kernel) => { - const notebook = getAssociatedNotebookDocument(kernel); - if (notebook) { - this.handleRestart({ state: KernelState.restarted, notebook }); - } + this.handleRestart({ state: KernelState.restarted, notebook: kernel.notebook }); }, this, this.disposables @@ -204,6 +198,6 @@ export class NotebookWatcher implements INotebookWatcher { // Check to see if this event was on the active notebook private isActiveNotebookEvent(kernelStateEvent: KernelStateEventArgs): boolean { - return getAssociatedNotebookDocument(this.activeKernel) === kernelStateEvent.notebook; + return this.activeKernel?.notebook === kernelStateEvent.notebook; } } From c0fe4cc55e5ca81fc5ec5f64d8b10ca79787ae42 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Tue, 19 Jul 2022 21:44:05 -0700 Subject: [PATCH 8/8] Unify node/web kernel and remove iw reference in kernel component (#10853) --- TELEMETRY.md | 16 +- .../interactiveWindowDebugger.node.ts | 2 +- .../debugger/startupCodeProvider.ts | 54 +++++++ .../serviceRegistry.node.ts | 7 +- src/interactive-window/serviceRegistry.web.ts | 7 +- src/kernels/kernel.node.ts | 153 ------------------ src/kernels/{kernel.base.ts => kernel.ts} | 40 ++--- src/kernels/kernel.web.ts | 89 ---------- src/kernels/kernelProvider.node.ts | 59 +++++-- src/kernels/kernelProvider.web.ts | 28 ++-- src/kernels/kernelStartupCodeProvider.node.ts | 80 +++++++++ src/kernels/serviceRegistry.node.ts | 10 +- src/kernels/types.ts | 11 ++ src/platform/common/scriptConstants.ts | 14 -- .../installationPrompts.vscode.test.ts | 2 +- .../kernels/kernelProvider.node.unit.test.ts | 7 +- 16 files changed, 254 insertions(+), 325 deletions(-) create mode 100644 src/interactive-window/debugger/startupCodeProvider.ts delete mode 100644 src/kernels/kernel.node.ts rename src/kernels/{kernel.base.ts => kernel.ts} (96%) delete mode 100644 src/kernels/kernel.web.ts create mode 100644 src/kernels/kernelStartupCodeProvider.node.ts delete mode 100644 src/platform/common/scriptConstants.ts diff --git a/TELEMETRY.md b/TELEMETRY.md index 572b4f21c12e..efce2f4affea 100644 --- a/TELEMETRY.md +++ b/TELEMETRY.md @@ -949,7 +949,7 @@ No properties for event ``` -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript } public async executeCell(cell: NotebookCell, codeOverride?: string): Promise { @@ -1908,7 +1908,7 @@ No description provided ## Locations Used -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript await this.executeSilently(session, startupCode, { traceErrors: true, @@ -2848,7 +2848,7 @@ No properties for event ``` -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript await (this._jupyterSessionPromise ? this.kernelExecution.restart(this._jupyterSessionPromise) @@ -2860,7 +2860,7 @@ No properties for event ``` -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript this.restarting = undefined; // If we get a kernel promise failure, then restarting timed out. Just shutdown and restart the entire server. @@ -4729,7 +4729,7 @@ No description provided ## Locations Used -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript await this.executeSilently(session, this.getUserStartupCommands(), { traceErrors: true, @@ -7311,7 +7311,7 @@ No properties for event ## Locations Used -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript // Setup telemetry if (!this.perceivedJupyterStartupTelemetryCaptured) { @@ -7323,7 +7323,7 @@ No properties for event ``` -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript sendKernelTelemetryEvent( @@ -8757,7 +8757,7 @@ No properties for event ## Locations Used -[src/kernels/kernel.base.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.base.ts) +[src/kernels/kernel.ts](https://github.com/microsoft/vscode-jupyter/tree/main/src/kernels/kernel.ts) ```typescript sendTelemetryEvent(Telemetry.PerceivedJupyterStartupNotebook, stopWatch.elapsedTime); executionPromise diff --git a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts index d8eb6f0301ff..1590902a47bf 100644 --- a/src/interactive-window/debugger/interactiveWindowDebugger.node.ts +++ b/src/interactive-window/debugger/interactiveWindowDebugger.node.ts @@ -12,7 +12,7 @@ import { DataScience } from '../../platform/common/utils/localize'; import { Identifiers } from '../../platform/common/constants'; import { Telemetry } from '../../telemetry'; import { JupyterDebuggerNotInstalledError } from '../../kernels/errors/jupyterDebuggerNotInstalledError'; -import { getPlainTextOrStreamOutput } from '../../kernels/kernel.base'; +import { getPlainTextOrStreamOutput } from '../../kernels/kernel'; import { IKernel, isLocalConnection } from '../../kernels/types'; import { IInteractiveWindowDebugger } from '../types'; import { IFileGeneratedCodes } from '../editor-integration/types'; diff --git a/src/interactive-window/debugger/startupCodeProvider.ts b/src/interactive-window/debugger/startupCodeProvider.ts new file mode 100644 index 000000000000..ecf2951bfa31 --- /dev/null +++ b/src/interactive-window/debugger/startupCodeProvider.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IKernel, isLocalConnection, IStartupCodeProvider, StartupCodePriority } from '../../kernels/types'; +import { InteractiveWindowView } from '../../platform/common/constants'; +import { IFileSystem } from '../../platform/common/platform/types'; +import { IConfigurationService, IExtensionContext, IsWebExtension } from '../../platform/common/types'; + +@injectable() +export class InteractiveWindowDebuggingStartupCodeProvider implements IStartupCodeProvider { + public priority = StartupCodePriority.Debugging; + private addRunCellHookContents?: Promise; + + constructor( + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IsWebExtension) private readonly isWebExtension: boolean + ) {} + + async getCode(kernel: IKernel): Promise { + if (!this.isWebExtension) { + const useNewDebugger = this.configService.getSettings(undefined).forceIPyKernelDebugger === true; + if (useNewDebugger) { + return []; + } + if (!isLocalConnection(kernel.kernelConnectionMetadata)) { + return []; + } + } + + if (kernel.notebook?.notebookType === InteractiveWindowView) { + // If using ipykernel 6, we need to set the IPYKERNEL_CELL_NAME so that + // debugging can work. However this code is harmless for IPYKERNEL 5 so just always do it + if (!this.addRunCellHookContents) { + this.addRunCellHookContents = this.fs.readFile( + Uri.joinPath( + this.context.extensionUri, + 'pythonFiles', + 'vscode_datascience_helpers', + 'kernel', + 'addRunCellHook.py' + ) + ); + } + const addRunCellHook = await this.addRunCellHookContents; + + return addRunCellHook.splitLines({ trim: false }); + } + return []; + } +} diff --git a/src/interactive-window/serviceRegistry.node.ts b/src/interactive-window/serviceRegistry.node.ts index 74ad1b16a290..f6acf7902fcb 100644 --- a/src/interactive-window/serviceRegistry.node.ts +++ b/src/interactive-window/serviceRegistry.node.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { ITracebackFormatter } from '../kernels/types'; +import { IStartupCodeProvider, ITracebackFormatter } from '../kernels/types'; import { IExtensionSyncActivationService, IExtensionSingleActivationService } from '../platform/activation/types'; import { IJupyterExtensionBanner } from '../platform/common/types'; import { IServiceManager } from '../platform/ioc/types'; @@ -28,6 +28,7 @@ import { IInteractiveWindowDebugger, IInteractiveWindowDebuggingManager, IIntera import { InteractiveWindowDebugger } from './debugger/interactiveWindowDebugger.node'; import { InteractiveWindowDebuggingManager } from './debugger/jupyter/debuggingManager'; import { BANNER_NAME_INTERACTIVE_SHIFTENTER, InteractiveShiftEnterBanner } from './shiftEnterBanner'; +import { InteractiveWindowDebuggingStartupCodeProvider } from './debugger/startupCodeProvider'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); @@ -64,4 +65,8 @@ export function registerTypes(serviceManager: IServiceManager) { InteractiveShiftEnterBanner, BANNER_NAME_INTERACTIVE_SHIFTENTER ); + serviceManager.addSingleton( + IStartupCodeProvider, + InteractiveWindowDebuggingStartupCodeProvider + ); } diff --git a/src/interactive-window/serviceRegistry.web.ts b/src/interactive-window/serviceRegistry.web.ts index 3c85221e480f..5f0b60ee3018 100644 --- a/src/interactive-window/serviceRegistry.web.ts +++ b/src/interactive-window/serviceRegistry.web.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; -import { ITracebackFormatter } from '../kernels/types'; +import { IStartupCodeProvider, ITracebackFormatter } from '../kernels/types'; import { IExtensionSingleActivationService, IExtensionSyncActivationService } from '../platform/activation/types'; import { IServiceManager } from '../platform/ioc/types'; import { CommandRegistry } from './commands/commandRegistry'; @@ -24,6 +24,7 @@ import { IGeneratedCodeStorageFactory } from './editor-integration/types'; import { GeneratedCodeStorageManager } from './generatedCodeStoreManager'; import { InteractiveWindowTracebackFormatter } from './outputs/tracebackFormatter'; import { InteractiveWindowDebuggingManager } from './debugger/jupyter/debuggingManager'; +import { InteractiveWindowDebuggingStartupCodeProvider } from './debugger/startupCodeProvider'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IInteractiveWindowProvider, InteractiveWindowProvider); @@ -53,4 +54,8 @@ export function registerTypes(serviceManager: IServiceManager) { undefined, [IExtensionSingleActivationService] ); + serviceManager.addSingleton( + IStartupCodeProvider, + InteractiveWindowDebuggingStartupCodeProvider + ); } diff --git a/src/kernels/kernel.node.ts b/src/kernels/kernel.node.ts deleted file mode 100644 index 60f4db377ea6..000000000000 --- a/src/kernels/kernel.node.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import { NotebookController, NotebookDocument, Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../platform/common/application/types'; -import { CodeSnippets, InteractiveWindowView } from '../platform/common/constants'; -import { traceInfo, traceError } from '../platform/logging'; -import { IPythonExecutionFactory } from '../platform/common/process/types.node'; -import { Resource, IConfigurationService, IExtensionContext } from '../platform/common/types'; -import { calculateWorkingDirectory } from '../platform/common/utils.node'; -import { isLocalHostConnection, isPythonKernelConnection } from './helpers'; -import { expandWorkingDir } from './jupyter/jupyterUtils'; -import { - INotebookProvider, - isLocalConnection, - ITracebackFormatter, - KernelActionSource, - KernelConnectionMetadata -} from './types'; -import { AddRunCellHook } from '../platform/common/scriptConstants'; -import { IStatusProvider } from '../platform/progress/types'; -import { sendTelemetryForPythonKernelExecutable } from './helpers.node'; -import { BaseKernel } from './kernel.base'; -import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; -import { getFilePath } from '../platform/common/platform/fs-paths'; -import { IFileSystem } from '../platform/common/platform/types'; - -/** - * Node specific changes for a BaseKernel. - */ -export class Kernel extends BaseKernel { - constructor( - uri: Uri, - resourceUri: Resource, - notebook: NotebookDocument | undefined, - kernelConnectionMetadata: Readonly, - notebookProvider: INotebookProvider, - launchTimeout: number, - interruptTimeout: number, - appShell: IApplicationShell, - private readonly fs: IFileSystem, - controller: NotebookController, - configService: IConfigurationService, - outputTracker: CellOutputDisplayIdTracker, - workspaceService: IWorkspaceService, - private readonly pythonExecutionFactory: IPythonExecutionFactory, - statusProvider: IStatusProvider, - creator: KernelActionSource, - context: IExtensionContext, - formatters: ITracebackFormatter[] - ) { - super( - uri, - resourceUri, - notebook, - kernelConnectionMetadata, - notebookProvider, - launchTimeout, - interruptTimeout, - appShell, - controller, - configService, - workspaceService, - outputTracker, - statusProvider, - creator, - context, - formatters - ); - } - - protected async getDebugCellHook(): Promise { - const useNewDebugger = this.configService.getSettings(undefined).forceIPyKernelDebugger === true; - if (useNewDebugger) { - return []; - } - if (!isLocalConnection(this.kernelConnectionMetadata)) { - return []; - } - // Only do this for interactive windows. IPYKERNEL_CELL_NAME is set other ways in - // notebooks - if (this.notebook?.notebookType === InteractiveWindowView) { - // If using ipykernel 6, we need to set the IPYKERNEL_CELL_NAME so that - // debugging can work. However this code is harmless for IPYKERNEL 5 so just always do it - const scriptPath = AddRunCellHook.getScriptPath(this.context); - if (await this.fs.exists(scriptPath)) { - const fileContents = await this.fs.readFile(scriptPath); - return fileContents.splitLines({ trim: false }); - } - traceError(`Cannot run non-existent script file: ${scriptPath}`); - } - return []; - } - - protected async getUpdateWorkingDirectoryAndPathCode(launchingFile?: Resource): Promise { - if ( - (isLocalConnection(this.kernelConnectionMetadata) || - isLocalHostConnection(this.kernelConnectionMetadata)) && - this.kernelConnectionMetadata.kind !== 'connectToLiveRemoteKernel' // Skip for live kernel. Don't change current directory on a kernel that's already running - ) { - let suggestedDir = await calculateWorkingDirectory( - this.configService, - this.workspaceService, - this.fs, - launchingFile - ); - if (suggestedDir && (await this.fs.exists(suggestedDir))) { - traceInfo('UpdateWorkingDirectoryAndPath in Kernel'); - // We should use the launch info directory. It trumps the possible dir - return this.getChangeDirectoryCode(suggestedDir); - } else if (launchingFile && (await this.fs.exists(launchingFile))) { - // Combine the working directory with this file if possible. - suggestedDir = Uri.file( - expandWorkingDir( - getFilePath(suggestedDir), - launchingFile, - this.workspaceService, - this.configService.getSettings(launchingFile) - ) - ); - if (suggestedDir && (await this.fs.exists(suggestedDir))) { - traceInfo('UpdateWorkingDirectoryAndPath in Kernel'); - return this.getChangeDirectoryCode(suggestedDir); - } - } - } - return []; - } - - // Update both current working directory and sys.path with the desired directory - private getChangeDirectoryCode(directory: Uri): string[] { - if ( - (isLocalConnection(this.kernelConnectionMetadata) || - isLocalHostConnection(this.kernelConnectionMetadata)) && - isPythonKernelConnection(this.kernelConnectionMetadata) - ) { - return CodeSnippets.UpdateCWDAndPath.format(getFilePath(directory)).splitLines({ trim: false }); - } - return []; - } - - protected override async sendTelemetryForPythonKernelExecutable() { - if (this.session) { - return sendTelemetryForPythonKernelExecutable( - this.session, - this.resourceUri, - this.kernelConnectionMetadata, - this.pythonExecutionFactory - ); - } - } -} diff --git a/src/kernels/kernel.base.ts b/src/kernels/kernel.ts similarity index 96% rename from src/kernels/kernel.base.ts rename to src/kernels/kernel.ts index 224310913259..bc26d8385b23 100644 --- a/src/kernels/kernel.base.ts +++ b/src/kernels/kernel.ts @@ -49,6 +49,7 @@ import { INotebookProvider, InterruptResult, isLocalConnection, + IStartupCodeProvider, ITracebackFormatter, KernelActionSource, KernelConnectionMetadata, @@ -67,7 +68,7 @@ import { KernelExecution } from './execution/kernelExecution'; /** * Represents an active kernel process running on the jupyter (or local) machine. */ -export abstract class BaseKernel implements IBaseKernel { +export class Kernel implements IBaseKernel { private readonly disposables: IDisposable[] = []; get onStatusChanged(): Event { return this._onStatusChanged.event; @@ -151,12 +152,14 @@ export abstract class BaseKernel implements IBaseKernel { protected readonly appShell: IApplicationShell, public readonly controller: NotebookController, protected readonly configService: IConfigurationService, - protected readonly workspaceService: IWorkspaceService, outputTracker: CellOutputDisplayIdTracker, + protected readonly workspaceService: IWorkspaceService, private readonly statusProvider: IStatusProvider, public readonly creator: KernelActionSource, - protected readonly context: IExtensionContext, - formatters: ITracebackFormatter[] + context: IExtensionContext, + formatters: ITracebackFormatter[], + private readonly startupCodeProviders: IStartupCodeProvider[], + private readonly sendTelemetryForPythonKernelExecutable: () => Promise ) { this.kernelExecution = new KernelExecution( this, @@ -493,8 +496,6 @@ export abstract class BaseKernel implements IBaseKernel { } } - protected abstract sendTelemetryForPythonKernelExecutable(): Promise; - protected async initializeAfterStart(session: IKernelConnectionSession | undefined) { traceVerbose(`Started running kernel initialization for ${getDisplayPath(this.uri)}`); if (!session) { @@ -614,24 +615,15 @@ export abstract class BaseKernel implements IBaseKernel { // Gather all of the startup code into a giant string array so we // can execute it all at once. const result: string[] = []; - if (isPythonKernelConnection(this.kernelConnectionMetadata)) { - const [changeDirScripts, debugCellScripts] = await Promise.all([ - // Change our initial directory and path - this.getUpdateWorkingDirectoryAndPathCode(this.resourceUri), - // Initialize debug cell support. - // (IPYKERNEL_CELL_NAME has to be set on every cell execution, but we can't execute a cell to change it) - this.getDebugCellHook() - ]); - - // Have our debug cell script run first for safety - if ( - isLocalConnection(this.kernelConnectionMetadata) && - !this.configService.getSettings(undefined).forceIPyKernelDebugger - ) { - result.push(...debugCellScripts); + const dirs = await Promise.all( + this.startupCodeProviders + .sort((a, b) => b.priority - a.priority) + .map((provider) => provider.getCode(this)) + ); + for (let dir of dirs) { + result.push(...dir); } - result.push(...changeDirScripts); // Set the ipynb file const file = getFilePath(this.resourceUri); @@ -706,8 +698,6 @@ export abstract class BaseKernel implements IBaseKernel { return results; } - protected abstract getDebugCellHook(): Promise; - private getUserStartupCommands(): string[] { const settings = this.configService.getSettings(this.resourceUri); // Run any startup commands that we specified. Support the old form too @@ -726,8 +716,6 @@ export abstract class BaseKernel implements IBaseKernel { return []; } - protected abstract getUpdateWorkingDirectoryAndPathCode(launchingFile?: Resource): Promise; - private async executeSilently( session: IKernelConnectionSession | undefined, code: string[], diff --git a/src/kernels/kernel.web.ts b/src/kernels/kernel.web.ts deleted file mode 100644 index 0b14d1a309c1..000000000000 --- a/src/kernels/kernel.web.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import { NotebookController, NotebookDocument, Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../platform/common/application/types'; -import { Resource, IConfigurationService, IExtensionContext } from '../platform/common/types'; -import { INotebookProvider, ITracebackFormatter, KernelActionSource, KernelConnectionMetadata } from './types'; -import { BaseKernel } from './kernel.base'; -import { IStatusProvider } from '../platform/progress/types'; -import { InteractiveWindowView } from '../platform/common/constants'; -import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; -import { IFileSystem } from '../platform/common/platform/types'; - -/** - * This class is just a stand in for now. It will connect to kernels in the web when this is finished. - * For now it's just here to get the service container to load. - */ -export class Kernel extends BaseKernel { - private addRunCellHookContents?: Promise; - constructor( - id: Uri, - resourceUri: Resource, - notebook: NotebookDocument | undefined, - kernelConnectionMetadata: Readonly, - notebookProvider: INotebookProvider, - launchTimeout: number, - interruptTimeout: number, - appShell: IApplicationShell, - controller: NotebookController, - configService: IConfigurationService, - outputTracker: CellOutputDisplayIdTracker, - workspaceService: IWorkspaceService, - statusProvider: IStatusProvider, - creator: KernelActionSource, - context: IExtensionContext, - formatters: ITracebackFormatter[], - private readonly fs: IFileSystem - ) { - super( - id, - resourceUri, - notebook, - kernelConnectionMetadata, - notebookProvider, - launchTimeout, - interruptTimeout, - appShell, - controller, - configService, - workspaceService, - outputTracker, - statusProvider, - creator, - context, - formatters - ); - } - - protected async getDebugCellHook(): Promise { - if (this.notebook?.notebookType === InteractiveWindowView) { - // If using ipykernel 6, we need to set the IPYKERNEL_CELL_NAME so that - // debugging can work. However this code is harmless for IPYKERNEL 5 so just always do it - if (!this.addRunCellHookContents) { - this.addRunCellHookContents = this.fs.readFile( - Uri.joinPath( - this.context.extensionUri, - 'pythonFiles', - 'vscode_datascience_helpers', - 'kernel', - 'addRunCellHook.py' - ) - ); - } - const addRunCellHook = await this.addRunCellHookContents; - return addRunCellHook.splitLines({ trim: false }); - } - return []; - } - - protected async getUpdateWorkingDirectoryAndPathCode(_launchingFile?: Resource): Promise { - // Not supported on web - return []; - } - - protected override async sendTelemetryForPythonKernelExecutable() { - // Does nothing at the moment - } -} diff --git a/src/kernels/kernelProvider.node.ts b/src/kernels/kernelProvider.node.ts index 885e76a254e0..5e759222c203 100644 --- a/src/kernels/kernelProvider.node.ts +++ b/src/kernels/kernelProvider.node.ts @@ -8,17 +8,24 @@ import { IApplicationShell, IWorkspaceService, IVSCodeNotebook } from '../platfo import { IPythonExecutionFactory } from '../platform/common/process/types.node'; import { IAsyncDisposableRegistry, - IDisposableRegistry, IConfigurationService, + IDisposableRegistry, IExtensionContext } from '../platform/common/types'; -import { Kernel } from './kernel.node'; -import { IBaseKernel, IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; import { IStatusProvider } from '../platform/progress/types'; import { BaseCoreKernelProvider, BaseThirdPartyKernelProvider } from './kernelProvider.base'; import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; -import { IFileSystem } from '../platform/common/platform/types'; +import { sendTelemetryForPythonKernelExecutable } from './helpers.node'; +import { Kernel } from './kernel'; +import { + IBaseKernel, + IKernel, + INotebookProvider, + IStartupCodeProvider, + ITracebackFormatter, + KernelOptions +} from './types'; /** * Node version of a kernel provider. Needed in order to create the node version of a kernel. @@ -31,14 +38,14 @@ export class KernelProvider extends BaseCoreKernelProvider { @inject(INotebookProvider) private notebookProvider: INotebookProvider, @inject(IConfigurationService) private configService: IConfigurationService, @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IFileSystem) private readonly fs: IFileSystem, @inject(CellOutputDisplayIdTracker) private readonly outputTracker: CellOutputDisplayIdTracker, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IVSCodeNotebook) notebook: IVSCodeNotebook, @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, @inject(IExtensionContext) private readonly context: IExtensionContext, - @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[] + @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[], + @multiInject(IStartupCodeProvider) private readonly startupCodeProviders: IStartupCodeProvider[] ) { super(asyncDisposables, disposables, notebook); } @@ -63,16 +70,27 @@ export class KernelProvider extends BaseCoreKernelProvider { waitForIdleTimeout, interruptTimeout, this.appShell, - this.fs, options.controller, this.configService, this.outputTracker, this.workspaceService, - this.pythonExecutionFactory, this.statusProvider, options.creator, this.context, - this.formatters + this.formatters, + this.startupCodeProviders, + () => { + if (kernel.session) { + return sendTelemetryForPythonKernelExecutable( + kernel.session, + kernel.resourceUri, + kernel.kernelConnectionMetadata, + this.pythonExecutionFactory + ); + } else { + return Promise.resolve(); + } + } ) as IKernel; kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); @@ -97,14 +115,14 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { @inject(INotebookProvider) private notebookProvider: INotebookProvider, @inject(IConfigurationService) private configService: IConfigurationService, @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IFileSystem) private readonly fs: IFileSystem, @inject(CellOutputDisplayIdTracker) private readonly outputTracker: CellOutputDisplayIdTracker, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IVSCodeNotebook) notebook: IVSCodeNotebook, @inject(IPythonExecutionFactory) private readonly pythonExecutionFactory: IPythonExecutionFactory, @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, @inject(IExtensionContext) private readonly context: IExtensionContext, - @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[] + @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[], + @multiInject(IStartupCodeProvider) private readonly startupCodeProviders: IStartupCodeProvider[] ) { super(asyncDisposables, disposables, notebook); } @@ -120,7 +138,7 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { const resourceUri = uri; const waitForIdleTimeout = this.configService.getSettings(resourceUri).jupyterLaunchTimeout; const interruptTimeout = this.configService.getSettings(resourceUri).jupyterInterruptTimeout; - const kernel = new Kernel( + let kernel: Kernel = new Kernel( uri, resourceUri, undefined, @@ -129,16 +147,27 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { waitForIdleTimeout, interruptTimeout, this.appShell, - this.fs, options.controller, this.configService, this.outputTracker, this.workspaceService, - this.pythonExecutionFactory, this.statusProvider, options.creator, this.context, - this.formatters + this.formatters, + this.startupCodeProviders, + () => { + if (kernel.session) { + return sendTelemetryForPythonKernelExecutable( + kernel.session, + kernel.resourceUri, + kernel.kernelConnectionMetadata, + this.pythonExecutionFactory + ); + } else { + return Promise.resolve(); + } + } ); kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); diff --git a/src/kernels/kernelProvider.web.ts b/src/kernels/kernelProvider.web.ts index 9471880ec97e..617cd2a913e6 100644 --- a/src/kernels/kernelProvider.web.ts +++ b/src/kernels/kernelProvider.web.ts @@ -3,21 +3,27 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; +import { IApplicationShell, IVSCodeNotebook, IWorkspaceService } from '../platform/common/application/types'; +import { InteractiveWindowView } from '../platform/common/constants'; import { NotebookDocument, Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService, IVSCodeNotebook } from '../platform/common/application/types'; import { IAsyncDisposableRegistry, - IDisposableRegistry, IConfigurationService, + IDisposableRegistry, IExtensionContext } from '../platform/common/types'; -import { Kernel } from './kernel.web'; -import { IBaseKernel, IKernel, INotebookProvider, ITracebackFormatter, KernelOptions } from './types'; import { BaseCoreKernelProvider, BaseThirdPartyKernelProvider } from './kernelProvider.base'; import { IStatusProvider } from '../platform/progress/types'; -import { InteractiveWindowView } from '../platform/common/constants'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; -import { IFileSystem } from '../platform/common/platform/types'; +import { Kernel } from './kernel'; +import { + IBaseKernel, + IKernel, + INotebookProvider, + IStartupCodeProvider, + ITracebackFormatter, + KernelOptions +} from './types'; /** * Web version of a kernel provider. Needed in order to create the web version of a kernel. @@ -36,7 +42,7 @@ export class KernelProvider extends BaseCoreKernelProvider { @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, @inject(IExtensionContext) private readonly context: IExtensionContext, @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[], - @inject(IFileSystem) private readonly fs: IFileSystem + @multiInject(IStartupCodeProvider) private readonly startupCodeProviders: IStartupCodeProvider[] ) { super(asyncDisposables, disposables, notebook); } @@ -69,7 +75,8 @@ export class KernelProvider extends BaseCoreKernelProvider { options.creator, this.context, this.formatters, - this.fs + this.startupCodeProviders, + () => Promise.resolve() ) as IKernel; kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); @@ -101,7 +108,7 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { @inject(IStatusProvider) private readonly statusProvider: IStatusProvider, @inject(IExtensionContext) private readonly context: IExtensionContext, @multiInject(ITracebackFormatter) private readonly formatters: ITracebackFormatter[], - @inject(IFileSystem) private readonly fs: IFileSystem + @multiInject(IStartupCodeProvider) private readonly startupCodeProviders: IStartupCodeProvider[] ) { super(asyncDisposables, disposables, notebook); } @@ -133,7 +140,8 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { options.creator, this.context, this.formatters, - this.fs + this.startupCodeProviders, + () => Promise.resolve() ); kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); diff --git a/src/kernels/kernelStartupCodeProvider.node.ts b/src/kernels/kernelStartupCodeProvider.node.ts new file mode 100644 index 000000000000..c91912fe6cd1 --- /dev/null +++ b/src/kernels/kernelStartupCodeProvider.node.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../platform/common/application/types'; +import { CodeSnippets } from '../platform/common/constants'; +import { getFilePath } from '../platform/common/platform/fs-paths'; +import { IFileSystem } from '../platform/common/platform/types'; +import { IConfigurationService } from '../platform/common/types'; +import { calculateWorkingDirectory } from '../platform/common/utils.node'; +import { traceInfo } from '../platform/logging'; +import { isLocalHostConnection, isPythonKernelConnection } from './helpers'; +import { expandWorkingDir } from './jupyter/jupyterUtils'; +import { IKernel, isLocalConnection, IStartupCodeProvider, StartupCodePriority } from './types'; + +@injectable() +export class KernelStartupCodeProvider implements IStartupCodeProvider { + public priority = StartupCodePriority.Base; + + constructor( + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + ) {} + + async getCode(kernel: IKernel): Promise { + if ( + !( + isLocalConnection(kernel.kernelConnectionMetadata) && + !this.configService.getSettings(undefined).forceIPyKernelDebugger + ) + ) { + return []; + } + + if ( + isLocalConnection(kernel.kernelConnectionMetadata) || + isLocalHostConnection(kernel.kernelConnectionMetadata) + ) { + let suggestedDir = await calculateWorkingDirectory( + this.configService, + this.workspaceService, + this.fs, + kernel.resourceUri + ); + if (suggestedDir && (await this.fs.exists(suggestedDir))) { + traceInfo('UpdateWorkingDirectoryAndPath in Kernel'); + // We should use the launch info directory. It trumps the possible dir + return this.getChangeDirectoryCode(kernel, suggestedDir); + } else if (kernel.resourceUri && (await this.fs.exists(kernel.resourceUri))) { + // Combine the working directory with this file if possible. + suggestedDir = Uri.file( + expandWorkingDir( + getFilePath(suggestedDir), + kernel.resourceUri, + this.workspaceService, + this.configService.getSettings(kernel.resourceUri) + ) + ); + if (suggestedDir && (await this.fs.exists(suggestedDir))) { + traceInfo('UpdateWorkingDirectoryAndPath in Kernel'); + return this.getChangeDirectoryCode(kernel, suggestedDir); + } + } + } + return []; + } + + private getChangeDirectoryCode(kernel: IKernel, directory: Uri): string[] { + if ( + (isLocalConnection(kernel.kernelConnectionMetadata) || + isLocalHostConnection(kernel.kernelConnectionMetadata)) && + isPythonKernelConnection(kernel.kernelConnectionMetadata) + ) { + return CodeSnippets.UpdateCWDAndPath.format(getFilePath(directory)).splitLines({ trim: false }); + } + return []; + } +} diff --git a/src/kernels/serviceRegistry.node.ts b/src/kernels/serviceRegistry.node.ts index 3740136b3873..cb4b22b96dae 100644 --- a/src/kernels/serviceRegistry.node.ts +++ b/src/kernels/serviceRegistry.node.ts @@ -30,7 +30,13 @@ import { PreWarmActivatedJupyterEnvironmentVariables } from './variables/preWarm import { PythonVariablesRequester } from './variables/pythonVariableRequester'; import { MultiplexingDebugService } from './debugger/multiplexingDebugService'; import { IDebugLocationTracker, IDebugLocationTrackerFactory, IJupyterDebugService } from './debugger/types'; -import { IKernelDependencyService, IKernelFinder, IKernelProvider, IThirdPartyKernelProvider } from './types'; +import { + IKernelDependencyService, + IKernelFinder, + IKernelProvider, + IStartupCodeProvider, + IThirdPartyKernelProvider +} from './types'; import { IJupyterVariables, IKernelVariableRequester } from './variables/types'; import { KernelCrashMonitor } from './kernelCrashMonitor'; import { KernelAutoRestartMonitor } from './kernelAutoRestartMonitor.node'; @@ -43,6 +49,7 @@ import { DebugLocationTrackerFactory } from './debugger/debugLocationTrackerFact import { Activation } from './activation.node'; import { PortAttributesProviders } from './port/portAttributeProvider.node'; import { IServerConnectionType } from './jupyter/types'; +import { KernelStartupCodeProvider } from './kernelStartupCodeProvider.node'; export function registerTypes(serviceManager: IServiceManager, isDevMode: boolean) { serviceManager.addSingleton(IExtensionSingleActivationService, Activation); @@ -138,4 +145,5 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea serviceManager.addSingleton(IDebugLocationTracker, DebugLocationTrackerFactory, undefined, [ IDebugLocationTrackerFactory ]); + serviceManager.addSingleton(IStartupCodeProvider, KernelStartupCodeProvider); } diff --git a/src/kernels/types.ts b/src/kernels/types.ts index 167d695944c6..3a5befb56a3b 100644 --- a/src/kernels/types.ts +++ b/src/kernels/types.ts @@ -616,3 +616,14 @@ export interface ITracebackFormatter { */ format(cell: NotebookCell, traceback: string[]): string[]; } + +export const enum StartupCodePriority { + Base = 0, + Debugging = 5 +} + +export const IStartupCodeProvider = Symbol('IStartupCodeProvider'); +export interface IStartupCodeProvider { + priority: StartupCodePriority; + getCode(kernel: IBaseKernel): Promise; +} diff --git a/src/platform/common/scriptConstants.ts b/src/platform/common/scriptConstants.ts deleted file mode 100644 index c04c327d24c2..000000000000 --- a/src/platform/common/scriptConstants.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { joinPath } from '../vscode-path/resources'; -import { IExtensionContext } from './types'; - -export namespace AddRunCellHook { - export function getScriptPath(context: IExtensionContext) { - return joinPath( - context.extensionUri, - 'pythonFiles', - 'vscode_datascience_helpers', - 'kernel', - 'addRunCellHook.py' - ); - } -} diff --git a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts index dfae61024c2e..b1603fc7e3bb 100644 --- a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts +++ b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts @@ -9,7 +9,7 @@ import * as sinon from 'sinon'; import { commands, Memento, workspace, window, Uri, NotebookCell, NotebookDocument, NotebookCellKind } from 'vscode'; import { IPythonApiProvider } from '../../../../platform/api/types'; import { ICommandManager, IVSCodeNotebook } from '../../../../platform/common/application/types'; -import { Kernel } from '../../../../platform/../kernels/kernel.node'; +import { Kernel } from '../../../../platform/../kernels/kernel'; import { getDisplayPath } from '../../../../platform/common/platform/fs-paths'; import { GLOBAL_MEMENTO, diff --git a/src/test/kernels/kernelProvider.node.unit.test.ts b/src/test/kernels/kernelProvider.node.unit.test.ts index 4f9d715fa3a8..db355e9f5799 100644 --- a/src/test/kernels/kernelProvider.node.unit.test.ts +++ b/src/test/kernels/kernelProvider.node.unit.test.ts @@ -17,7 +17,6 @@ import { IApplicationShell, IVSCodeNotebook, IWorkspaceService } from '../../pla import { AsyncDisposableRegistry } from '../../platform/common/asyncDisposableRegistry'; import { JupyterNotebookView } from '../../platform/common/constants'; import { disposeAllDisposables } from '../../platform/common/helpers'; -import { IFileSystemNode } from '../../platform/common/platform/types.node'; import { IPythonExecutionFactory } from '../../platform/common/process/types.node'; import { IConfigurationService, @@ -41,7 +40,6 @@ suite('KernelProvider Node', () => { let workspaceService: IWorkspaceService; let vscNotebook: IVSCodeNotebook; let statusProvider: IStatusProvider; - let fs: IFileSystemNode; let pythonExecFactory: IPythonExecutionFactory; let context: IExtensionContext; let onDidCloseNotebookDocument: EventEmitter; @@ -78,7 +76,6 @@ suite('KernelProvider Node', () => { vscNotebook = mock(); statusProvider = mock(); context = mock(); - fs = mock(); pythonExecFactory = mock(); const configSettings = mock(); when(vscNotebook.onDidCloseNotebookDocument).thenReturn(onDidCloseNotebookDocument.event); @@ -95,13 +92,13 @@ suite('KernelProvider Node', () => { instance(notebookProvider), instance(configService), instance(appShell), - instance(fs), instance(outputTracker), instance(workspaceService), instance(vscNotebook), instance(pythonExecFactory), instance(statusProvider), instance(context), + [], [] ); thirdPartyKernelProvider = new ThirdPartyKernelProvider( @@ -110,13 +107,13 @@ suite('KernelProvider Node', () => { instance(notebookProvider), instance(configService), instance(appShell), - instance(fs), instance(outputTracker), instance(workspaceService), instance(vscNotebook), instance(pythonExecFactory), instance(statusProvider), instance(context), + [], [] ); });