Skip to content

Commit 05e6637

Browse files
committed
Use different method for checking if kernelspec is available (#10114)
* Use different method for checking if kernelspec is available * Fix unit tests
1 parent 92b221e commit 05e6637

File tree

4 files changed

+65
-41
lines changed

4 files changed

+65
-41
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Check whether kernelspec module exists.
2+
import sys
3+
import jupyter_client
4+
import jupyter_client.kernelspec
5+
6+
sys.stdout.write(jupyter_client.__version__)
7+
sys.stdout.flush()

src/client/datascience/jupyter/interpreter/jupyterCommand.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,49 @@ export class InterpreterJupyterKernelSpecCommand extends InterpreterJupyterComma
201201
return output;
202202
}
203203

204-
// We're only interested in `python -m jupyter kernelspec list --json`
205-
const interpreter = await this.interpreter();
206-
if (
207-
!interpreter ||
208-
this.moduleName.toLowerCase() !== 'jupyter' ||
209-
this.args.join(' ').toLowerCase() !== `-m jupyter ${JupyterCommands.KernelSpecCommand}`.toLowerCase() ||
210-
args.join(' ').toLowerCase() !== 'list --json'
211-
) {
204+
const defaultAction = () => {
212205
if (exception) {
213206
throw exception;
214207
}
215208
return output;
209+
};
210+
211+
// We're only interested in `python -m jupyter kernelspec`
212+
const interpreter = await this.interpreter();
213+
if (!interpreter || this.moduleName.toLowerCase() !== 'jupyter' || this.args.join(' ').toLowerCase() !== `-m jupyter ${JupyterCommands.KernelSpecCommand}`.toLowerCase()) {
214+
return defaultAction();
216215
}
216+
217+
// Otherwise try running a script instead.
217218
try {
218-
// Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception.
219-
const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({ interpreter, bypassCondaExecution: true });
220-
return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernels.py')], { ...options, throwOnStdErr: true });
219+
if (args.join(' ').toLowerCase() === 'list --json') {
220+
// Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception.
221+
return this.getKernelSpecList(interpreter, options);
222+
} else if (args.join(' ').toLowerCase() === '--version') {
223+
// Try getting kernelspec version using python script, if that fails (even if there's output in stderr) rethrow original exception.
224+
return this.getKernelSpecVersion(interpreter, options);
225+
}
221226
} catch (innerEx) {
222227
traceError('Failed to get a list of the kernelspec using python script', innerEx);
223-
// Rethrow original exception.
224-
if (exception) {
225-
throw exception;
226-
}
227-
return output;
228228
}
229+
return defaultAction();
230+
}
231+
232+
private async getKernelSpecList(interpreter: PythonInterpreter, options: SpawnOptions) {
233+
// Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception.
234+
const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({
235+
interpreter,
236+
bypassCondaExecution: true
237+
});
238+
return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernels.py')], { ...options, throwOnStdErr: true });
239+
}
240+
private async getKernelSpecVersion(interpreter: PythonInterpreter, options: SpawnOptions) {
241+
// Try getting kernels using python script, if that fails (even if there's output in stderr) rethrow original exception.
242+
const activatedEnv = await this.pythonExecutionFactory.createActivatedEnvironment({
243+
interpreter,
244+
bypassCondaExecution: true
245+
});
246+
return activatedEnv.exec([path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterKernelspecVersion.py')], { ...options, throwOnStdErr: true });
229247
}
230248
}
231249

src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import { CancellationToken } from 'vscode';
88
import { IApplicationShell } from '../../../common/application/types';
99
import { Cancellation, createPromiseFromCancellation, wrapCancellationTokens } from '../../../common/cancellation';
1010
import { ProductNames } from '../../../common/installer/productNames';
11-
import { IPythonExecutionFactory } from '../../../common/process/types';
1211
import { IInstaller, InstallerResponse, Product } from '../../../common/types';
1312
import { Common, DataScience } from '../../../common/utils/localize';
1413
import { noop } from '../../../common/utils/misc';
1514
import { PythonInterpreter } from '../../../interpreter/contracts';
1615
import { sendTelemetryEvent } from '../../../telemetry';
17-
import { HelpLinks, Telemetry } from '../../constants';
16+
import { HelpLinks, JupyterCommands, Telemetry } from '../../constants';
17+
import { IJupyterCommandFactory } from '../../types';
1818
import { JupyterInstallError } from '../jupyterInstallError';
1919

2020
export enum JupyterInterpreterDependencyResponse {
@@ -108,7 +108,7 @@ export class JupyterInterpreterDependencyService {
108108
constructor(
109109
@inject(IApplicationShell) private readonly applicationShell: IApplicationShell,
110110
@inject(IInstaller) private readonly installer: IInstaller,
111-
@inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory
111+
@inject(IJupyterCommandFactory) private readonly commandFactory: IJupyterCommandFactory
112112
) {}
113113
/**
114114
* Configures the python interpreter to ensure it can run Jupyter server by installing any missing dependencies.
@@ -270,13 +270,10 @@ export class JupyterInterpreterDependencyService {
270270
* @returns {Promise<boolean>}
271271
* @memberof JupyterInterpreterConfigurationService
272272
*/
273-
private async isKernelSpecAvailable(interpreter: PythonInterpreter, token?: CancellationToken): Promise<boolean> {
274-
const execService = await this.pythonExecFactory.createActivatedEnvironment({ interpreter, allowEnvironmentFetchExceptions: true, bypassCondaExecution: true });
275-
if (Cancellation.isCanceled(token)) {
276-
return false;
277-
}
278-
return execService
279-
.execModule('jupyter', ['kernelspec', '--version'], { throwOnStdErr: true })
273+
private async isKernelSpecAvailable(interpreter: PythonInterpreter, _token?: CancellationToken): Promise<boolean> {
274+
const command = this.commandFactory.createInterpreterCommand(JupyterCommands.KernelSpecCommand, 'jupyter', ['-m', 'jupyter', 'kernelspec'], interpreter, false);
275+
return command
276+
.exec(['--version'], { throwOnStdErr: true })
280277
.then(() => true)
281278
.catch(() => {
282279
sendTelemetryEvent(Telemetry.KernelSpecNotFound);
@@ -298,9 +295,10 @@ export class JupyterInterpreterDependencyService {
298295
*/
299296
private async checkKernelSpecAvailability(interpreter: PythonInterpreter, token?: CancellationToken): Promise<JupyterInterpreterDependencyResponse> {
300297
if (await this.isKernelSpecAvailable(interpreter)) {
301-
sendTelemetryEvent(Telemetry.JupyterInstalledButNotKernelSpecModule);
302298
return JupyterInterpreterDependencyResponse.ok;
303299
}
300+
// Indicate no kernel spec module.
301+
sendTelemetryEvent(Telemetry.JupyterInstalledButNotKernelSpecModule);
304302
if (Cancellation.isCanceled(token)) {
305303
return JupyterInterpreterDependencyResponse.cancel;
306304
}

src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,26 @@
44
'use strict';
55

66
import { assert } from 'chai';
7-
import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
7+
import { anything, instance, mock, verify, when } from 'ts-mockito';
88
import { ApplicationShell } from '../../../../client/common/application/applicationShell';
99
import { IApplicationShell } from '../../../../client/common/application/types';
1010
import { ProductInstaller } from '../../../../client/common/installer/productInstaller';
11-
import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory';
12-
import { PythonExecutionService } from '../../../../client/common/process/pythonProcess';
13-
import { IPythonExecutionService } from '../../../../client/common/process/types';
1411
import { IInstaller, InstallerResponse, Product } from '../../../../client/common/types';
1512
import { DataScience } from '../../../../client/common/utils/localize';
1613
import { Architecture } from '../../../../client/common/utils/platform';
14+
import { InterpreterJupyterKernelSpecCommand, JupyterCommandFactory } from '../../../../client/datascience/jupyter/interpreter/jupyterCommand';
1715
import { JupyterInterpreterDependencyResponse, JupyterInterpreterDependencyService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService';
16+
import { IJupyterCommand, IJupyterCommandFactory } from '../../../../client/datascience/types';
1817
import { InterpreterType, PythonInterpreter } from '../../../../client/interpreter/contracts';
1918

20-
// tslint:disable-next-line: max-func-body-length
19+
// tslint:disable: max-func-body-length no-any
20+
2121
suite('Data Science - Jupyter Interpreter Configuration', () => {
2222
let configuration: JupyterInterpreterDependencyService;
2323
let appShell: IApplicationShell;
2424
let installer: IInstaller;
25-
let pythonExecService: IPythonExecutionService;
25+
let commandFactory: IJupyterCommandFactory;
26+
let command: IJupyterCommand;
2627
const pythonInterpreter: PythonInterpreter = {
2728
path: '',
2829
architecture: Architecture.Unknown,
@@ -33,14 +34,14 @@ suite('Data Science - Jupyter Interpreter Configuration', () => {
3334
setup(() => {
3435
appShell = mock(ApplicationShell);
3536
installer = mock(ProductInstaller);
36-
pythonExecService = mock(PythonExecutionService);
37-
const pythonExecFactory = mock(PythonExecutionFactory);
38-
when(pythonExecFactory.createActivatedEnvironment(anything())).thenResolve(instance(pythonExecService));
39-
// tslint:disable-next-line: no-any
40-
instance(pythonExecService as any).then = undefined;
41-
when(pythonExecService.execModule('jupyter', deepEqual(['kernelspec', '--version']), anything())).thenResolve({ stdout: '' });
37+
commandFactory = mock(JupyterCommandFactory);
38+
command = mock(InterpreterJupyterKernelSpecCommand);
39+
instance(commandFactory as any).then = undefined;
40+
instance(command as any).then = undefined;
41+
when(commandFactory.createInterpreterCommand(anything(), anything(), anything(), anything(), anything())).thenReturn(instance(command));
42+
when(command.exec(anything(), anything())).thenResolve({ stdout: '' });
4243

43-
configuration = new JupyterInterpreterDependencyService(instance(appShell), instance(installer), instance(pythonExecFactory));
44+
configuration = new JupyterInterpreterDependencyService(instance(appShell), instance(installer), instance(commandFactory));
4445
});
4546
test('Return ok if all dependencies are installed', async () => {
4647
when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(true);
@@ -72,7 +73,7 @@ suite('Data Science - Jupyter Interpreter Configuration', () => {
7273
// tslint:disable-next-line: no-any
7374
DataScience.jupyterInstall() as any
7475
);
75-
when(pythonExecService.execModule('jupyter', deepEqual(['kernelspec', '--version']), anything())).thenReject(new Error('Not found'));
76+
when(command.exec(anything(), anything())).thenReject(new Error('Not found'));
7677
when(installer.install(anything(), anything(), anything())).thenResolve(InstallerResponse.Installed);
7778

7879
const response = await configuration.installMissingDependencies(pythonInterpreter);

0 commit comments

Comments
 (0)