diff --git a/news/2 Fixes/10071.md b/news/2 Fixes/10071.md new file mode 100644 index 000000000000..842c8c2786d1 --- /dev/null +++ b/news/2 Fixes/10071.md @@ -0,0 +1 @@ +Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. diff --git a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts index a1d7fc2495a5..34ef6f01aaae 100644 --- a/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts +++ b/src/client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.ts @@ -57,9 +57,8 @@ function sortProductsInOrderForInstallation(products: Product[]) { * @returns {string} */ export function getMessageForLibrariesNotInstalled(products: Product[]): string { + // Even though kernelspec cannot be installed, display it so user knows what is missing. const names = products - // Ignore kernelspec as it not something that can be installed. - .filter(product => product !== Product.kernelspec) .map(product => ProductNames.get(product)) .filter(name => !!name) .map(name => name as string); @@ -122,15 +121,15 @@ export class JupyterInterpreterDependencyService { _error?: JupyterInstallError, token?: CancellationToken ): Promise { - const productsToInstall = await this.getDependenciesNotInstalled(interpreter, token); + const missingProducts = await this.getDependenciesNotInstalled(interpreter, token); if (Cancellation.isCanceled(token)) { return JupyterInterpreterDependencyResponse.cancel; } - if (productsToInstall.length === 0) { + if (missingProducts.length === 0) { return JupyterInterpreterDependencyResponse.ok; } - const message = getMessageForLibrariesNotInstalled(productsToInstall); + const message = getMessageForLibrariesNotInstalled(missingProducts); sendTelemetryEvent(Telemetry.JupyterNotInstalledErrorShown); const selection = await this.applicationShell.showErrorMessage( @@ -147,8 +146,15 @@ export class JupyterInterpreterDependencyService { switch (selection) { case DataScience.jupyterInstall(): { + // Ignore kernelspec as it not something that can be installed. + // If kernelspec isn't available, then re-install `Jupyter`. + if (missingProducts.includes(Product.kernelspec) && !missingProducts.includes(Product.jupyter)) { + missingProducts.push(Product.jupyter); + } + const productsToInstall = missingProducts.filter(product => product !== Product.kernelspec); // Install jupyter, then notebook, then others in that order. sortProductsInOrderForInstallation(productsToInstall); + let productToInstall = productsToInstall.shift(); const cancellatonPromise = createPromiseFromCancellation({ cancelAction: 'resolve', defaultValue: InstallerResponse.Ignore, token }); while (productToInstall) { diff --git a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts index 6d3391569da3..a230f6607b1e 100644 --- a/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts +++ b/src/test/datascience/jupyter/interpreter/jupyterInterpreterDependencyService.unit.test.ts @@ -17,6 +17,7 @@ import { Architecture } from '../../../../client/common/utils/platform'; import { JupyterInterpreterDependencyResponse, JupyterInterpreterDependencyService } from '../../../../client/datascience/jupyter/interpreter/jupyterInterpreterDependencyService'; import { InterpreterType, PythonInterpreter } from '../../../../client/interpreter/contracts'; +// tslint:disable-next-line: max-func-body-length suite('Data Science - Jupyter Interpreter Configuration', () => { let configuration: JupyterInterpreterDependencyService; let appShell: IApplicationShell; @@ -62,6 +63,24 @@ suite('Data Science - Jupyter Interpreter Configuration', () => { test('Prompt to install if Jupyter is not installed', async () => testPromptIfModuleNotInstalled(false, true)); test('Prompt to install if notebook is not installed', async () => testPromptIfModuleNotInstalled(true, false)); test('Prompt to install if jupyter & notebook is not installed', async () => testPromptIfModuleNotInstalled(false, false)); + test('Reinstall Jupyter if jupyter and notebook are installed but kernelspec is not found', async () => { + when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(true); + when(installer.isInstalled(Product.notebook, pythonInterpreter)).thenResolve(true); + when(appShell.showErrorMessage(anything(), anything(), anything(), anything())).thenResolve( + // tslint:disable-next-line: no-any + DataScience.jupyterInstall() as any + ); + when(pythonExecService.execModule('jupyter', deepEqual(['kernelspec', '--version']), anything())).thenReject(new Error('Not found')); + when(installer.install(anything(), anything(), anything())).thenResolve(InstallerResponse.Installed); + + const response = await configuration.installMissingDependencies(pythonInterpreter); + + // Jupyter must be installed & not kernelspec or anything else. + verify(installer.install(Product.jupyter, anything(), anything())).once(); + verify(installer.install(anything(), anything(), anything())).once(); + verify(appShell.showErrorMessage(anything(), DataScience.jupyterInstall(), DataScience.selectDifferentJupyterInterpreter(), anything())).once(); + assert.equal(response, JupyterInterpreterDependencyResponse.cancel); + }); async function testInstallationOfJupyter(installerResponse: InstallerResponse, expectedConfigurationReponse: JupyterInterpreterDependencyResponse): Promise { when(installer.isInstalled(Product.jupyter, pythonInterpreter)).thenResolve(false);