Skip to content

Commit 96da8f1

Browse files
authored
Use separate class to manage kernel dependencies (#11472)
Use a single class that manages dependencies in kernels. I.e. move both of the following into its own class. * Is ipykernel installed * Install ipykernel if required
1 parent 47b722a commit 96da8f1

14 files changed

+250
-76
lines changed

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,5 +488,5 @@
488488
"DataScience.unhandledMessage": "Unhandled kernel message from a widget: {0} : {1}",
489489
"DataScience.qgridWidgetScriptVersionCompatibilityWarning": "Unable to load a compatible version of the widget 'qgrid'. Consider downgrading to version 1.1.1.",
490490
"DataScience.kernelStarted": "Started kernel {0}",
491-
"DataScience.ipykernelNotInstalled": "Ipykernel is not installed."
491+
"DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter": "Data Science library {1} is not installed in interpreter {0}. Install?"
492492
}

src/client/common/process/baseDaemon.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
StdErrError
2222
} from './types';
2323

24-
type ErrorResponse = { error?: string };
25-
24+
export type ErrorResponse = { error?: string };
25+
export type ExecResponse = ErrorResponse & { stdout: string; stderr?: string };
2626
export class ConnectionClosedError extends Error {
2727
constructor(public readonly message: string) {
2828
super();
@@ -181,7 +181,6 @@ export abstract class BasePythonDaemon {
181181
): ObservableExecutionResult<string> {
182182
const subject = new Subject<Output<string>>();
183183
const start = async () => {
184-
type ExecResponse = ErrorResponse & { stdout: string; stderr?: string };
185184
let response: ExecResponse;
186185
if ('fileName' in moduleOrFile) {
187186
const request = new RequestType<
@@ -282,7 +281,6 @@ export abstract class BasePythonDaemon {
282281
args: string[],
283282
options: SpawnOptions
284283
): Promise<ExecutionResult<string>> {
285-
type ExecResponse = ErrorResponse & { stdout: string; stderr?: string };
286284
const request = new RequestType<
287285
// tslint:disable-next-line: no-any
288286
{ file_name: string; args: string[]; cwd?: string; env?: any },
@@ -304,7 +302,6 @@ export abstract class BasePythonDaemon {
304302
args: string[],
305303
options: SpawnOptions
306304
): Promise<ExecutionResult<string>> {
307-
type ExecResponse = ErrorResponse & { stdout: string; stderr?: string };
308305
const request = new RequestType<
309306
// tslint:disable-next-line: no-any
310307
{ module_name: string; args: string[]; cwd?: string; env?: any },

src/client/common/utils/localize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ export namespace DataScience {
350350
'DataScience.libraryRequiredToLaunchJupyterNotInstalledInterpreter',
351351
'Data Science library {1} is not installed in interpreter {0}.'
352352
);
353+
export const libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter = localize(
354+
'DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter',
355+
'Data Science library {1} is not installed in interpreter {0}. Install?'
356+
);
353357
export const librariesRequiredToLaunchJupyterNotInstalledInterpreter = localize(
354358
'DataScience.librariesRequiredToLaunchJupyterNotInstalledInterpreter',
355359
'Data Science libraries {1} are not installed in interpreter {0}.'
@@ -920,7 +924,6 @@ export namespace DataScience {
920924
);
921925

922926
export const kernelStarted = localize('DataScience.kernelStarted', 'Started kernel {0}.');
923-
export const ipykernelNotInstalled = localize('DataScience.ipykernelNotInstalled', 'Ipykernel is not installed.');
924927
}
925928

926929
export namespace DebugConfigStrings {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { CancellationToken } from 'vscode';
8+
import { IApplicationShell } from '../../../common/application/types';
9+
import { createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation';
10+
import { ProductNames } from '../../../common/installer/productNames';
11+
import { IInstaller, InstallerResponse, Product } from '../../../common/types';
12+
import { Common, DataScience } from '../../../common/utils/localize';
13+
import { PythonInterpreter } from '../../../interpreter/contracts';
14+
import { IKernelDependencyService, KernelInterpreterDependencyResponse } from '../../types';
15+
16+
/**
17+
* Responsible for managing dependencies of a Python interpreter required to run as a Jupyter Kernel.
18+
* If required modules aren't installed, will prompt user to install them.
19+
*/
20+
@injectable()
21+
export class KernelDependencyService implements IKernelDependencyService {
22+
constructor(
23+
@inject(IApplicationShell) private readonly appShell: IApplicationShell,
24+
@inject(IInstaller) private readonly installer: IInstaller
25+
) {}
26+
/**
27+
* Configures the python interpreter to ensure it can run a Jupyter Kernel by installing any missing dependencies.
28+
* If user opts not to install they can opt to select another interpreter.
29+
*/
30+
public async installMissingDependencies(
31+
interpreter: PythonInterpreter,
32+
token?: CancellationToken
33+
): Promise<KernelInterpreterDependencyResponse> {
34+
if (await this.areDependenciesInstalled(interpreter, token)) {
35+
return KernelInterpreterDependencyResponse.ok;
36+
}
37+
38+
const promptCancellationPromise = createPromiseFromCancellation({
39+
cancelAction: 'resolve',
40+
defaultValue: undefined,
41+
token
42+
});
43+
const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter().format(
44+
interpreter.displayName || interpreter.envName || interpreter.path,
45+
ProductNames.get(Product.ipykernel)!
46+
);
47+
const installerToken = wrapCancellationTokens(token);
48+
const selection = await Promise.race([
49+
this.appShell.showErrorMessage(message, Common.ok(), Common.cancel()),
50+
promptCancellationPromise
51+
]);
52+
if (installerToken.isCancellationRequested) {
53+
return KernelInterpreterDependencyResponse.cancel;
54+
}
55+
56+
if (selection === Common.ok()) {
57+
const cancellatonPromise = createPromiseFromCancellation({
58+
cancelAction: 'resolve',
59+
defaultValue: InstallerResponse.Ignore,
60+
token
61+
});
62+
// Always pass a cancellation token to `install`, to ensure it waits until the module is installed.
63+
const response = await Promise.race([
64+
this.installer.install(Product.ipykernel, interpreter, installerToken),
65+
cancellatonPromise
66+
]);
67+
if (response === InstallerResponse.Installed) {
68+
return KernelInterpreterDependencyResponse.ok;
69+
}
70+
}
71+
return KernelInterpreterDependencyResponse.cancel;
72+
}
73+
public areDependenciesInstalled(interpreter: PythonInterpreter, _token?: CancellationToken): Promise<boolean> {
74+
return this.installer.isInstalled(Product.ipykernel, interpreter).then((installed) => installed === true);
75+
}
76+
}

src/client/datascience/jupyter/kernels/kernelSelector.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CancellationToken } from 'vscode-jsonrpc';
99

1010
import { IApplicationShell } from '../../../common/application/types';
1111
import { traceError, traceInfo, traceVerbose } from '../../../common/logger';
12-
import { IInstaller, Product, Resource } from '../../../common/types';
12+
import { Resource } from '../../../common/types';
1313
import * as localize from '../../../common/utils/localize';
1414
import { noop } from '../../../common/utils/misc';
1515
import { StopWatch } from '../../../common/utils/stopWatch';
@@ -18,7 +18,7 @@ import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../../telemetr
1818
import { KnownNotebookLanguages, Telemetry } from '../../constants';
1919
import { reportAction } from '../../progress/decorator';
2020
import { ReportableAction } from '../../progress/types';
21-
import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types';
21+
import { IJupyterKernelSpec, IJupyterSessionManager, IKernelDependencyService } from '../../types';
2222
import { KernelSelectionProvider } from './kernelSelections';
2323
import { KernelService } from './kernelService';
2424
import { IKernelSpecQuickPickItem, LiveKernelModel } from './types';
@@ -57,7 +57,7 @@ export class KernelSelector {
5757
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
5858
@inject(KernelService) private readonly kernelService: KernelService,
5959
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
60-
@inject(IInstaller) private readonly installer: IInstaller
60+
@inject(IKernelDependencyService) private readonly kernelDepdencyService: IKernelDependencyService
6161
) {}
6262

6363
/**
@@ -378,7 +378,7 @@ export class KernelSelector {
378378
): Promise<KernelSpecInterpreter> {
379379
let kernelSpec: IJupyterKernelSpec | undefined;
380380

381-
if (await this.installer.isInstalled(Product.ipykernel, interpreter)) {
381+
if (await this.kernelDepdencyService.areDependenciesInstalled(interpreter, cancelToken)) {
382382
// Find the kernel associated with this interpter.
383383
kernelSpec = await this.kernelService.findMatchingKernelSpec(interpreter, session, cancelToken);
384384

src/client/datascience/jupyter/kernels/kernelService.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import '../../../common/extensions';
1515
import { traceDecorators, traceError, traceInfo, traceVerbose, traceWarning } from '../../../common/logger';
1616
import { IFileSystem } from '../../../common/platform/types';
1717
import { IPythonExecutionFactory } from '../../../common/process/types';
18-
import { IInstaller, InstallerResponse, Product, ReadWrite } from '../../../common/types';
18+
import { ReadWrite } from '../../../common/types';
1919
import { sleep } from '../../../common/utils/async';
2020
import { noop } from '../../../common/utils/misc';
2121
import { IEnvironmentActivationService } from '../../../interpreter/activation/types';
@@ -24,7 +24,13 @@ import { captureTelemetry, sendTelemetryEvent } from '../../../telemetry';
2424
import { Telemetry } from '../../constants';
2525
import { reportAction } from '../../progress/decorator';
2626
import { ReportableAction } from '../../progress/types';
27-
import { IJupyterKernelSpec, IJupyterSessionManager, IJupyterSubCommandExecutionService } from '../../types';
27+
import {
28+
IJupyterKernelSpec,
29+
IJupyterSessionManager,
30+
IJupyterSubCommandExecutionService,
31+
IKernelDependencyService,
32+
KernelInterpreterDependencyResponse
33+
} from '../../types';
2834
import { JupyterKernelSpec } from './jupyterKernelSpec';
2935
import { LiveKernelModel } from './types';
3036

@@ -61,7 +67,7 @@ export class KernelService {
6167
private readonly jupyterInterpreterExecService: IJupyterSubCommandExecutionService,
6268
@inject(IPythonExecutionFactory) private readonly execFactory: IPythonExecutionFactory,
6369
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
64-
@inject(IInstaller) private readonly installer: IInstaller,
70+
@inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService,
6571
@inject(IFileSystem) private readonly fileSystem: IFileSystem,
6672
@inject(IEnvironmentActivationService) private readonly activationHelper: IEnvironmentActivationService
6773
) {}
@@ -305,15 +311,14 @@ export class KernelService {
305311
execServicePromise.ignoreErrors();
306312
const name = this.generateKernelNameForIntepreter(interpreter);
307313
// If ipykernel is not installed, prompt to install it.
308-
if (!(await this.installer.isInstalled(Product.ipykernel, interpreter)) && !disableUI) {
314+
if (!(await this.kernelDependencyService.areDependenciesInstalled(interpreter, cancelToken)) && !disableUI) {
309315
// If we wish to wait for installation to complete, we must provide a cancel token.
310316
const token = new CancellationTokenSource();
311-
const response = await this.installer.promptToInstall(
312-
Product.ipykernel,
317+
const response = await this.kernelDependencyService.installMissingDependencies(
313318
interpreter,
314319
wrapCancellationTokens(cancelToken, token.token)
315320
);
316-
if (response !== InstallerResponse.Installed) {
321+
if (response !== KernelInterpreterDependencyResponse.ok) {
317322
traceWarning(
318323
`Prompted to install ipykernel, however ipykernel not installed in the interpreter ${interpreter.path}. Response ${response}`
319324
);

src/client/datascience/jupyter/kernels/kernelSwitcher.ts

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7-
import { CancellationTokenSource, ProgressLocation, ProgressOptions } from 'vscode';
7+
import { ProgressLocation, ProgressOptions } from 'vscode';
88
import { IApplicationShell } from '../../../common/application/types';
9-
import { traceVerbose } from '../../../common/logger';
10-
import { IConfigurationService, IInstaller, InstallerResponse, Product, Resource } from '../../../common/types';
9+
import { IConfigurationService, Resource } from '../../../common/types';
1110
import { Common, DataScience } from '../../../common/utils/localize';
1211
import { StopWatch } from '../../../common/utils/stopWatch';
1312
import { JupyterSessionStartError } from '../../baseJupyterSession';
14-
// import * as localize from '../../common/utils/localize';
1513
import { Commands, Settings } from '../../constants';
16-
import { IJupyterConnection, IJupyterKernelSpec, IJupyterSessionManagerFactory, INotebook } from '../../types';
14+
import {
15+
IJupyterConnection,
16+
IJupyterKernelSpec,
17+
IJupyterSessionManagerFactory,
18+
IKernelDependencyService,
19+
INotebook,
20+
KernelInterpreterDependencyResponse
21+
} from '../../types';
1722
import { JupyterInvalidKernelError } from '../jupyterInvalidKernelError';
1823
import { KernelSelector, KernelSpecInterpreter } from './kernelSelector';
1924
import { LiveKernelModel } from './types';
@@ -25,7 +30,7 @@ export class KernelSwitcher {
2530
@inject(IJupyterSessionManagerFactory) private jupyterSessionManagerFactory: IJupyterSessionManagerFactory,
2631
@inject(KernelSelector) private kernelSelector: KernelSelector,
2732
@inject(IApplicationShell) private appShell: IApplicationShell,
28-
@inject(IInstaller) private readonly installer: IInstaller
33+
@inject(IKernelDependencyService) private readonly kernelDependencyService: IKernelDependencyService
2934
) {}
3035

3136
public async switchKernel(notebook: INotebook): Promise<KernelSpecInterpreter | undefined> {
@@ -126,17 +131,9 @@ export class KernelSwitcher {
126131
}
127132
}
128133
private async switchToKernel(notebook: INotebook, kernel: KernelSpecInterpreter): Promise<void> {
129-
if (
130-
notebook.connection?.type === 'raw' &&
131-
!(await this.installer.isInstalled(Product.ipykernel, kernel.interpreter))
132-
) {
133-
const token = new CancellationTokenSource();
134-
const response = await this.installer.promptToInstall(Product.ipykernel, kernel.interpreter, token.token);
135-
if (response === InstallerResponse.Installed) {
136-
traceVerbose(`ipykernel installed in ${kernel.interpreter!.path}.`);
137-
} else {
138-
this.appShell.showErrorMessage(DataScience.ipykernelNotInstalled());
139-
traceVerbose(`ipykernel is not installed in ${kernel.interpreter!.path}.`);
134+
if (notebook.connection?.type === 'raw' && kernel.interpreter) {
135+
const response = await this.kernelDependencyService.installMissingDependencies(kernel.interpreter);
136+
if (response === KernelInterpreterDependencyResponse.cancel) {
140137
return;
141138
}
142139
}

src/client/datascience/serviceRegistry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { JupyterPasswordConnect } from './jupyter/jupyterPasswordConnect';
6565
import { JupyterServerWrapper } from './jupyter/jupyterServerWrapper';
6666
import { JupyterSessionManagerFactory } from './jupyter/jupyterSessionManagerFactory';
6767
import { JupyterVariables } from './jupyter/jupyterVariables';
68+
import { KernelDependencyService } from './jupyter/kernels/kernelDependencyService';
6869
import { KernelSelectionProvider } from './jupyter/kernels/kernelSelections';
6970
import { KernelSelector } from './jupyter/kernels/kernelSelector';
7071
import { KernelService } from './jupyter/kernels/kernelService';
@@ -112,6 +113,7 @@ import {
112113
IJupyterSessionManagerFactory,
113114
IJupyterSubCommandExecutionService,
114115
IJupyterVariables,
116+
IKernelDependencyService,
115117
INotebookEditor,
116118
INotebookEditorProvider,
117119
INotebookExecutionLogger,
@@ -209,6 +211,7 @@ export function registerTypes(serviceManager: IServiceManager) {
209211
serviceManager.addSingleton<IPyWidgetMessageDispatcherFactory>(IPyWidgetMessageDispatcherFactory, IPyWidgetMessageDispatcherFactory);
210212
serviceManager.addSingleton<IJupyterInterpreterDependencyManager>(IJupyterInterpreterDependencyManager, JupyterInterpreterSubCommandExecutionService);
211213
serviceManager.addSingleton<IJupyterSubCommandExecutionService>(IJupyterSubCommandExecutionService, JupyterInterpreterSubCommandExecutionService);
214+
serviceManager.addSingleton<IKernelDependencyService>(IKernelDependencyService, KernelDependencyService);
212215

213216
registerGatherTypes(serviceManager);
214217
}

src/client/datascience/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,3 +1137,17 @@ export interface IJMPConnection extends IDisposable {
11371137
// tslint:disable-next-line: no-any
11381138
subscribe(handlerFunc: (message: KernelMessage.IMessage) => void, errorHandler?: (exc: any) => void): void;
11391139
}
1140+
1141+
export enum KernelInterpreterDependencyResponse {
1142+
ok,
1143+
cancel
1144+
}
1145+
1146+
export const IKernelDependencyService = Symbol('IKernelDependencyService');
1147+
export interface IKernelDependencyService {
1148+
installMissingDependencies(
1149+
interpreter: PythonInterpreter,
1150+
token?: CancellationToken
1151+
): Promise<KernelInterpreterDependencyResponse>;
1152+
areDependenciesInstalled(interpreter: PythonInterpreter, _token?: CancellationToken): Promise<boolean>;
1153+
}

src/test/datascience/dataScienceIocContainer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ import { JupyterPasswordConnect } from '../../client/datascience/jupyter/jupyter
214214
import { JupyterServerWrapper } from '../../client/datascience/jupyter/jupyterServerWrapper';
215215
import { JupyterSessionManagerFactory } from '../../client/datascience/jupyter/jupyterSessionManagerFactory';
216216
import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables';
217+
import { KernelDependencyService } from '../../client/datascience/jupyter/kernels/kernelDependencyService';
217218
import { KernelSelectionProvider } from '../../client/datascience/jupyter/kernels/kernelSelections';
218219
import { KernelSelector } from '../../client/datascience/jupyter/kernels/kernelSelector';
219220
import { KernelService } from '../../client/datascience/jupyter/kernels/kernelService';
@@ -260,6 +261,7 @@ import {
260261
IJupyterSessionManagerFactory,
261262
IJupyterSubCommandExecutionService,
262263
IJupyterVariables,
264+
IKernelDependencyService,
263265
INotebookEditor,
264266
INotebookEditorProvider,
265267
INotebookExecutionLogger,
@@ -751,6 +753,7 @@ export class DataScienceIocContainer extends UnitTestIocContainer {
751753
this.serviceManager.addSingleton<KernelSelector>(KernelSelector, KernelSelector);
752754
this.serviceManager.addSingleton<KernelSelectionProvider>(KernelSelectionProvider, KernelSelectionProvider);
753755
this.serviceManager.addSingleton<KernelSwitcher>(KernelSwitcher, KernelSwitcher);
756+
this.serviceManager.addSingleton<IKernelDependencyService>(IKernelDependencyService, KernelDependencyService);
754757
this.serviceManager.addSingleton<IProductService>(IProductService, ProductService);
755758
this.serviceManager.addSingleton<IProductPathService>(
756759
IProductPathService,

0 commit comments

Comments
 (0)