diff --git a/extensions/positron-python/src/client/common/installer/productInstaller.ts b/extensions/positron-python/src/client/common/installer/productInstaller.ts index 8a2ac2d1a3a..d0d98c30b9c 100644 --- a/extensions/positron-python/src/client/common/installer/productInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/productInstaller.ts @@ -304,7 +304,10 @@ export class DataScienceInstaller extends BaseInstaller { if (pipInstaller) { traceInfo(`Installing pip as its not available to install ${moduleName}.`); await pipInstaller - .installModule(Product.pip, interpreter, cancel) + // --- Start Positron --- + // installAsProcess is required to respect the "Install in Terminal" setting? + .installModule(Product.pip, interpreter, cancel, undefined, { installAsProcess: true }) + // --- End Positron --- .catch((ex) => traceError( `Error in installing the module '${moduleName} as Pip could not be installed', ${ex}`, @@ -418,17 +421,37 @@ export class DataScienceInstaller extends BaseInstaller { ): Promise<InstallerResponse> { // --- Start Positron --- const productName = ProductNames.get(product)!; - const install = await positron.window.showSimpleModalDialogPrompt( - l10n.t('Install Python package "{0}"?', productName), - message ?? - l10n.t( - 'To enable Python support, Positron needs to install the package "{0}" for the active interpreter.', - productName, - ), - l10n.t('Install'), - ); + + let hasPip = true; + if (_flags && _flags & ModuleInstallFlags.installPipIfRequired) { + const installer = this.serviceContainer.get<IInstaller>(IInstaller); + hasPip = await installer.isInstalled(Product.pip, resource); + } + + let install; + if (hasPip) { + install = await positron.window.showSimpleModalDialogPrompt( + l10n.t('Install Python package "{0}"?', productName), + message ?? + l10n.t( + 'To enable Python support, Positron needs to install the package "{0}" for the active interpreter.', + productName, + ), + l10n.t('Install'), + ); + } else { + install = await positron.window.showSimpleModalDialogPrompt( + l10n.t('Install Python packages "{0}" and "{1}"?', ProductNames.get(Product.pip)!, productName), + message ?? + l10n.t( + 'To enable Python support, Positron needs to install the package "{0}" for the active interpreter.', + productName, + ), + l10n.t('Install'), + ); + } if (install) { - return this.install(product, resource, cancel, undefined, options); + return this.install(product, resource, cancel, _flags, options); } // --- End Positron --- return InstallerResponse.Ignore; diff --git a/extensions/positron-python/src/client/positron/session.ts b/extensions/positron-python/src/client/positron/session.ts index 1d5fa5411c9..a856fc78db9 100644 --- a/extensions/positron-python/src/client/positron/session.ts +++ b/extensions/positron-python/src/client/positron/session.ts @@ -12,7 +12,8 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; import PQueue from 'p-queue'; import { ProductNames } from '../common/installer/productNames'; -import { InstallOptions } from '../common/installer/types'; +import { InstallOptions, ModuleInstallFlags } from '../common/installer/types'; + import { IConfigurationService, IInstaller, @@ -254,6 +255,9 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs ); } + // Check if we have Pip installed, already + const hasPip = await installer.isInstalled(Product.pip, interpreter); + // Pass a cancellation token to enable VSCode's progress indicator and let the user // cancel the install. const tokenSource = new vscode.CancellationTokenSource(); @@ -265,19 +269,32 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs const installOrUpgrade = hasCompatibleKernel === ProductInstallStatus.NeedsUpgrade ? 'upgrade' : 'install'; const product = Product.ipykernel; - const message = vscode.l10n.t( - 'To enable Python support, Positron needs to {0} the package <code>{1}</code> for the active interpreter {2} at: <code>{3}</code>.', - installOrUpgrade, - ProductNames.get(product)!, - `Python ${this.runtimeMetadata.languageVersion}`, - this.runtimeMetadata.runtimePath, - ); + + let message; + if (!hasPip) { + message = vscode.l10n.t( + 'To enable Python support, Positron needs to {0} the packages <code>{1}</code> and <code>{2}</code> for the active interpreter {3} at: <code>{4}</code>.', + installOrUpgrade, + ProductNames.get(Product.pip)!, + ProductNames.get(product)!, + `Python ${this.runtimeMetadata.languageVersion}`, + this.runtimeMetadata.runtimePath, + ); + } else { + message = vscode.l10n.t( + 'To enable Python support, Positron needs to {0} the package <code>{1}</code> for the active interpreter {2} at: <code>{3}</code>.', + installOrUpgrade, + ProductNames.get(product)!, + `Python ${this.runtimeMetadata.languageVersion}`, + this.runtimeMetadata.runtimePath, + ); + } const response = await installer.promptToInstall( product, interpreter, installerToken, - undefined, + ModuleInstallFlags.installPipIfRequired, installOptions, message, ); diff --git a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts index 2aff33a552e..c804ce7fe9c 100644 --- a/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/productInstaller.unit.test.ts @@ -1,5 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// --- Start Positron --- +// Disable eslint rules for our import block below. This appears at the top of the file to stop +// auto-formatting tools from reordering the imports. +/* eslint-disable import/no-duplicates */ +/* eslint-disable import/order */ +// --- End Positron --- 'use strict'; @@ -8,6 +14,9 @@ import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../../client/common/application/types'; import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +// --- Start Positron --- +import { ModuleInstallFlags } from '../../../client/common/installer/types'; +// --- End Positron --- import { InstallerResponse, Product } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -220,4 +229,72 @@ suite('DataScienceInstaller install', async () => { const result = await dataScienceInstaller.install(Product.ipykernel, testEnvironment); expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); }); + + // --- Start Positron --- + test('Will install pip if necessary', async () => { + const testEnvironment: PythonEnvironment = { + envType: EnvironmentType.VirtualEnv, + envName: 'test', + envPath: interpreterPath, + path: interpreterPath, + architecture: Architecture.x64, + sysPrefix: '', + }; + const testInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + + testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); + + // Mock a function to install Product.pip + testInstaller + .setup((c) => + c.installModule( + TypeMoq.It.isValue(Product.pip), + TypeMoq.It.isValue(testEnvironment), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + // We added the `options` param in https://github.com/posit-dev/positron-python/pull/66. + TypeMoq.It.isAny(), + ), + ) + .callback(() => { + // Add the testInstaller to the available channels once installModule is called + // with Product.pip + installationChannelManager + .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([testInstaller.object])); + }) + .returns(() => Promise.resolve()); + + testInstaller + .setup((c) => + c.installModule( + TypeMoq.It.isValue(Product.ipykernel), + TypeMoq.It.isValue(testEnvironment), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + // We added the `options` param in https://github.com/posit-dev/positron-python/pull/66. + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve()); + + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(IModuleInstaller))) + .returns(() => [testInstaller.object]); + + installationChannelManager + .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) + // Specify no installation channels from the get-go + .returns(() => Promise.resolve([])); + + const result = await dataScienceInstaller.install( + Product.ipykernel, + testEnvironment, + undefined, + // Pass in the flag to install Pip if it's not available yet + ModuleInstallFlags.installPipIfRequired, + ); + expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); + }); + // --- End Positron --- });