Skip to content

Commit 8ec4cd7

Browse files
authored
Auto select kernel when starting jupyter (#8914)
* Kernel Selection UI
1 parent 5c3433b commit 8ec4cd7

39 files changed

+1168
-1123
lines changed

package.nls.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@
169169
"DataScience.redo": "Redo",
170170
"DataScience.clearAll": "Remove all cells",
171171
"DataScience.pythonVersionHeader": "Python version:",
172-
"DataScience.pythonVersionHeaderNoPyKernel": "Python version may not match, no ipykernel found:",
173172
"DataScience.pythonRestartHeader": "Restarted kernel:",
174173
"DataScience.pythonNewHeader": "Started new kernel:",
175174
"DataScience.executingCodeFailure": "Executing code failed : {0}",
@@ -387,5 +386,9 @@
387386
"DataScience.jupyterSelectURINewLabel" : "Existing",
388387
"DataScience.jupyterSelectURINewDetail" : "Specify the URI of an existing server",
389388
"DataScience.jupyterSelectURIRunningDetailFormat" : "Last activity {0}. {1} existing connections.",
390-
"DataScience.jupyterSelectURINotRunningDetail": "Cannot connect at this time. Status unknown."
389+
"DataScience.jupyterSelectURINotRunningDetail": "Cannot connect at this time. Status unknown.",
390+
"DataScience.fallbackToUseActiveInterpeterAsKernel": "Couldn't find kernel '{0}' that the notebook was created with. Using the current interpreter.",
391+
"DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel": "Couldn't find kernel '{0}' that the notebook was created with. Registering a new kernel using the current interpreter.",
392+
"DataScience.fallBackToPromptToUseActiveInterpreterOrSelectAKernel": "Couldn't find kernel '{0}' that the notebook was created with.",
393+
"DataScience.kernelDescriptionForKernelPicker": "(kernel)"
391394
}

package.nls.nl.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@
9494
"DataScience.redo": "Opnieuw uitvoeren",
9595
"DataScience.clearAll": "Alle cellen verwijderen",
9696
"DataScience.pythonVersionHeader": "Python versie:",
97-
"DataScience.pythonVersionHeaderNoPyKernel": "Python versie kan niet overeenkomen, geen ipykernel gevonden:",
9897
"DataScience.pythonRestartHeader": "Kernel herstart:",
9998
"Linter.InstalledButNotEnabled": "Linter {0} is geinstalleerd maar niet ingeschakeld.",
10099
"Linter.replaceWithSelectedLinter": "Meerdere linters zijn ingeschakeld in de instellingen. Vervangen met '{0}'?",

pythonFiles/datascience/jupyter_daemon.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4+
import json
45
import sys
56
import logging
67
import os
@@ -25,7 +26,9 @@ def m_exec_module(self, module_name, args=[], cwd=None, env=None):
2526
self.log.info("Exec in DS Daemon %s with args %s", module_name, args)
2627
args = [] if args is None else args
2728

28-
if module_name == "jupyter" and args == ["kernelspec", "list"]:
29+
if module_name == "jupyter" and args == ["kernelspec", "list", "--json"]:
30+
return self._execute_and_capture_output(self._print_kernel_list_json)
31+
elif module_name == "jupyter" and args == ["kernelspec", "list"]:
2932
return self._execute_and_capture_output(self._print_kernel_list)
3033
elif module_name == "jupyter" and args == ["kernelspec", "--version"]:
3134
return self._execute_and_capture_output(self._print_kernelspec_version)
@@ -76,7 +79,7 @@ def _print_kernelspec_version(self):
7679
sys.stdout.flush()
7780

7881
def _print_kernel_list(self):
79-
self.log.info("check kernels")
82+
self.log.info("listing kernels")
8083
# Get kernel specs.
8184
import jupyter_client.kernelspec
8285

@@ -86,6 +89,17 @@ def _print_kernel_list(self):
8689
)
8790
sys.stdout.flush()
8891

92+
def _print_kernel_list_json(self):
93+
self.log.info("listing kernels as json")
94+
# Get kernel specs.
95+
import jupyter_client.kernelspec
96+
specs = jupyter_client.kernelspec.KernelSpecManager().get_all_specs()
97+
all_specs = {
98+
"kernelspecs": specs
99+
}
100+
sys.stdout.write(json.dumps(all_specs))
101+
sys.stdout.flush()
102+
89103
def _convert(self, args):
90104
self.log.info("nbconvert")
91105
from nbconvert import nbconvertapp as app

src/client/common/process/pythonDaemon.ts

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ export class ConnectionClosedError extends Error {
3434
super();
3535
}
3636
}
37+
38+
export class DaemonError extends Error {
39+
constructor(public readonly message: string){
40+
super();
41+
}
42+
}
3743
export class PythonDaemonExecutionService implements IPythonDaemonExecutionService {
3844
private connectionClosedMessage: string = '';
3945
private outputObservale = new Subject<Output<string>>();
@@ -66,8 +72,8 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
6672
this.disposables.forEach(item => item.dispose());
6773
}
6874
public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> {
69-
this.throwIfRPCConnectionIsDead();
7075
try {
76+
this.throwIfRPCConnectionIsDead();
7177
type InterpreterInfoResponse = ErrorResponse & { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean };
7278
const request = new RequestType0<InterpreterInfoResponse, void, void>('get_interpreter_information');
7379
const response = await this.sendRequestWithoutArgs(request);
@@ -79,69 +85,104 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
7985
sysVersion: response.sysVersion,
8086
sysPrefix: response.sysPrefix
8187
};
82-
} catch {
88+
} catch (ex) {
89+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
8390
return this.pythonExecutionService.getInterpreterInformation();
8491
}
8592
}
8693
public async getExecutablePath(): Promise<string> {
87-
this.throwIfRPCConnectionIsDead();
8894
try {
95+
this.throwIfRPCConnectionIsDead();
8996
type ExecutablePathResponse = ErrorResponse & { path: string };
9097
const request = new RequestType0<ExecutablePathResponse, void, void>('get_executable');
9198
const response = await this.sendRequestWithoutArgs(request);
9299
if (response.error) {
93-
throw new Error(response.error);
100+
throw new DaemonError(response.error);
94101
}
95102
return response.path;
96-
} catch {
103+
} catch (ex) {
104+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
97105
return this.pythonExecutionService.getExecutablePath();
98106
}
99107
}
100108
public getExecutionInfo(args: string[]): PythonExecutionInfo {
101109
return this.pythonExecutionService.getExecutionInfo(args);
102110
}
103111
public async isModuleInstalled(moduleName: string): Promise<boolean> {
104-
this.throwIfRPCConnectionIsDead();
105112
try {
113+
this.throwIfRPCConnectionIsDead();
106114
type ModuleInstalledResponse = ErrorResponse & { exists: boolean };
107115
const request = new RequestType<{ module_name: string }, ModuleInstalledResponse, void, void>('is_module_installed');
108116
const response = await this.sendRequest(request, { module_name: moduleName });
109117
if (response.error) {
110-
throw new Error(response.error);
118+
throw new DaemonError(response.error);
111119
}
112120
return response.exists;
113-
} catch {
121+
} catch (ex) {
122+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
114123
return this.pythonExecutionService.isModuleInstalled(moduleName);
115124
}
116125
}
117126
public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
118-
this.throwIfRPCConnectionIsDead();
119-
if (this.canExecFileUsingDaemon(args, options)) {
120-
return this.execFileWithDaemonAsObservable(args[0], args.slice(1), options);
127+
if (this.isAlive && this.canExecFileUsingDaemon(args, options)) {
128+
try {
129+
return this.execAsObservable({ fileName: args[0] }, args.slice(1), options);
130+
} catch (ex) {
131+
if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) {
132+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
133+
return this.pythonExecutionService.execObservable(args, options);
134+
} else {
135+
throw ex;
136+
}
137+
}
121138
} else {
122139
return this.pythonExecutionService.execObservable(args, options);
123140
}
124141
}
125142
public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
126-
this.throwIfRPCConnectionIsDead();
127-
if (this.canExecModuleUsingDaemon(moduleName, args, options)) {
128-
return this.execModuleWithDaemonAsObservable(moduleName, args, options);
143+
if (this.isAlive && this.canExecModuleUsingDaemon(moduleName, args, options)) {
144+
try {
145+
return this.execAsObservable({ moduleName }, args, options);
146+
} catch (ex) {
147+
if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) {
148+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
149+
return this.pythonExecutionService.execModuleObservable(moduleName, args, options);
150+
} else {
151+
throw ex;
152+
}
153+
}
129154
} else {
130155
return this.pythonExecutionService.execModuleObservable(moduleName, args, options);
131156
}
132157
}
133158
public async exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> {
134-
this.throwIfRPCConnectionIsDead();
135-
if (this.canExecFileUsingDaemon(args, options)) {
136-
return this.execFileWithDaemon(args[0], args.slice(1), options);
159+
if (this.isAlive && this.canExecFileUsingDaemon(args, options)) {
160+
try {
161+
return await this.execFileWithDaemon(args[0], args.slice(1), options);
162+
} catch (ex) {
163+
if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) {
164+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
165+
return this.pythonExecutionService.exec(args, options);
166+
} else {
167+
throw ex;
168+
}
169+
}
137170
} else {
138171
return this.pythonExecutionService.exec(args, options);
139172
}
140173
}
141174
public async execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> {
142-
this.throwIfRPCConnectionIsDead();
143-
if (this.canExecModuleUsingDaemon(moduleName, args, options)) {
144-
return this.execModuleWithDaemon(moduleName, args, options);
175+
if (this.isAlive && this.canExecModuleUsingDaemon(moduleName, args, options)) {
176+
try {
177+
return await this.execModuleWithDaemon(moduleName, args, options);
178+
} catch (ex) {
179+
if (ex instanceof DaemonError || ex instanceof ConnectionClosedError) {
180+
traceWarning('Falling back to Python Execution Service due to failure in daemon', ex);
181+
return this.pythonExecutionService.execModule(moduleName, args, options);
182+
} else {
183+
throw ex;
184+
}
185+
}
145186
} else {
146187
return this.pythonExecutionService.execModule(moduleName, args, options);
147188
}
@@ -174,7 +215,7 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
174215
*/
175216
private processResponse(response: { error?: string | undefined; stdout: string; stderr?: string }, options: SpawnOptions) {
176217
if (response.error) {
177-
throw new StdErrError(`Failed to execute using the daemon, ${response.error}`);
218+
throw new DaemonError(`Failed to execute using the daemon, ${response.error}`);
178219
}
179220
// Throw an error if configured to do so if there's any output in stderr.
180221
if (response.stderr && options.throwOnStdErr) {
@@ -193,9 +234,6 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
193234
this.processResponse(response, options);
194235
return response;
195236
}
196-
private execFileWithDaemonAsObservable(fileName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
197-
return this.execAsObservable({ fileName }, args, options);
198-
}
199237
private async execModuleWithDaemon(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> {
200238
type ExecResponse = ErrorResponse & { stdout: string; stderr?: string };
201239
// tslint:disable-next-line: no-any
@@ -204,9 +242,6 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
204242
this.processResponse(response, options);
205243
return response;
206244
}
207-
private execModuleWithDaemonAsObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
208-
return this.execAsObservable({ moduleName }, args, options);
209-
}
210245
private execAsObservable(moduleOrFile: { moduleName: string } | { fileName: string }, args: string[], options: SpawnOptions): ObservableExecutionResult<string> {
211246
const subject = new Subject<Output<string>>();
212247
const start = async () => {
@@ -223,7 +258,7 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
223258
}
224259
// Might not get a response object back, as its observable.
225260
if (response && response.error){
226-
throw new StdErrError(response.error);
261+
throw new DaemonError(response.error);
227262
}
228263
};
229264
let stdErr = '';
@@ -280,21 +315,22 @@ export class PythonDaemonExecutionService implements IPythonDaemonExecutionServi
280315
this.connection.onNotification(OuputNotification, output => this.outputObservale.next(output));
281316
const logNotification = new NotificationType<{level: 'WARN'|'WARNING'|'INFO'|'DEBUG'|'NOTSET'; msg: string}, void>('log');
282317
this.connection.onNotification(logNotification, output => {
318+
const msg = `Python Daemon: ${output.msg}`;
283319
if (output.level === 'DEBUG' || output.level === 'NOTSET'){
284-
traceVerbose(output.msg);
320+
traceVerbose(msg);
285321
} else if (output.level === 'INFO'){
286-
traceInfo(output.msg);
322+
traceInfo(msg);
287323
} else if (output.level === 'WARN' || output.level === 'WARNING') {
288-
traceWarning(output.msg);
324+
traceWarning(msg);
289325
} else {
290-
traceError(output.msg);
326+
traceError(msg);
291327
}
292328
});
293329
this.connection.onUnhandledNotification(traceError);
294330
}
295331
private throwIfRPCConnectionIsDead() {
296-
if (this.connectionClosedMessage) {
297-
throw new Error(this.connectionClosedMessage);
332+
if (!this.isAlive) {
333+
throw new ConnectionClosedError(this.connectionClosedMessage);
298334
}
299335
}
300336
}

src/client/common/utils/localize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,6 @@ export namespace DataScience {
176176
export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:');
177177
export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:');
178178
export const pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new kernel:');
179-
export const pythonVersionHeaderNoPyKernel = localize('DataScience.pythonVersionHeaderNoPyKernel', 'Python Version may not match, no ipykernel found:');
180179

181180
export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of the running Jupyter server');
182181
export const jupyterSelectURIQuickPickTitle = localize('DataScience.jupyterSelectURIQuickPickTitle', 'Pick how to connect to Jupyter');
@@ -304,6 +303,10 @@ export namespace DataScience {
304303
export const gatheredScriptDescription = localize('DataScience.gatheredScriptDescription', '# This file contains only the code required to produce the results of the gathered cell.\n');
305304
export const gatheredNotebookDescriptionInMarkdown = localize('DataScience.gatheredNotebookDescriptionInMarkdown', '# Gathered Notebook\nGenerated from ```{0}```\n\nThis notebook contains only the code and cells required to produce the same results as the gathered cell.\n\nPlease note that the python analysis is quite conservative, so if it is unsure whether a line of code is necessary for execution, it will err on the side of including it.');
306305
export const savePngTitle = localize('DataScience.savePngTitle', 'Save Image');
306+
export const fallbackToUseActiveInterpeterAsKernel = localize('DataScience.fallbackToUseActiveInterpeterAsKernel', 'Couldn\'t find kernel \'{0}\' that the notebook was created with. Using the current interpreter.');
307+
export const fallBackToRegisterAndUseActiveInterpeterAsKernel = localize('DataScience.fallBackToRegisterAndUseActiveInterpeterAsKernel', 'Couldn\'t find kernel \'{0}\' that the notebook was created with. Registering a new kernel using the current interpreter.');
308+
export const kernelDescriptionForKernelPicker = localize('DataScience.kernelDescriptionForKernelPicker', '(kernel)');
309+
export const fallBackToPromptToUseActiveInterpreterOrSelectAKernel = localize('DataScience.fallBackToPromptToUseActiveInterpreterOrSelectAKernel', 'Couldn\'t find kernel \'{0}\' that the notebook was created with.');
307310
}
308311

309312
export namespace DebugConfigStrings {

src/client/datascience/constants.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,7 @@ export namespace CodeSnippits {
319319
export enum JupyterCommands {
320320
NotebookCommand = 'notebook',
321321
ConvertCommand = 'nbconvert',
322-
KernelSpecCommand = 'kernelspec',
323-
KernelCreateCommand = 'ipykernel'
324-
322+
KernelSpecCommand = 'kernelspec'
325323
}
326324

327325
export namespace LiveShare {
@@ -341,8 +339,6 @@ export namespace LiveShare {
341339
export namespace LiveShareCommands {
342340
export const isNotebookSupported = 'isNotebookSupported';
343341
export const isImportSupported = 'isImportSupported';
344-
export const isKernelCreateSupported = 'isKernelCreateSupported';
345-
export const isKernelSpecSupported = 'isKernelSpecSupported';
346342
export const connectToNotebookServer = 'connectToNotebookServer';
347343
export const getUsableJupyterPython = 'getUsableJupyterPython';
348344
export const executeObservable = 'executeObservable';

src/client/datascience/data-viewing/dataViewerProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable
4343
}
4444

4545
public async getPandasVersion(notebook: INotebook): Promise<{ major: number; minor: number; build: number } | undefined> {
46-
const interpreter = await notebook.getMatchingInterpreter();
46+
const interpreter = notebook.getMatchingInterpreter();
4747

4848
if (interpreter) {
4949
const launcher = await this.pythonFactory.createActivatedEnvironment({ resource: undefined, interpreter, allowEnvironmentFetchExceptions: true });

src/client/datascience/interactive-common/intellisense/intellisenseProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class IntellisenseProvider implements IInteractiveWindowListener {
157157

158158
// Interpreter should be the interpreter currently active in the notebook
159159
const activeNotebook = await this.getNotebook();
160-
const interpreter = activeNotebook ? await activeNotebook.getMatchingInterpreter() : await this.interpreterService.getActiveInterpreter(resource);
160+
const interpreter = activeNotebook ? activeNotebook.getMatchingInterpreter() : await this.interpreterService.getActiveInterpreter(resource);
161161

162162
// See if the resource or the interpreter are different
163163
if (resource?.toString() !== this.resource?.toString() || interpreter?.path !== this.interpreter?.path || this.languageServer === undefined) {

src/client/datascience/interactive-common/interactiveBase.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,10 +1106,6 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
11061106
private async generateSysInfoMessage(reason: SysInfoReason): Promise<string> {
11071107
switch (reason) {
11081108
case SysInfoReason.Start:
1109-
// Message depends upon if ipykernel is supported or not.
1110-
if (!(await this.jupyterExecution.isKernelCreateSupported())) {
1111-
return localize.DataScience.pythonVersionHeaderNoPyKernel();
1112-
}
11131109
return localize.DataScience.pythonVersionHeader();
11141110
break;
11151111
case SysInfoReason.Restart:

0 commit comments

Comments
 (0)