diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index a9118acbb16..00550c3b201 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -837,6 +837,12 @@ "description": "%python.venvPath.description%", "scope": "machine", "type": "string" + }, + "python.installModulesInTerminal": { + "default": false, + "markdownDescription": "%python.installModulesInTerminal.description%", + "scope": "resource", + "type": "boolean" } }, "title": "Python", diff --git a/extensions/positron-python/package.nls.json b/extensions/positron-python/package.nls.json index d6f238d00bc..1bfcdd7504d 100644 --- a/extensions/positron-python/package.nls.json +++ b/extensions/positron-python/package.nls.json @@ -104,6 +104,7 @@ "python.testing.unittestEnabled.description": "Enable testing using unittest.", "python.venvFolders.description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "python.venvPath.description": "Path to folder with a list of Virtual Environments (e.g. ~/.pyenv, ~/Envs, ~/.virtualenvs).", + "python.installModulesInTerminal.description": "Whether to install Python modules (such as `ipykernel`) in the Terminal, instead of in a background process. Installing modules in the Terminal allows you to see the output of the installation command.", "walkthrough.pythonWelcome.title": "Get Started with Python Development", "walkthrough.pythonWelcome.description": "Your first steps to set up a Python project with all the powerful tools and features that the Python extension has to offer!", "walkthrough.step.python.createPythonFile.title": "Create a Python file", diff --git a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts index a757e4683ae..dce30ab9d37 100644 --- a/extensions/positron-python/src/client/common/installer/moduleInstaller.ts +++ b/extensions/positron-python/src/client/common/installer/moduleInstaller.ts @@ -1,6 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +// --- Start Positron --- +/* eslint-disable max-classes-per-file, import/no-duplicates */ +import { CancellationTokenSource } from 'vscode'; +// --- End Positron --- + import { injectable } from 'inversify'; import * as path from 'path'; import { CancellationToken, l10n, ProgressLocation, ProgressOptions } from 'vscode'; @@ -22,6 +27,8 @@ import { ProductNames } from './productNames'; import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; // --- Start Positron --- +// eslint-disable-next-line import/newline-after-import +import { IWorkspaceService } from '../application/types'; class ExternallyManagedEnvironmentError extends Error {} // --- End Positron --- @@ -44,7 +51,9 @@ export abstract class ModuleInstaller implements IModuleInstaller { flags?: ModuleInstallFlags, options?: InstallOptions, ): Promise { - const shouldExecuteInTerminal = !options?.installAsProcess; + // --- Start Positron --- + const shouldExecuteInTerminal = this.installModulesInTerminal() || !options?.installAsProcess; + // --- End Positron --- const name = typeof productOrModuleName === 'string' ? productOrModuleName @@ -249,6 +258,18 @@ export abstract class ModuleInstaller implements IModuleInstaller { .get(ITerminalServiceFactory) .getTerminalService(options); + // --- Start Positron --- + // When running with the `python.installModulesInTerminal` setting enabled, we want to + // ensure that the terminal command is fully executed before returning. Otherwise, the + // calling code of the install will not be able to tell when the installation is complete. + if (this.installModulesInTerminal()) { + // Ensure we pass a cancellation token so that we await the full terminal command + // execution before returning. + const cancelToken = token ?? new CancellationTokenSource().token; + await terminalService.sendCommand(command, args, token ?? cancelToken); + return; + } + // --- End Positron --- terminalService.sendCommand(command, args, token); } else { const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); @@ -278,6 +299,22 @@ export abstract class ModuleInstaller implements IModuleInstaller { // --- End Positron --- } } + + // --- Start Positron --- + /** + * Check if the user has enabled the setting to install modules in the terminal. + * + * `python.installModulesInTerminal` is a setting that allows the user to force modules to be + * installed in the Terminal. Usually, such installations occur in the background. However, + * for debugging, it can be helpful to see the Terminal output of the installation process. + * @returns `true` if the user has enabled the setting to install modules in the Terminal, + * `false` if the user has disabled the setting, and `undefined` if the setting is not found. + */ + private installModulesInTerminal(): boolean | undefined { + const workspaceService = this.serviceContainer.get(IWorkspaceService); + return workspaceService.getConfiguration('python').get('installModulesInTerminal'); + } + // --- End Positron --- } export function translateProductToModule(product: Product): string { diff --git a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts index e2d8b22d4cc..1f8085312c0 100644 --- a/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/extensions/positron-python/src/test/common/installer/moduleInstaller.unit.test.ts @@ -282,6 +282,15 @@ suite('Module Installer', () => { workspaceService .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) .returns(() => http.object); + // --- Start Positron --- + const pythonConfig = TypeMoq.Mock.ofType(); + pythonConfig + .setup((p) => p.get(TypeMoq.It.isValue('installModulesInTerminal'), TypeMoq.It.isAny())) + .returns(() => false); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object); + // --- End Positron --- installer = new InstallerClass(serviceContainer.object); }); teardown(() => {