Skip to content

Commit af1aaf5

Browse files
authored
Recommend creating venv/conda env in errors (#16483)
* Recommend creating venv/conda env in errors * Fix formatting * Fixes * errors
1 parent 2fbe67d commit af1aaf5

File tree

16 files changed

+177
-37
lines changed

16 files changed

+177
-37
lines changed

src/interactive-window/interactiveWindow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export class InteractiveWindow implements IInteractiveWindow {
381381
.then((cell) =>
382382
// If our cell result was a failure show an error
383383
this.errorHandler
384-
.getErrorMessageForDisplayInCell(ex, 'execution', this.owningResource)
384+
.getErrorMessageForDisplayInCellOutput(ex, 'execution', this.owningResource)
385385
.then((message) => this.showErrorForCell(message, cell))
386386
)
387387
.catch(noop);

src/kernels/errors/kernelErrorHandler.ts

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
} from '../../platform/interpreter/helpers';
6969
import { JupyterServerCollection } from '../../api';
7070
import { getJupyterDisplayName } from '../jupyter/connection/jupyterServerProviderRegistry';
71+
import { wasIPyKernelInstalAttempted } from '../../platform/interpreter/installer/productInstaller';
7172

7273
/***
7374
* Common code for handling errors.
@@ -129,6 +130,21 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
129130
}
130131
}
131132
public async getErrorMessageForDisplayInCell(error: Error, errorContext: KernelAction, resource: Resource) {
133+
return this.getErrorMessageForDisplayInCellImpl(error, errorContext, resource, false);
134+
}
135+
public async getErrorMessageForDisplayInCellOutput(
136+
err: Error,
137+
errorContext: KernelAction,
138+
resource: Resource
139+
): Promise<string> {
140+
return this.getErrorMessageForDisplayInCellImpl(err, errorContext, resource, true);
141+
}
142+
private async getErrorMessageForDisplayInCellImpl(
143+
error: Error,
144+
errorContext: KernelAction,
145+
resource: Resource,
146+
displayInCellOutput: boolean
147+
): Promise<string> {
132148
error = WrappedError.unwrap(error);
133149
if (!isCancellationError(error)) {
134150
logger.error(`Error in execution (get message for cell)`, error);
@@ -138,7 +154,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
138154
// No need to display errors in each cell.
139155
return '';
140156
} else if (error instanceof JupyterKernelDependencyError) {
141-
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
157+
const hasWorkspaceEnv = this.interpreterService
158+
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
159+
: false;
160+
return (
161+
getIPyKernelMissingErrorMessageForCell(
162+
error.kernelConnectionMetadata,
163+
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
164+
) || error.message
165+
);
142166
} else if (error instanceof JupyterInstallError) {
143167
return getJupyterMissingErrorMessageForCell(error) || error.message;
144168
} else if (error instanceof RemoteJupyterServerConnectionError && !isWebExtension()) {
@@ -180,7 +204,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
180204
) {
181205
// We don't look for ipykernel dependencies before we start a kernel, hence
182206
// its possible the kernel failed to start due to missing dependencies.
183-
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
207+
const hasWorkspaceEnv = this.interpreterService
208+
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
209+
: false;
210+
return (
211+
getIPyKernelMissingErrorMessageForCell(
212+
error.kernelConnectionMetadata,
213+
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
214+
) || error.message
215+
);
184216
} else if (error instanceof BaseKernelError || error instanceof WrappedKernelError) {
185217
const [files, sysPrefix] = await Promise.all([
186218
this.getFilesInWorkingDirectoryThatCouldPotentiallyOverridePythonModules(resource),
@@ -199,7 +231,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
199231
failureInfo.reason === KernelFailureReason.moduleNotFoundFailure &&
200232
['ipykernel_launcher', 'ipykernel'].includes(failureInfo.moduleName)
201233
) {
202-
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
234+
const hasWorkspaceEnv = this.interpreterService
235+
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
236+
: false;
237+
return (
238+
getIPyKernelMissingErrorMessageForCell(
239+
error.kernelConnectionMetadata,
240+
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
241+
) || error.message
242+
);
203243
}
204244
const messageParts = [failureInfo.message];
205245
if (failureInfo.moreInfoLink) {
@@ -569,7 +609,10 @@ function getCombinedErrorMessage(prefix: string = '', message: string = '') {
569609
}
570610
return errorMessage;
571611
}
572-
function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnectionMetadata) {
612+
function getIPyKernelMissingErrorMessageForCell(
613+
kernelConnection: KernelConnectionMetadata,
614+
recommendCreatingAndEnvironment: boolean
615+
) {
573616
if (
574617
kernelConnection.kind === 'connectToLiveRemoteKernel' ||
575618
kernelConnection.kind === 'startUsingRemoteKernelSpec' ||
@@ -602,12 +645,23 @@ function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnecti
602645
getFilePath(kernelConnection.interpreter.uri)
603646
)} -m pip install ${ipyKernelModuleName} -U --user --force-reinstall`;
604647
}
605-
const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter(
606-
displayNameOfKernel,
607-
ProductNames.get(Product.ipykernel)!
608-
);
609-
const installationInstructions = DataScience.installPackageInstructions(ipyKernelName, installerCommand);
610-
return message + '\n' + installationInstructions;
648+
const messages = [
649+
DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter(
650+
displayNameOfKernel,
651+
ProductNames.get(Product.ipykernel)!
652+
)
653+
];
654+
if (recommendCreatingAndEnvironment) {
655+
if (wasIPyKernelInstalAttempted(kernelConnection.interpreter)) {
656+
messages.push(DataScience.createANewPythonEnvironment());
657+
} else {
658+
messages.push(DataScience.createANewPythonEnvironment());
659+
messages.push(DataScience.OrInstallPackageInstructions(ipyKernelName, installerCommand));
660+
}
661+
} else {
662+
messages.push(DataScience.installPackageInstructions(ipyKernelName, installerCommand));
663+
}
664+
return messages.join('\n');
611665
}
612666
function getJupyterMissingErrorMessageForCell(err: JupyterInstallError) {
613667
const productNames = `${ProductNames.get(Product.jupyter)} ${Common.and} ${ProductNames.get(Product.notebook)}`;

src/kernels/errors/kernelErrorHandler.unit.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,7 +738,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d
738738
result,
739739
[
740740
"Running cells with 'condaEnv1 (Python 3.12.7)' requires the ipykernel package.",
741-
"Run the following command to install 'ipykernel' into the Python environment. ",
741+
"Install 'ipykernel' into the Python environment. ",
742742
`Command: 'conda install -n condaEnv1 ipykernel --update-deps --force-reinstall'`
743743
].join('\n')
744744
);
@@ -790,7 +790,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d
790790
result,
791791
[
792792
"Running cells with 'Hello (Python 3.12.7)' requires the ipykernel package.",
793-
"Run the following command to install 'ipykernel' into the Python environment. ",
793+
"Install 'ipykernel' into the Python environment. ",
794794
command
795795
].join('\n')
796796
);

src/kernels/errors/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export interface IDataScienceErrorHandler {
3333
* Thus based on the context the error message would be different.
3434
*/
3535
getErrorMessageForDisplayInCell(err: Error, errorContext: KernelAction, resource: Resource): Promise<string>;
36+
/**
37+
* Same as `getErrorMessageForDisplayInCell`, but can contain commands & hyperlinks that can be displayed in the output.
38+
*/
39+
getErrorMessageForDisplayInCellOutput(err: Error, errorContext: KernelAction, resource: Resource): Promise<string>;
3640
}
3741

3842
export abstract class BaseKernelError extends BaseError {

src/kernels/kernelDependencyService.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class KernelDependencyService implements IKernelDependencyService {
199199
.isInstalled(Product.ipykernel, kernelConnection.interpreter)
200200
.then((installed) => installed === true);
201201
installedPromise.then((installed) => {
202-
if (installed) {
202+
if (installed && kernelConnection.interpreter) {
203203
trackPackageInstalledIntoInterpreter(
204204
this.memento,
205205
Product.ipykernel,

src/notebooks/controllers/kernelConnector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export class KernelConnector {
104104
// If we failed to start the kernel, then clear cache used to track
105105
// whether we have dependencies installed or not.
106106
// Possible something is missing.
107-
clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter.uri).catch(noop);
107+
clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter).catch(noop);
108108
}
109109

110110
const handleResult = await errorHandler.handleKernelError(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { CancellationTokenSource, commands, window } from 'vscode';
5+
import type { IExtensionSyncActivationService } from '../../../platform/activation/types';
6+
import { DisposableStore } from '../../../platform/common/utils/lifecycle';
7+
import { injectable } from 'inversify';
8+
import { JVSC_EXTENSION_ID } from '../../../platform/common/constants';
9+
import { PythonEnvKernelConnectionCreator } from '../pythonEnvKernelConnectionCreator.node';
10+
11+
@injectable()
12+
export class EnvironmentCreationCommand implements IExtensionSyncActivationService {
13+
activate(): void {
14+
commands.registerCommand('jupyter.createPythonEnvAndSelectController', async () => {
15+
const editor = window.activeNotebookEditor;
16+
if (!editor) {
17+
return;
18+
}
19+
20+
const disposables = new DisposableStore();
21+
const token = disposables.add(new CancellationTokenSource()).token;
22+
const creator = disposables.add(new PythonEnvKernelConnectionCreator(editor.notebook, token));
23+
const result = await creator.createPythonEnvFromKernelPicker();
24+
if (!result || 'action' in result) {
25+
return;
26+
}
27+
28+
await commands.executeCommand('notebook.selectKernel', {
29+
editor,
30+
id: result.kernelConnection.id,
31+
extension: JVSC_EXTENSION_ID
32+
});
33+
});
34+
}
35+
}

src/notebooks/controllers/preferredKernelConnectionService.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function findPythonEnvironmentClosestToNotebook(notebook: NotebookDocument, envs
4343
}
4444
}
4545

46-
function findPythonEnvBelongingToFolder(folder: Uri, pythonEnvs: readonly Environment[]) {
46+
export function findPythonEnvBelongingToFolder(folder: Uri, pythonEnvs: readonly Environment[]) {
4747
const localEnvs = pythonEnvs.filter((p) =>
4848
// eslint-disable-next-line local-rules/dont-use-fspath
4949
isParentPath(p.environment?.folderUri?.fsPath || p.executable.uri?.fsPath || p.path, folder.fsPath)

src/notebooks/controllers/serviceRegistry.node.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { IServiceManager } from '../../platform/ioc/types';
66
import { ConnectionDisplayDataProvider } from './connectionDisplayData.node';
77
import { ControllerRegistration } from './controllerRegistration';
88
import { registerTypes as registerWidgetTypes } from './ipywidgets/serviceRegistry.node';
9+
import { EnvironmentCreationCommand } from './kernelSource/environmentCreationCommand';
910
import { KernelSourceCommandHandler } from './kernelSource/kernelSourceCommandHandler';
1011
import { LocalNotebookKernelSourceSelector } from './kernelSource/localNotebookKernelSourceSelector.node';
1112
import { LocalPythonEnvNotebookKernelSourceSelector } from './kernelSource/localPythonEnvKernelSourceSelector.node';
@@ -42,5 +43,9 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea
4243
IExtensionSyncActivationService,
4344
KernelSourceCommandHandler
4445
);
46+
serviceManager.addSingleton<IExtensionSyncActivationService>(
47+
IExtensionSyncActivationService,
48+
EnvironmentCreationCommand
49+
);
4550
registerWidgetTypes(serviceManager, isDevMode);
4651
}

src/notebooks/controllers/vscodeNotebookController.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont
602602
await endCellAndDisplayErrorsInCell(
603603
firstCell,
604604
controller,
605-
await errorHandler.getErrorMessageForDisplayInCell(ex, currentContext, doc.uri),
605+
await errorHandler.getErrorMessageForDisplayInCellOutput(ex, currentContext, doc.uri),
606606
isCancelled
607607
);
608608
}
@@ -646,7 +646,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont
646646
await endCellAndDisplayErrorsInCell(
647647
cell,
648648
controller,
649-
await errorHandler.getErrorMessageForDisplayInCell(ex, currentContext, doc.uri),
649+
await errorHandler.getErrorMessageForDisplayInCellOutput(ex, currentContext, doc.uri),
650650
isCancelled
651651
);
652652
}

0 commit comments

Comments
 (0)