Skip to content

Recommend creating venv/conda env in errors #16483

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/interactive-window/interactiveWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export class InteractiveWindow implements IInteractiveWindow {
.then((cell) =>
// If our cell result was a failure show an error
this.errorHandler
.getErrorMessageForDisplayInCell(ex, 'execution', this.owningResource)
.getErrorMessageForDisplayInCellOutput(ex, 'execution', this.owningResource)
.then((message) => this.showErrorForCell(message, cell))
)
.catch(noop);
Expand Down
74 changes: 64 additions & 10 deletions src/kernels/errors/kernelErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
} from '../../platform/interpreter/helpers';
import { JupyterServerCollection } from '../../api';
import { getJupyterDisplayName } from '../jupyter/connection/jupyterServerProviderRegistry';
import { wasIPyKernelInstalAttempted } from '../../platform/interpreter/installer/productInstaller';

/***
* Common code for handling errors.
Expand Down Expand Up @@ -129,6 +130,21 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
}
}
public async getErrorMessageForDisplayInCell(error: Error, errorContext: KernelAction, resource: Resource) {
return this.getErrorMessageForDisplayInCellImpl(error, errorContext, resource, false);
}
public async getErrorMessageForDisplayInCellOutput(
err: Error,
errorContext: KernelAction,
resource: Resource
): Promise<string> {
return this.getErrorMessageForDisplayInCellImpl(err, errorContext, resource, true);
}
private async getErrorMessageForDisplayInCellImpl(
error: Error,
errorContext: KernelAction,
resource: Resource,
displayInCellOutput: boolean
): Promise<string> {
error = WrappedError.unwrap(error);
if (!isCancellationError(error)) {
logger.error(`Error in execution (get message for cell)`, error);
Expand All @@ -138,7 +154,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
// No need to display errors in each cell.
return '';
} else if (error instanceof JupyterKernelDependencyError) {
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
const hasWorkspaceEnv = this.interpreterService
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
: false;
return (
getIPyKernelMissingErrorMessageForCell(
error.kernelConnectionMetadata,
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
) || error.message
);
} else if (error instanceof JupyterInstallError) {
return getJupyterMissingErrorMessageForCell(error) || error.message;
} else if (error instanceof RemoteJupyterServerConnectionError && !isWebExtension()) {
Expand Down Expand Up @@ -180,7 +204,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
) {
// We don't look for ipykernel dependencies before we start a kernel, hence
// its possible the kernel failed to start due to missing dependencies.
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
const hasWorkspaceEnv = this.interpreterService
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
: false;
return (
getIPyKernelMissingErrorMessageForCell(
error.kernelConnectionMetadata,
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
) || error.message
);
} else if (error instanceof BaseKernelError || error instanceof WrappedKernelError) {
const [files, sysPrefix] = await Promise.all([
this.getFilesInWorkingDirectoryThatCouldPotentiallyOverridePythonModules(resource),
Expand All @@ -199,7 +231,15 @@ export abstract class DataScienceErrorHandler implements IDataScienceErrorHandle
failureInfo.reason === KernelFailureReason.moduleNotFoundFailure &&
['ipykernel_launcher', 'ipykernel'].includes(failureInfo.moduleName)
) {
return getIPyKernelMissingErrorMessageForCell(error.kernelConnectionMetadata) || error.message;
const hasWorkspaceEnv = this.interpreterService
? await this.interpreterService.hasWorkspaceSpecificEnvironment()
: false;
return (
getIPyKernelMissingErrorMessageForCell(
error.kernelConnectionMetadata,
!hasWorkspaceEnv && !isWebExtension() && displayInCellOutput
) || error.message
);
}
const messageParts = [failureInfo.message];
if (failureInfo.moreInfoLink) {
Expand Down Expand Up @@ -569,7 +609,10 @@ function getCombinedErrorMessage(prefix: string = '', message: string = '') {
}
return errorMessage;
}
function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnectionMetadata) {
function getIPyKernelMissingErrorMessageForCell(
kernelConnection: KernelConnectionMetadata,
recommendCreatingAndEnvironment: boolean
) {
if (
kernelConnection.kind === 'connectToLiveRemoteKernel' ||
kernelConnection.kind === 'startUsingRemoteKernelSpec' ||
Expand Down Expand Up @@ -602,12 +645,23 @@ function getIPyKernelMissingErrorMessageForCell(kernelConnection: KernelConnecti
getFilePath(kernelConnection.interpreter.uri)
)} -m pip install ${ipyKernelModuleName} -U --user --force-reinstall`;
}
const message = DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter(
displayNameOfKernel,
ProductNames.get(Product.ipykernel)!
);
const installationInstructions = DataScience.installPackageInstructions(ipyKernelName, installerCommand);
return message + '\n' + installationInstructions;
const messages = [
DataScience.libraryRequiredToLaunchJupyterKernelNotInstalledInterpreter(
displayNameOfKernel,
ProductNames.get(Product.ipykernel)!
)
];
if (recommendCreatingAndEnvironment) {
if (wasIPyKernelInstalAttempted(kernelConnection.interpreter)) {
messages.push(DataScience.createANewPythonEnvironment());
} else {
messages.push(DataScience.createANewPythonEnvironment());
messages.push(DataScience.OrInstallPackageInstructions(ipyKernelName, installerCommand));
}
} else {
messages.push(DataScience.installPackageInstructions(ipyKernelName, installerCommand));
}
return messages.join('\n');
}
function getJupyterMissingErrorMessageForCell(err: JupyterInstallError) {
const productNames = `${ProductNames.get(Product.jupyter)} ${Common.and} ${ProductNames.get(Product.notebook)}`;
Expand Down
4 changes: 2 additions & 2 deletions src/kernels/errors/kernelErrorHandler.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d
result,
[
"Running cells with 'condaEnv1 (Python 3.12.7)' requires the ipykernel package.",
"Run the following command to install 'ipykernel' into the Python environment. ",
"Install 'ipykernel' into the Python environment. ",
`Command: 'conda install -n condaEnv1 ipykernel --update-deps --force-reinstall'`
].join('\n')
);
Expand Down Expand Up @@ -790,7 +790,7 @@ Failed to run jupyter as observable with args notebook --no-browser --notebook-d
result,
[
"Running cells with 'Hello (Python 3.12.7)' requires the ipykernel package.",
"Run the following command to install 'ipykernel' into the Python environment. ",
"Install 'ipykernel' into the Python environment. ",
command
].join('\n')
);
Expand Down
4 changes: 4 additions & 0 deletions src/kernels/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface IDataScienceErrorHandler {
* Thus based on the context the error message would be different.
*/
getErrorMessageForDisplayInCell(err: Error, errorContext: KernelAction, resource: Resource): Promise<string>;
/**
* Same as `getErrorMessageForDisplayInCell`, but can contain commands & hyperlinks that can be displayed in the output.
*/
getErrorMessageForDisplayInCellOutput(err: Error, errorContext: KernelAction, resource: Resource): Promise<string>;
}

export abstract class BaseKernelError extends BaseError {
Expand Down
2 changes: 1 addition & 1 deletion src/kernels/kernelDependencyService.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export class KernelDependencyService implements IKernelDependencyService {
.isInstalled(Product.ipykernel, kernelConnection.interpreter)
.then((installed) => installed === true);
installedPromise.then((installed) => {
if (installed) {
if (installed && kernelConnection.interpreter) {
trackPackageInstalledIntoInterpreter(
this.memento,
Product.ipykernel,
Expand Down
2 changes: 1 addition & 1 deletion src/notebooks/controllers/kernelConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class KernelConnector {
// If we failed to start the kernel, then clear cache used to track
// whether we have dependencies installed or not.
// Possible something is missing.
clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter.uri).catch(noop);
clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, metadata.interpreter).catch(noop);
}

const handleResult = await errorHandler.handleKernelError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { CancellationTokenSource, commands, window } from 'vscode';
import type { IExtensionSyncActivationService } from '../../../platform/activation/types';
import { DisposableStore } from '../../../platform/common/utils/lifecycle';
import { injectable } from 'inversify';
import { JVSC_EXTENSION_ID } from '../../../platform/common/constants';
import { PythonEnvKernelConnectionCreator } from '../pythonEnvKernelConnectionCreator.node';

@injectable()
export class EnvironmentCreationCommand implements IExtensionSyncActivationService {
activate(): void {
commands.registerCommand('jupyter.createPythonEnvAndSelectController', async () => {
const editor = window.activeNotebookEditor;
if (!editor) {
return;
}

const disposables = new DisposableStore();
const token = disposables.add(new CancellationTokenSource()).token;
const creator = disposables.add(new PythonEnvKernelConnectionCreator(editor.notebook, token));
const result = await creator.createPythonEnvFromKernelPicker();
if (!result || 'action' in result) {
return;
}

await commands.executeCommand('notebook.selectKernel', {
editor,
id: result.kernelConnection.id,
extension: JVSC_EXTENSION_ID
});
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function findPythonEnvironmentClosestToNotebook(notebook: NotebookDocument, envs
}
}

function findPythonEnvBelongingToFolder(folder: Uri, pythonEnvs: readonly Environment[]) {
export function findPythonEnvBelongingToFolder(folder: Uri, pythonEnvs: readonly Environment[]) {
const localEnvs = pythonEnvs.filter((p) =>
// eslint-disable-next-line local-rules/dont-use-fspath
isParentPath(p.environment?.folderUri?.fsPath || p.executable.uri?.fsPath || p.path, folder.fsPath)
Expand Down
5 changes: 5 additions & 0 deletions src/notebooks/controllers/serviceRegistry.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IServiceManager } from '../../platform/ioc/types';
import { ConnectionDisplayDataProvider } from './connectionDisplayData.node';
import { ControllerRegistration } from './controllerRegistration';
import { registerTypes as registerWidgetTypes } from './ipywidgets/serviceRegistry.node';
import { EnvironmentCreationCommand } from './kernelSource/environmentCreationCommand';
import { KernelSourceCommandHandler } from './kernelSource/kernelSourceCommandHandler';
import { LocalNotebookKernelSourceSelector } from './kernelSource/localNotebookKernelSourceSelector.node';
import { LocalPythonEnvNotebookKernelSourceSelector } from './kernelSource/localPythonEnvKernelSourceSelector.node';
Expand Down Expand Up @@ -42,5 +43,9 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea
IExtensionSyncActivationService,
KernelSourceCommandHandler
);
serviceManager.addSingleton<IExtensionSyncActivationService>(
IExtensionSyncActivationService,
EnvironmentCreationCommand
);
registerWidgetTypes(serviceManager, isDevMode);
}
4 changes: 2 additions & 2 deletions src/notebooks/controllers/vscodeNotebookController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont
await endCellAndDisplayErrorsInCell(
firstCell,
controller,
await errorHandler.getErrorMessageForDisplayInCell(ex, currentContext, doc.uri),
await errorHandler.getErrorMessageForDisplayInCellOutput(ex, currentContext, doc.uri),
isCancelled
);
}
Expand Down Expand Up @@ -646,7 +646,7 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont
await endCellAndDisplayErrorsInCell(
cell,
controller,
await errorHandler.getErrorMessageForDisplayInCell(ex, currentContext, doc.uri),
await errorHandler.getErrorMessageForDisplayInCellOutput(ex, currentContext, doc.uri),
isCancelled
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/notebooks/notebookCommandListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,11 @@ export class NotebookCommandListener implements IDataScienceCommandListener {
await endCellAndDisplayErrorsInCell(
currentCell,
kernel.controller,
await this.errorHandler.getErrorMessageForDisplayInCell(ex, currentContext, kernel.resourceUri),
await this.errorHandler.getErrorMessageForDisplayInCellOutput(
ex,
currentContext,
kernel.resourceUri
),
false
);
} else {
Expand Down
21 changes: 21 additions & 0 deletions src/platform/api/pythonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
} from '../interpreter/helpers';
import { getWorkspaceFolderIdentifier } from '../common/application/workspace.base';
import { trackInterpreterDiscovery, trackPythonExtensionActivation } from '../../kernels/telemetry/notebookTelemetry';
import { findPythonEnvBelongingToFolder } from '../../notebooks/controllers/preferredKernelConnectionService.node';

export function deserializePythonEnvironment(
pythonVersion: Partial<PythonEnvironment_PythonApi> | undefined,
Expand Down Expand Up @@ -351,6 +352,26 @@ export class InterpreterService implements IInterpreterService {
public initialize() {
this.hookupOnDidChangeInterpreterEvent();
}
public async hasWorkspaceSpecificEnvironment(): Promise<boolean> {
if (!(workspace.workspaceFolders || []).length) {
return false;
}

const api = await this.getApi();
if (!api) {
return false;
}
if (
(workspace.workspaceFolders || []).some((folder) =>
findPythonEnvBelongingToFolder(folder.uri, api.environments.known)
)
) {
return true;
}

return false;
}

public async resolveEnvironment(id: string | Environment): Promise<ResolvedEnvironment | undefined> {
return this.getApi().then((api) => {
if (!api) {
Expand Down
10 changes: 6 additions & 4 deletions src/platform/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,14 @@ export namespace DataScience {
pythonEnvName: string,
pythonModuleName: string
) => l10n.t("Running cells with '{0}' requires the {1} package.", pythonEnvName, pythonModuleName);
export const installPackageInstructions = (pythonModuleName: string, commandId: string) =>
export const createANewPythonEnvironment = () =>
l10n.t(
"Run the following command to install '{0}' into the Python environment. \nCommand: '{1}'",
pythonModuleName,
commandId
'[Create a Python Environment](command:jupyter.createPythonEnvAndSelectController) with the required packages.'
);
export const installPackageInstructions = (pythonModuleName: string, commandId: string) =>
l10n.t("Install '{0}' into the Python environment. \nCommand: '{1}'", pythonModuleName, commandId);
export const OrInstallPackageInstructions = (pythonModuleName: string, commandId: string) =>
l10n.t("Or install '{0}' using the command: '{1}'", pythonModuleName, commandId);
export const pythonCondaKernelsWithoutPython = l10n.t(
'The Python Runtime and IPyKernel will be automatically installed upon selecting this environment.'
);
Expand Down
1 change: 1 addition & 0 deletions src/platform/interpreter/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const IInterpreterService = Symbol('IInterpreterService');
export interface IInterpreterService {
// #region New API
resolveEnvironment(id: string | Environment): Promise<ResolvedEnvironment | undefined>;
hasWorkspaceSpecificEnvironment(): Promise<boolean>;
// #endregion

// #region Old API
Expand Down
19 changes: 14 additions & 5 deletions src/platform/interpreter/installer/productInstaller.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Memento, Uri } from 'vscode';
import { Memento } from 'vscode';
import { ProductNames } from './productNames';
import { Product } from './types';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { InterpreterUri } from '../../common/types';
import { isResource } from '../../common/utils/misc';
import { getInterpreterHash } from '../../pythonEnvironments/info/interpreter';

const interpretersIntoWhichIPyKernelWasInstalledInSession = new Set<string>();
/**
* Keep track of the fact that we attempted to install a package into an interpreter.
* (don't care whether it was successful or not).
*/
export async function trackPackageInstalledIntoInterpreter(
memento: Memento,
product: Product,
interpreter: InterpreterUri
interpreter: PythonEnvironment
) {
if (isResource(interpreter)) {
return;
}
interpretersIntoWhichIPyKernelWasInstalledInSession.add(interpreter.id);
const key = `${await getInterpreterHash(interpreter)}#${ProductNames.get(product)}`;
await memento.update(key, true);
}
export async function clearInstalledIntoInterpreterMemento(memento: Memento, product: Product, interpreterPath: Uri) {
const key = `${await getInterpreterHash({ uri: interpreterPath })}#${ProductNames.get(product)}`;
export async function clearInstalledIntoInterpreterMemento(
memento: Memento,
product: Product,
interpreterPath: PythonEnvironment
) {
const key = `${await getInterpreterHash(interpreterPath)}#${ProductNames.get(product)}`;
await memento.update(key, undefined);
}
export async function isModulePresentInEnvironmentCache(
Expand All @@ -36,3 +41,7 @@ export async function isModulePresentInEnvironmentCache(
const key = `${await getInterpreterHash(interpreter)}#${ProductNames.get(product)}`;
return memento.get<boolean>(key, false);
}

export function wasIPyKernelInstalAttempted(interpreter: PythonEnvironment): boolean {
return interpretersIntoWhichIPyKernelWasInstalledInSession.has(interpreter.id);
}
Loading