Skip to content

Commit a29a8de

Browse files
authored
Add Kernel selection picker for local and remote sessions (#8873)
* Support installing ipykernel as a product * No need * Linter fixes * Address code review comments
1 parent 3938bea commit a29a8de

File tree

7 files changed

+577
-8
lines changed

7 files changed

+577
-8
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 * as localize from '../../../common/utils/localize';
9+
import { IInterpreterSelector } from '../../../interpreter/configuration/types';
10+
import { IJupyterKernel, IJupyterKernelSpec, IJupyterSessionManager } from '../../types';
11+
import { KernelService } from './kernelService';
12+
import { IKernelSelectionListProvider, IKernelSpecQuickPickItem } from './types';
13+
14+
// Small classes, hence all put into one file.
15+
// tslint:disable: max-classes-per-file
16+
17+
/**
18+
* Given a kernel spec, this will return a quick pick item with appropriate display names and the like.
19+
*
20+
* @param {IJupyterKernelSpec} kernelSpec
21+
* @returns {IKernelSpecQuickPickItem}
22+
*/
23+
function getQuickPickItemForKernelSpec(kernelSpec: IJupyterKernelSpec): IKernelSpecQuickPickItem {
24+
return {
25+
label: kernelSpec.display_name,
26+
// tslint:disable-next-line: no-suspicious-comment
27+
// TODO: Localize & fix as per spec.
28+
description: '(kernel)',
29+
selection: { kernelModel: undefined, kernelSpec: kernelSpec, interpreter: undefined }
30+
};
31+
}
32+
33+
/**
34+
* Given an active kernel, this will return a quick pick item with appropriate display names and the like.
35+
*
36+
* @param {(IJupyterKernel & Partial<IJupyterKernelSpec>)} kernel
37+
* @returns {IKernelSpecQuickPickItem}
38+
*/
39+
function getQuickPickItemForActiveKernel(kernel: IJupyterKernel & Partial<IJupyterKernelSpec>): IKernelSpecQuickPickItem {
40+
return {
41+
label: kernel.display_name || kernel.name || '',
42+
description: localize.DataScience.jupyterSelectURIRunningDetailFormat().format(kernel.lastActivityTime.toLocaleString(), kernel.numberOfConnections.toString()),
43+
selection: { kernelModel: kernel, kernelSpec: undefined, interpreter: undefined }
44+
};
45+
}
46+
47+
/**
48+
* Provider for active kernel specs in a jupyter session.
49+
*
50+
* @export
51+
* @class ActiveJupyterSessionKernelSelectionListProvider
52+
* @implements {IKernelSelectionListProvider}
53+
*/
54+
export class ActiveJupyterSessionKernelSelectionListProvider implements IKernelSelectionListProvider {
55+
constructor(private readonly sessionManager: IJupyterSessionManager) {}
56+
public async getKernelSelections(_cancelToken?: CancellationToken | undefined): Promise<IKernelSpecQuickPickItem[]> {
57+
const [activeKernels, kernelSpecs] = await Promise.all([this.sessionManager.getRunningKernels(), this.sessionManager.getKernelSpecs()]);
58+
const items = activeKernels.map(item => {
59+
const matchingSpec: Partial<IJupyterKernelSpec> = kernelSpecs.find(spec => spec.name === item.name) || {};
60+
return {
61+
...item,
62+
...matchingSpec
63+
};
64+
});
65+
return items.filter(item => item.display_name || item.name).map(getQuickPickItemForActiveKernel);
66+
}
67+
}
68+
69+
/**
70+
* Provider for installed kernel specs (`python -m jupyter kernelspec list`).
71+
*
72+
* @export
73+
* @class InstalledJupyterKernelSelectionListProvider
74+
* @implements {IKernelSelectionListProvider}
75+
*/
76+
export class InstalledJupyterKernelSelectionListProvider implements IKernelSelectionListProvider {
77+
constructor(private readonly kernelService: KernelService, private readonly sessionManager?: IJupyterSessionManager) {}
78+
public async getKernelSelections(cancelToken?: CancellationToken | undefined): Promise<IKernelSpecQuickPickItem[]> {
79+
const items = await this.kernelService.getKernelSpecs(this.sessionManager, cancelToken);
80+
return items.map(getQuickPickItemForKernelSpec);
81+
}
82+
}
83+
84+
/**
85+
* Provider for interpreters to be treated as kernel specs.
86+
* I.e. return interpreters that are to be treated as kernel specs, and not yet installed as kernels.
87+
*
88+
* @export
89+
* @class InterpreterKernelSelectionListProvider
90+
* @implements {IKernelSelectionListProvider}
91+
*/
92+
export class InterpreterKernelSelectionListProvider implements IKernelSelectionListProvider {
93+
constructor(private readonly interpreterSelector: IInterpreterSelector) {}
94+
public async getKernelSelections(_cancelToken?: CancellationToken | undefined): Promise<IKernelSpecQuickPickItem[]> {
95+
const items = await this.interpreterSelector.getSuggestions(undefined);
96+
return items.map(item => {
97+
return {
98+
...item,
99+
// tslint:disable-next-line: no-suspicious-comment
100+
// TODO: Localize & fix as per spec.
101+
description: '(register and use interpreter as kernel)',
102+
selection: { kernelModel: undefined, interpreter: item.interpreter, kernelSpec: undefined }
103+
};
104+
});
105+
}
106+
}
107+
108+
/**
109+
* Provides a list of kernel specs for selection, for both local and remote sessions.
110+
*
111+
* @export
112+
* @class KernelSelectionProviderFactory
113+
*/
114+
@injectable()
115+
export class KernelSelectionProvider {
116+
constructor(@inject(KernelService) private readonly kernelService: KernelService, @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector) {}
117+
/**
118+
* Gets a selection of kernel specs from a remote session.
119+
*
120+
* @param {IJupyterSessionManager} sessionManager
121+
* @param {CancellationToken} [cancelToken]
122+
* @returns {Promise<IKernelSpecQuickPickItem[]>}
123+
* @memberof KernelSelectionProvider
124+
*/
125+
public async getKernelSelectionsForRemoteSession(sessionManager: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IKernelSpecQuickPickItem[]> {
126+
return new ActiveJupyterSessionKernelSelectionListProvider(sessionManager).getKernelSelections(cancelToken);
127+
}
128+
/**
129+
* Gets a selection of kernel specs for a local session.
130+
*
131+
* @param {IJupyterSessionManager} [sessionManager]
132+
* @param {CancellationToken} [cancelToken]
133+
* @returns {Promise<IKernelSelectionListProvider>}
134+
* @memberof KernelSelectionProvider
135+
*/
136+
public async getKernelSelectionsForLocalSession(sessionManager?: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IKernelSpecQuickPickItem[]> {
137+
const activeKernelsPromise = sessionManager ? new ActiveJupyterSessionKernelSelectionListProvider(sessionManager).getKernelSelections(cancelToken) : Promise.resolve([]);
138+
const jupyterKernelsPromise = new InstalledJupyterKernelSelectionListProvider(this.kernelService).getKernelSelections(cancelToken);
139+
const interpretersPromise = new InterpreterKernelSelectionListProvider(this.interpreterSelector).getKernelSelections(cancelToken);
140+
const [activeKernels, jupyterKernels, interprters] = await Promise.all([activeKernelsPromise, jupyterKernelsPromise, interpretersPromise]);
141+
return [...jupyterKernels!, ...activeKernels!, ...interprters];
142+
}
143+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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-jsonrpc';
8+
import { IApplicationShell } from '../../../common/application/types';
9+
import { Cancellation } from '../../../common/cancellation';
10+
import { traceInfo, traceWarning } from '../../../common/logger';
11+
import { IInstaller, InstallerResponse, Product } from '../../../common/types';
12+
import { PythonInterpreter } from '../../../interpreter/contracts';
13+
import { IJupyterKernelSpec, IJupyterSessionManager } from '../../types';
14+
import { KernelSelectionProvider } from './kernelSelections';
15+
import { KernelService } from './kernelService';
16+
17+
@injectable()
18+
export class KernelSelector {
19+
constructor(
20+
@inject(KernelSelectionProvider) private readonly selectionProvider: KernelSelectionProvider,
21+
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
22+
@inject(KernelService) private readonly kernelService: KernelService,
23+
@inject(IInstaller) private readonly installer: IInstaller
24+
) {}
25+
public async selectRemoteKernel(session: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec | undefined> {
26+
const suggestions = this.selectionProvider.getKernelSelectionsForRemoteSession(session, cancelToken);
27+
const selection = await this.applicationShell.showQuickPick(suggestions, undefined, cancelToken);
28+
if (!selection) {
29+
return;
30+
}
31+
32+
if (selection.selection.kernelSpec) {
33+
return selection.selection.kernelSpec;
34+
}
35+
// This is not possible (remote kernels selector can only display remote kernels).
36+
throw new Error('Invalid Selection in kernel spec (somehow a local kernel/interpreter has been selected for a remote session!');
37+
}
38+
39+
public async selectLocalKernel(session?: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec | undefined> {
40+
const suggestions = this.selectionProvider.getKernelSelectionsForLocalSession(session, cancelToken);
41+
const selection = await this.applicationShell.showQuickPick(suggestions, undefined, cancelToken);
42+
if (!selection) {
43+
return;
44+
}
45+
46+
// Check if ipykernel is installed in this kernel.
47+
const interpreter = selection.selection.interpreter;
48+
if (interpreter) {
49+
const isValid = await this.isSelectionValid(interpreter, cancelToken);
50+
if (isValid) {
51+
// Find the kernel associated with this interpter.
52+
const kernelSpec = await this.kernelService.findMatchingKernelSpec(interpreter, session, cancelToken);
53+
if (kernelSpec){
54+
traceInfo(`ipykernel installed in ${interpreter.path}, and matching found.`);
55+
return kernelSpec;
56+
}
57+
traceInfo(`ipykernel installed in ${interpreter.path}, no matching kernel found. Will register kernel.`);
58+
}
59+
60+
// Try an install this interpreter as a kernel.
61+
return this.kernelService.registerKernel(interpreter, cancelToken);
62+
} else {
63+
return selection.selection.kernelSpec;
64+
}
65+
}
66+
67+
private async isSelectionValid(interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<boolean> {
68+
// Is ipykernel installed in this environment.
69+
if (await this.installer.isInstalled(Product.ipykernel, interpreter)) {
70+
return true;
71+
}
72+
if (Cancellation.isCanceled(cancelToken)) {
73+
return false;
74+
}
75+
const response = await this.installer.promptToInstall(Product.ipykernel, interpreter);
76+
if (response === InstallerResponse.Installed) {
77+
return true;
78+
}
79+
traceWarning(`Prompted to install ipykernel, however ipykernel not installed in the interpreter ${interpreter.path}`);
80+
return false;
81+
}
82+
}

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,22 @@ export class KernelService {
221221
traceInfo(`Kernel successfully registered for ${interpreter.path} with the name=${name} and spec can be found here ${kernel.specFile}`);
222222
return kernel;
223223
}
224+
/**
225+
* Gets a list of all kernel specs.
226+
*
227+
* @param {IJupyterSessionManager} [sessionManager]
228+
* @param {CancellationToken} [cancelToken]
229+
* @returns {Promise<IJupyterKernelSpec[]>}
230+
* @memberof KernelService
231+
*/
232+
public async getKernelSpecs(sessionManager?: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec[]> {
233+
const enumerator = sessionManager ? sessionManager.getKernelSpecs() : this.enumerateSpecs(cancelToken);
234+
if (Cancellation.isCanceled(cancelToken)) {
235+
return [];
236+
}
237+
const specs = await enumerator;
238+
return specs.filter(item => !!item);
239+
}
224240
/**
225241
* Not all characters are allowed in a kernel name.
226242
* This method will generate a name for a kernel based on display name and path.
@@ -233,14 +249,6 @@ export class KernelService {
233249
private async generateKernelNameForIntepreter(interpreter: PythonInterpreter): Promise<string> {
234250
return `${interpreter.displayName || ''}_${await this.fileSystem.getFileHash(interpreter.path)}`.replace(/[^A-Za-z0-9]/g, '');
235251
}
236-
private async getKernelSpecs(sessionManager?: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec[]> {
237-
const enumerator = sessionManager ? sessionManager.getKernelSpecs() : this.enumerateSpecs(cancelToken);
238-
if (Cancellation.isCanceled(cancelToken)) {
239-
return [];
240-
}
241-
const specs = await enumerator;
242-
return specs.filter(item => !!item);
243-
}
244252
private hasSpecPathMatch = async (info: PythonInterpreter | undefined, cancelToken?: CancellationToken): Promise<boolean> => {
245253
if (info) {
246254
// Enumerate our specs
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { CancellationToken, QuickPickItem } from 'vscode';
7+
import { PythonInterpreter } from '../../../interpreter/contracts';
8+
import { IJupyterKernel, IJupyterKernelSpec } from '../../types';
9+
10+
export interface IKernelSpecQuickPickItem extends QuickPickItem {
11+
/**
12+
* Whether a
13+
* - Kernel spec (IJupyterKernelSpec)
14+
* - Active kernel (IJupyterKernel) or
15+
* - Interpreter has been selected.
16+
* If interpreter is selected, then we might need to install this as a kernel to get the kernel spec.
17+
*
18+
* @type {({ kernelModel: IJupyterKernel; kernelSpec: IJupyterKernelSpec; interpreter: undefined }
19+
* | { kernelModel: undefined; kernelSpec: IJupyterKernelSpec; interpreter: undefined }
20+
* | { kernelModel: undefined; kernelSpec: undefined; interpreter: PythonInterpreter })}
21+
* @memberof IKernelSpecQuickPickItem
22+
*/
23+
selection:
24+
| { kernelModel: IJupyterKernel & Partial<IJupyterKernelSpec>; kernelSpec: undefined; interpreter: undefined }
25+
| { kernelModel: undefined; kernelSpec: IJupyterKernelSpec; interpreter: undefined }
26+
| { kernelModel: undefined; kernelSpec: undefined; interpreter: PythonInterpreter };
27+
}
28+
29+
export interface IKernelSelectionListProvider {
30+
getKernelSelections(cancelToken?: CancellationToken): Promise<IKernelSpecQuickPickItem[]>;
31+
}

src/client/datascience/serviceRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { JupyterPasswordConnect } from './jupyter/jupyterPasswordConnect';
3838
import { JupyterServerFactory } from './jupyter/jupyterServerFactory';
3939
import { JupyterSessionManagerFactory } from './jupyter/jupyterSessionManagerFactory';
4040
import { JupyterVariables } from './jupyter/jupyterVariables';
41+
import { KernelSelectionProvider } from './jupyter/kernels/kernelSelections';
42+
import { KernelSelector } from './jupyter/kernels/kernelSelector';
4143
import { PlotViewer } from './plotting/plotViewer';
4244
import { PlotViewerProvider } from './plotting/plotViewerProvider';
4345
import { StatusProvider } from './statusProvider';
@@ -121,4 +123,6 @@ export function registerTypes(serviceManager: IServiceManager) {
121123
serviceManager.addSingleton<IDebugLocationTracker>(IDebugLocationTracker, DebugLocationTrackerFactory);
122124
serviceManager.addSingleton<JupyterCommandFinder>(JupyterCommandFinder, JupyterCommandFinder);
123125
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, Activation);
126+
serviceManager.addSingleton<KernelSelector>(KernelSelector, KernelSelector);
127+
serviceManager.addSingleton<KernelSelectionProvider>(KernelSelectionProvider, KernelSelectionProvider);
124128
}

0 commit comments

Comments
 (0)