diff --git a/src/interactive-window/interactiveWindow.ts b/src/interactive-window/interactiveWindow.ts index 0a16901bfef..bff7416e113 100644 --- a/src/interactive-window/interactiveWindow.ts +++ b/src/interactive-window/interactiveWindow.ts @@ -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); diff --git a/src/kernels/errors/kernelErrorHandler.ts b/src/kernels/errors/kernelErrorHandler.ts index 4f4c6d9a393..c8cca83f24b 100644 --- a/src/kernels/errors/kernelErrorHandler.ts +++ b/src/kernels/errors/kernelErrorHandler.ts @@ -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. @@ -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 { + return this.getErrorMessageForDisplayInCellImpl(err, errorContext, resource, true); + } + private async getErrorMessageForDisplayInCellImpl( + error: Error, + errorContext: KernelAction, + resource: Resource, + displayInCellOutput: boolean + ): Promise { error = WrappedError.unwrap(error); if (!isCancellationError(error)) { logger.error(`Error in execution (get message for cell)`, error); @@ -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()) { @@ -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), @@ -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) { @@ -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' || @@ -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)}`; diff --git a/src/kernels/errors/kernelErrorHandler.unit.test.ts b/src/kernels/errors/kernelErrorHandler.unit.test.ts index cd6e86655bf..31c2181ca5a 100644 --- a/src/kernels/errors/kernelErrorHandler.unit.test.ts +++ b/src/kernels/errors/kernelErrorHandler.unit.test.ts @@ -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') ); @@ -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') ); diff --git a/src/kernels/errors/types.ts b/src/kernels/errors/types.ts index 04ac5515877..b862b54f86f 100644 --- a/src/kernels/errors/types.ts +++ b/src/kernels/errors/types.ts @@ -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; + /** + * Same as `getErrorMessageForDisplayInCell`, but can contain commands & hyperlinks that can be displayed in the output. + */ + getErrorMessageForDisplayInCellOutput(err: Error, errorContext: KernelAction, resource: Resource): Promise; } export abstract class BaseKernelError extends BaseError { diff --git a/src/kernels/kernelDependencyService.node.ts b/src/kernels/kernelDependencyService.node.ts index d77f2bd5a5a..4c8b57190fc 100644 --- a/src/kernels/kernelDependencyService.node.ts +++ b/src/kernels/kernelDependencyService.node.ts @@ -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, diff --git a/src/notebooks/controllers/kernelConnector.ts b/src/notebooks/controllers/kernelConnector.ts index 40c7d91227b..234703ba034 100644 --- a/src/notebooks/controllers/kernelConnector.ts +++ b/src/notebooks/controllers/kernelConnector.ts @@ -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( diff --git a/src/notebooks/controllers/kernelSource/environmentCreationCommand.ts b/src/notebooks/controllers/kernelSource/environmentCreationCommand.ts new file mode 100644 index 00000000000..e7756f244cd --- /dev/null +++ b/src/notebooks/controllers/kernelSource/environmentCreationCommand.ts @@ -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 + }); + }); + } +} diff --git a/src/notebooks/controllers/preferredKernelConnectionService.node.ts b/src/notebooks/controllers/preferredKernelConnectionService.node.ts index 642442299c5..c2a4a8f2e54 100644 --- a/src/notebooks/controllers/preferredKernelConnectionService.node.ts +++ b/src/notebooks/controllers/preferredKernelConnectionService.node.ts @@ -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) diff --git a/src/notebooks/controllers/serviceRegistry.node.ts b/src/notebooks/controllers/serviceRegistry.node.ts index 9c4bf55c680..9ef2f82af22 100644 --- a/src/notebooks/controllers/serviceRegistry.node.ts +++ b/src/notebooks/controllers/serviceRegistry.node.ts @@ -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'; @@ -42,5 +43,9 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea IExtensionSyncActivationService, KernelSourceCommandHandler ); + serviceManager.addSingleton( + IExtensionSyncActivationService, + EnvironmentCreationCommand + ); registerWidgetTypes(serviceManager, isDevMode); } diff --git a/src/notebooks/controllers/vscodeNotebookController.ts b/src/notebooks/controllers/vscodeNotebookController.ts index 1f53f2b201e..3418ed4f2eb 100644 --- a/src/notebooks/controllers/vscodeNotebookController.ts +++ b/src/notebooks/controllers/vscodeNotebookController.ts @@ -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 ); } @@ -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 ); } diff --git a/src/notebooks/notebookCommandListener.ts b/src/notebooks/notebookCommandListener.ts index 9da3b6ba996..b95b1475c8c 100644 --- a/src/notebooks/notebookCommandListener.ts +++ b/src/notebooks/notebookCommandListener.ts @@ -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 { diff --git a/src/platform/api/pythonApi.ts b/src/platform/api/pythonApi.ts index 9c1a89be7d9..d3b700fe5e8 100644 --- a/src/platform/api/pythonApi.ts +++ b/src/platform/api/pythonApi.ts @@ -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 | undefined, @@ -351,6 +352,26 @@ export class InterpreterService implements IInterpreterService { public initialize() { this.hookupOnDidChangeInterpreterEvent(); } + public async hasWorkspaceSpecificEnvironment(): Promise { + 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 { return this.getApi().then((api) => { if (!api) { diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 1d8e09818bb..75f75e7ac86 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -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.' ); diff --git a/src/platform/interpreter/contracts.ts b/src/platform/interpreter/contracts.ts index 0d4be24bcb3..b711d4dd111 100644 --- a/src/platform/interpreter/contracts.ts +++ b/src/platform/interpreter/contracts.ts @@ -9,6 +9,7 @@ export const IInterpreterService = Symbol('IInterpreterService'); export interface IInterpreterService { // #region New API resolveEnvironment(id: string | Environment): Promise; + hasWorkspaceSpecificEnvironment(): Promise; // #endregion // #region Old API diff --git a/src/platform/interpreter/installer/productInstaller.ts b/src/platform/interpreter/installer/productInstaller.ts index 9d394e9547e..7f2931491ea 100644 --- a/src/platform/interpreter/installer/productInstaller.ts +++ b/src/platform/interpreter/installer/productInstaller.ts @@ -1,14 +1,14 @@ // 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(); /** * Keep track of the fact that we attempted to install a package into an interpreter. * (don't care whether it was successful or not). @@ -16,16 +16,21 @@ import { getInterpreterHash } from '../../pythonEnvironments/info/interpreter'; 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( @@ -36,3 +41,7 @@ export async function isModulePresentInEnvironmentCache( const key = `${await getInterpreterHash(interpreter)}#${ProductNames.get(product)}`; return memento.get(key, false); } + +export function wasIPyKernelInstalAttempted(interpreter: PythonEnvironment): boolean { + return interpretersIntoWhichIPyKernelWasInstalledInSession.has(interpreter.id); +} diff --git a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts index f18fc337543..87b0e69a84a 100644 --- a/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts +++ b/src/test/datascience/jupyter/kernels/installationPrompts.vscode.test.ts @@ -61,6 +61,7 @@ import { isUri } from '../../../../platform/common/utils/misc'; import { hasErrorOutput, translateCellErrorOutput } from '../../../../kernels/execution/helpers'; import { BaseKernelError } from '../../../../kernels/errors/types'; import { IControllerRegistration } from '../../../../notebooks/controllers/types'; +import type { PythonEnvironment } from '../../../../api'; /* eslint-disable no-invalid-this, @typescript-eslint/no-explicit-any */ suite('Install IPyKernel (install) @kernelCore', function () { @@ -90,6 +91,9 @@ suite('Install IPyKernel (install) @kernelCore', function () { this.timeout(120_000); // Slow test, we need to uninstall/install ipykernel. let configSettings: ReadWrite; let previousDisableJupyterAutoStartValue: boolean; + let venvNoKernelPathEnv: PythonEnvironment | undefined; + let venvNoRegPathEnv: PythonEnvironment | undefined; + let venvKernelPathEnv: PythonEnvironment | undefined; /* This test requires a virtual environment to be created & registered as a kernel. It also needs to have ipykernel installed in it. @@ -116,7 +120,7 @@ suite('Install IPyKernel (install) @kernelCore', function () { await pythonApi?.environments.refreshEnvironments({ forceRefresh: true }); const interpreterService = api.serviceContainer.get(IInterpreterService); let lastError: Error | undefined = undefined; - const [interpreter1, interpreter2, interpreter3] = await waitForCondition( + [venvNoKernelPathEnv, venvNoRegPathEnv, venvKernelPathEnv] = await waitForCondition( async () => { try { return await Promise.all([ @@ -131,12 +135,12 @@ suite('Install IPyKernel (install) @kernelCore', function () { defaultNotebookTestTimeout, () => `Failed to get interpreter information for 1,2 &/or 3, ${lastError?.toString()}` ); - if (!interpreter1 || !interpreter2 || !interpreter3) { + if (!venvKernelPathEnv || !venvNoKernelPathEnv || !venvNoRegPathEnv) { throw new Error('Unable to get information for interpreter 1,2,3'); } - venvNoKernelPath = interpreter1.uri; - venvNoRegPath = interpreter2.uri; - venvKernelPath = interpreter3.uri; + venvNoKernelPath = venvNoKernelPathEnv.uri; + venvNoRegPath = venvNoRegPathEnv.uri; + venvKernelPath = venvKernelPathEnv.uri; }); setup(async function () { console.log(`Start test ${this.currentTest?.title}`); @@ -158,8 +162,8 @@ suite('Install IPyKernel (install) @kernelCore', function () { ]); await closeActiveWindows(); await Promise.all([ - clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, venvNoKernelPath), - clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, venvNoRegPath) + clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, venvNoKernelPathEnv!), + clearInstalledIntoInterpreterMemento(memento, Product.ipykernel, venvNoRegPathEnv!) ]); sinon.restore(); console.log(`Start Test completed ${this.currentTest?.title}`); @@ -607,9 +611,10 @@ suite('Install IPyKernel (install) @kernelCore', function () { } function verifyInstallIPyKernelInstructionsInOutput(cell: NotebookCell) { - const textToLookFor = `Run the following command to install '${ProductNames.get(Product.ipykernel)!}'`; + const textToLookFor = `install '${ProductNames.get(Product.ipykernel)!}'`; const err = translateCellErrorOutput(cell.outputs[0]); - assert.include(err.traceback.join(''), textToLookFor); + assert.include(err.traceback.join('').toLowerCase(), textToLookFor.toLowerCase()); + assert.include(err.traceback.join('').toLowerCase(), 'command:'); return true; } type Awaited = T extends PromiseLike ? U : T;