Skip to content

Commit 4be1c61

Browse files
authored
Changes to listing kernel specs (#8880)
* Support installing ipykernel as a product
1 parent 03f76c6 commit 4be1c61

File tree

11 files changed

+144
-93
lines changed

11 files changed

+144
-93
lines changed

src/client/common/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Request as RequestResult } from 'request';
77
import { ConfigurationTarget, DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode';
88
import { CommandsWithoutArgs } from './application/commands';
99
import { ExtensionChannels } from './insidersBuild/types';
10+
import { InterpreterUri } from './installer/types';
1011
import { EnvironmentVariables } from './variables/types';
1112
export const IOutputChannel = Symbol('IOutputChannel');
1213
export interface IOutputChannel extends OutputChannel { }
@@ -113,9 +114,9 @@ export enum ModuleNamePurpose {
113114
export const IInstaller = Symbol('IInstaller');
114115

115116
export interface IInstaller {
116-
promptToInstall(product: Product, resource?: Uri): Promise<InstallerResponse>;
117-
install(product: Product, resource?: Uri): Promise<InstallerResponse>;
118-
isInstalled(product: Product, resource?: Uri): Promise<boolean | undefined>;
117+
promptToInstall(product: Product, resource?: InterpreterUri): Promise<InstallerResponse>;
118+
install(product: Product, resource?: InterpreterUri): Promise<InstallerResponse>;
119+
isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean | undefined>;
119120
translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string;
120121
}
121122

src/client/datascience/jupyter/jupyterSessionManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class JupyterSessionManager implements IJupyterSessionManager {
8686
return session;
8787
}
8888

89-
public async getActiveKernelSpecs(): Promise<IJupyterKernelSpec[]> {
89+
public async getKernelSpecs(): Promise<IJupyterKernelSpec[]> {
9090
if (!this.connInfo || !this.sessionManager || !this.contentsManager) {
9191
throw new Error(localize.DataScience.sessionDisposed());
9292
}
Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
2-
// Licensed under the MIT License.
3-
'use strict';
4-
import { Kernel } from '@jupyterlab/services';
5-
import * as fs from 'fs-extra';
6-
import * as path from 'path';
7-
8-
import { noop } from '../../../common/utils/misc';
9-
import { PythonInterpreter } from '../../../interpreter/contracts';
10-
import { IJupyterKernelSpec } from '../../types';
11-
12-
const IsGuidRegEx = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13-
14-
export class JupyterKernelSpec implements IJupyterKernelSpec {
15-
public name: string;
16-
public language: string;
17-
public path: string;
18-
public specFile: string | undefined;
19-
public display_name: string;
20-
// tslint:disable-next-line: no-any
21-
public metadata?: Record<string, any> & { interpreter?: Partial<PythonInterpreter> };
22-
constructor(specModel: Kernel.ISpecModel, file?: string) {
23-
this.name = specModel.name;
24-
this.language = specModel.language;
25-
this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : '';
26-
this.specFile = file;
27-
this.display_name = specModel.display_name;
28-
this.metadata = specModel.metadata;
29-
}
30-
public dispose = async () => {
31-
if (this.specFile &&
32-
IsGuidRegEx.test(path.basename(path.dirname(this.specFile)))) {
33-
// There is more than one location for the spec file directory
34-
// to be cleaned up. If one fails, the other likely deleted it already.
35-
try {
36-
await fs.remove(path.dirname(this.specFile));
37-
} catch {
38-
noop();
39-
}
40-
this.specFile = undefined;
41-
}
42-
}
43-
}
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
import { Kernel } from '@jupyterlab/services';
5+
import * as fs from 'fs-extra';
6+
import * as path from 'path';
7+
8+
import { noop } from '../../../common/utils/misc';
9+
import { PythonInterpreter } from '../../../interpreter/contracts';
10+
import { IJupyterKernelSpec } from '../../types';
11+
12+
const IsGuidRegEx = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13+
14+
export class JupyterKernelSpec implements IJupyterKernelSpec {
15+
public name: string;
16+
public language: string;
17+
public path: string;
18+
public specFile: string | undefined;
19+
public display_name: string;
20+
// tslint:disable-next-line: no-any
21+
public metadata?: Record<string, any> & { interpreter?: Partial<PythonInterpreter> };
22+
constructor(specModel: Kernel.ISpecModel, file?: string) {
23+
this.name = specModel.name;
24+
this.language = specModel.language;
25+
this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : '';
26+
this.specFile = file;
27+
this.display_name = specModel.display_name;
28+
this.metadata = specModel.metadata;
29+
}
30+
public dispose = async () => {
31+
if (this.specFile &&
32+
IsGuidRegEx.test(path.basename(path.dirname(this.specFile)))) {
33+
// There is more than one location for the spec file directory
34+
// to be cleaned up. If one fails, the other likely deleted it already.
35+
try {
36+
await fs.remove(path.dirname(this.specFile));
37+
} catch {
38+
noop();
39+
}
40+
this.specFile = undefined;
41+
}
42+
}
43+
}

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

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class KernelService {
7979
*/
8080
public async findMatchingKernelSpec(
8181
interpreter: PythonInterpreter,
82-
sessionManager: IJupyterSessionManager | undefined,
82+
sessionManager?: IJupyterSessionManager | undefined,
8383
cancelToken?: CancellationToken
8484
): Promise<IJupyterKernelSpec | undefined>;
8585
public async findMatchingKernelSpec(
@@ -118,7 +118,7 @@ export class KernelService {
118118
}
119119

120120
// Now enumerate them again
121-
const enumerator = sessionManager ? () => sessionManager.getActiveKernelSpecs() : () => this.enumerateSpecs(cancelToken);
121+
const enumerator = sessionManager ? () => sessionManager.getKernelSpecs() : () => this.enumerateSpecs(cancelToken);
122122

123123
// Then find our match
124124
return this.findSpecMatch(enumerator);
@@ -132,6 +132,7 @@ export class KernelService {
132132
}
133133
}
134134
}
135+
135136
/**
136137
* Registers an interprter as a kernel.
137138
* The assumption is that `ipykernel` has been installed in the interpreter.
@@ -147,40 +148,64 @@ export class KernelService {
147148
*/
148149
@captureTelemetry(Telemetry.RegisterInterpreterAsKernel, undefined, true)
149150
@traceDecorators.error('Failed to register an interpreter as a kernel')
150-
public async registerKernel(interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec> {
151-
if (!interpreter.displayName){
151+
public async registerKernel(interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec | undefined> {
152+
if (!interpreter.displayName) {
152153
throw new Error('Interpreter does not have a display name');
153154
}
154155
const ipykernelCommand = await this.commandFinder.findBestCommand(JupyterCommands.KernelCreateCommand, cancelToken);
155-
if (ipykernelCommand.status === ModuleExistsStatus.NotFound || !ipykernelCommand.command){
156+
if (ipykernelCommand.status === ModuleExistsStatus.NotFound || !ipykernelCommand.command) {
156157
throw new Error('Command not found to install the kernel');
157158
}
159+
if (Cancellation.isCanceled(cancelToken)) {
160+
return;
161+
}
158162
const name = await this.generateKernelNameForIntepreter(interpreter);
159163
const output = await ipykernelCommand.command.exec(['install', '--user', '--name', name, '--display-name', interpreter.displayName], {
160164
throwOnStdErr: true,
161165
encoding: 'utf8',
162166
token: cancelToken
163167
});
164-
const kernel = await this.findMatchingKernelSpec({display_name: interpreter.displayName, name}, undefined, cancelToken);
165-
if (!kernel){
168+
if (Cancellation.isCanceled(cancelToken)) {
169+
return;
170+
}
171+
172+
const kernel = await this.findMatchingKernelSpec({ display_name: interpreter.displayName, name }, undefined, cancelToken);
173+
if (Cancellation.isCanceled(cancelToken)) {
174+
return;
175+
}
176+
if (!kernel) {
166177
const error = `Kernel not created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`;
167178
throw new Error(error);
168179
}
169180
if (!(kernel instanceof JupyterKernelSpec)) {
170181
const error = `Kernel not registered locally, created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`;
171182
throw new Error(error);
172183
}
173-
if (!kernel.specFile){
184+
if (!kernel.specFile) {
174185
const error = `kernel.json not created with the name ${name}, display_name ${interpreter.displayName}. Output is ${output.stdout}`;
175186
throw new Error(error);
176187
}
177188
const specModel: ReadWrite<Kernel.ISpecModel> = JSON.parse(await this.fileSystem.readFile(kernel.specFile));
178189

190+
// Ensure we use a fully qualified path to the python interpreter in `argv`.
191+
if (specModel.argv[0].toLowerCase() === 'conda') {
192+
// If conda is the first word, its possible its a conda activation command.
193+
traceInfo(`Spec argv[0], not updated as it is using conda.`);
194+
} else {
195+
traceInfo(`Spec argv[0] updated from '${specModel.argv[0]}' to '${interpreter.path}'`);
196+
specModel.argv[0] = interpreter.path;
197+
}
198+
179199
// Get the activated environment variables (as a work around for `conda run` and similar).
180200
// This ensures the code runs within the context of an activated environment.
181-
specModel.env = await this.activationHelper.getActivatedEnvironmentVariables(undefined, interpreter, true)
182-
// tslint:disable-next-line: no-any
183-
.catch(noop).then(env => (env || {}) as any);
201+
specModel.env = await this.activationHelper
202+
.getActivatedEnvironmentVariables(undefined, interpreter, true)
203+
.catch(noop)
204+
// tslint:disable-next-line: no-any
205+
.then(env => (env || {}) as any);
206+
if (Cancellation.isCanceled(cancelToken)) {
207+
return;
208+
}
184209

185210
// Ensure we update the metadata to include interpreter stuff as well (we'll use this to search kernels that match an interpreter).
186211
// We'll need information such as interpreter type, display name, path, etc...
@@ -193,6 +218,7 @@ export class KernelService {
193218
await this.fileSystem.writeFile(kernel.specFile, JSON.stringify(specModel, undefined, 2));
194219
kernel.metadata = specModel.metadata;
195220

221+
traceInfo(`Kernel successfully registered for ${interpreter.path} with the name=${name} and spec can be found here ${kernel.specFile}`);
196222
return kernel;
197223
}
198224
/**
@@ -208,7 +234,7 @@ export class KernelService {
208234
return `${interpreter.displayName || ''}_${await this.fileSystem.getFileHash(interpreter.path)}`.replace(/[^A-Za-z0-9]/g, '');
209235
}
210236
private async getKernelSpecs(sessionManager?: IJupyterSessionManager, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec[]> {
211-
const enumerator = sessionManager ? sessionManager.getActiveKernelSpecs() : this.enumerateSpecs(cancelToken);
237+
const enumerator = sessionManager ? sessionManager.getKernelSpecs() : this.enumerateSpecs(cancelToken);
212238
if (Cancellation.isCanceled(cancelToken)) {
213239
return [];
214240
}
@@ -402,21 +428,23 @@ export class KernelService {
402428
traceInfo('Parsing kernelspecs from jupyter');
403429
// This should give us back a key value pair we can parse
404430
const kernelSpecs = JSON.parse(output.stdout.trim()) as Record<string, { resource_dir: string; spec: Omit<Kernel.ISpecModel, 'name'> }>;
405-
const specs = await Promise.all(Object.keys(kernelSpecs).map(async kernelName => {
406-
const specFile = path.join(kernelSpecs[kernelName].resource_dir, 'kernel.json');
407-
const spec = kernelSpecs[kernelName].spec;
408-
// Add the missing name property.
409-
const model = {
410-
...spec,
411-
name: kernelName
412-
};
413-
// Check if the spec file exists.
414-
if (await this.fileSystem.fileExists(specFile)){
415-
return new JupyterKernelSpec(model as Kernel.ISpecModel, specFile);
416-
} else {
417-
return;
418-
}
419-
}));
431+
const specs = await Promise.all(
432+
Object.keys(kernelSpecs).map(async kernelName => {
433+
const specFile = path.join(kernelSpecs[kernelName].resource_dir, 'kernel.json');
434+
const spec = kernelSpecs[kernelName].spec;
435+
// Add the missing name property.
436+
const model = {
437+
...spec,
438+
name: kernelName
439+
};
440+
// Check if the spec file exists.
441+
if (await this.fileSystem.fileExists(specFile)) {
442+
return new JupyterKernelSpec(model as Kernel.ISpecModel, specFile);
443+
} else {
444+
return;
445+
}
446+
})
447+
);
420448
return specs.filter(item => !!item).map(item => item as JupyterKernelSpec);
421449
} catch (ex) {
422450
traceError('Failed to list kernels', ex);

src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class GuestJupyterSessionManager implements IJupyterSessionManager {
1717
return this.realSessionManager.startNew(kernelSpec, cancelToken);
1818
}
1919

20-
public async getActiveKernelSpecs(): Promise<IJupyterKernelSpec[]> {
20+
public async getKernelSpecs(): Promise<IJupyterKernelSpec[]> {
2121
// Don't return any kernel specs in guest mode. They're only needed for the host side
2222
return Promise.resolve([]);
2323
}

src/client/datascience/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export interface IJupyterSessionManagerFactory {
182182

183183
export interface IJupyterSessionManager extends IAsyncDisposable {
184184
startNew(kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise<IJupyterSession>;
185-
getActiveKernelSpecs(): Promise<IJupyterKernelSpec[]>;
185+
getKernelSpecs(): Promise<IJupyterKernelSpec[]>;
186186
getConnInfo(): IConnection;
187187
getRunningKernels(): Promise<IJupyterKernel[]>;
188188
}

src/client/interpreter/configuration/interpreterSelector.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export class InterpreterSelector implements IInterpreterSelector {
4242
// tslint:disable-next-line:no-non-null-assertion
4343
label: suggestion.displayName!,
4444
detail: `${cachedPrefix}${detail}`,
45-
path: suggestion.path
45+
path: suggestion.path,
46+
interpreter: suggestion
4647
};
4748
}
4849

src/client/interpreter/configuration/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export interface IInterpreterSelector extends Disposable {
2626

2727
export interface IInterpreterQuickPickItem extends QuickPickItem {
2828
path: string;
29+
/**
30+
* The interpreter related to this quickpick item.
31+
*
32+
* @type {PythonInterpreter}
33+
* @memberof IInterpreterQuickPickItem
34+
*/
35+
interpreter: PythonInterpreter;
2936
}
3037

3138
export const IInterpreterComparer = Symbol('IInterpreterComparer');

src/test/configuration/interpreterSelector.unit.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class InterpreterQuickPickItem implements IInterpreterQuickPickItem {
3131
public label: string;
3232
public description!: string;
3333
public detail?: string;
34+
// tslint:disable-next-line: no-any
35+
public interpreter = {} as any;
3436
constructor(l: string, p: string) {
3537
this.path = p;
3638
this.label = l;
@@ -136,7 +138,9 @@ suite('Interpreters - selector', () => {
136138
pythonSettings.setup(p => p.pythonPath).returns(() => 'python');
137139
const selectedItem: IInterpreterQuickPickItem = {
138140
description: '', detail: '', label: '',
139-
path: 'This is the selected Python path'
141+
path: 'This is the selected Python path',
142+
// tslint:disable-next-line: no-any
143+
interpreter: {} as any
140144
};
141145

142146
workspace.setup(w => w.workspaceFolders).returns(() => undefined);
@@ -166,7 +170,9 @@ suite('Interpreters - selector', () => {
166170
pythonSettings.setup(p => p.pythonPath).returns(() => 'python');
167171
const selectedItem: IInterpreterQuickPickItem = {
168172
description: '', detail: '', label: '',
169-
path: 'This is the selected Python path'
173+
path: 'This is the selected Python path',
174+
// tslint:disable-next-line: no-any
175+
interpreter: {} as any
170176
};
171177

172178
const folder = { name: 'one', uri: Uri.parse('one'), index: 0 };
@@ -197,7 +203,9 @@ suite('Interpreters - selector', () => {
197203
pythonSettings.setup(p => p.pythonPath).returns(() => 'python');
198204
const selectedItem: IInterpreterQuickPickItem = {
199205
description: '', detail: '', label: '',
200-
path: 'This is the selected Python path'
206+
path: 'This is the selected Python path',
207+
// tslint:disable-next-line: no-any
208+
interpreter: {} as any
201209
};
202210

203211
const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 };
@@ -232,7 +240,9 @@ suite('Interpreters - selector', () => {
232240

233241
const selectedItem: IInterpreterQuickPickItem = {
234242
description: '', detail: '', label: '',
235-
path: 'This is the selected Python path'
243+
path: 'This is the selected Python path',
244+
// tslint:disable-next-line: no-any
245+
interpreter: {} as any
236246
};
237247

238248
const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 };

0 commit comments

Comments
 (0)