From d4faf973f0feeb19e531eb73d4bc00e0fcf361c3 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 6 Apr 2022 15:10:07 -0700 Subject: [PATCH] Cherry pick fixes for release (#18874) * Ensure `conda info` command isn't run multiple times during startup when large number of conda interpreters are present (#18808) * Wrap file paths containg an ampersand in double quotation marks for running commands in a shell (#18855) * If a conda environment is not returned via the `conda env list` command, resolve it as unknown (#18856) * If a conda environment is not returned via the conda env list command, resolve it as unknown * News entry * Fix unit tests * Do not use conda run when launching a debugger (#18858) * Do not use conda run when launching a debugger * News * Only build VSIX * Revert "Only build VSIX" This reverts commit 0ade929b400912a97f93099510950ba7d81779a2. * Fixes support for python binaries not following the standard names (#18860) * Fixes support for python binaries not following the standard names * news * Remove comment * Do not validate conda binaries using shell by default (#18866) * Do not validate conda binaries using shell * Fix tests * Fix lint * Fix tests * Ensure string prototypes extension extends are unique enough (#18870) --- news/2 Fixes/18200.md | 1 + news/2 Fixes/18530.md | 1 + news/2 Fixes/18722.md | 1 + news/2 Fixes/18835.md | 1 + news/2 Fixes/18847.md | 1 + src/client/common/extensions.ts | 14 +++--- src/client/common/installer/condaInstaller.ts | 13 +++-- .../common/installer/moduleInstaller.ts | 3 +- .../common/process/internal/scripts/index.ts | 6 +-- src/client/common/process/logger.ts | 2 +- .../environmentActivationProviders/bash.ts | 2 +- .../commandPrompt.ts | 4 +- .../condaActivationProvider.ts | 16 +++--- .../pipEnvActivationProvider.ts | 4 +- .../pyenvActivationProvider.ts | 4 +- src/client/common/terminal/helper.ts | 4 +- .../debugger/extension/adapter/factory.ts | 35 +------------ .../extension/adapter/remoteLaunchers.ts | 7 ++- src/client/interpreter/activation/service.ts | 4 +- src/client/interpreter/contracts.ts | 2 +- .../base/info/environmentInfoService.ts | 2 +- .../base/info/interpreter.ts | 5 +- .../base/locators/composite/envsResolver.ts | 5 +- .../base/locators/composite/resolverUtils.ts | 13 ++--- .../pythonEnvironments/common/commonUtils.ts | 18 +++---- .../common/environmentManagers/conda.ts | 50 ++++++++++++------- .../environmentManagers/condaService.ts | 7 ++- .../pythonEnvironments/common/posixUtils.ts | 4 +- .../pythonEnvironments/info/executable.ts | 5 +- .../codeExecution/djangoShellCodeExecution.ts | 2 +- .../codeExecution/terminalCodeExecution.ts | 6 ++- src/test/api.functional.test.ts | 8 ++- src/test/common/extensions.unit.test.ts | 24 +++++---- .../installer/condaInstaller.unit.test.ts | 6 +-- .../installer/moduleInstaller.unit.test.ts | 25 +++++++--- .../terminals/activation.bash.unit.test.ts | 2 +- .../activation.commandPrompt.unit.test.ts | 9 ++-- .../terminals/activation.conda.unit.test.ts | 14 ++++-- .../terminalActivation.testvirtualenvs.ts | 5 +- src/test/common/terminals/helper.unit.test.ts | 4 +- .../synchronousTerminalService.unit.test.ts | 4 +- .../activation/service.unit.test.ts | 2 +- .../composite/envsResolver.unit.test.ts | 9 +--- .../composite/resolverUtils.unit.test.ts | 18 +++---- .../locators/lowLevel/watcherTestUtils.ts | 2 +- .../environmentManagers/conda.unit.test.ts | 3 +- .../djangoShellCodeExect.unit.test.ts | 8 +-- .../terminalCodeExec.unit.test.ts | 10 ++-- typings/extensions.d.ts | 15 +++--- 49 files changed, 222 insertions(+), 188 deletions(-) create mode 100644 news/2 Fixes/18200.md create mode 100644 news/2 Fixes/18530.md create mode 100644 news/2 Fixes/18722.md create mode 100644 news/2 Fixes/18835.md create mode 100644 news/2 Fixes/18847.md diff --git a/news/2 Fixes/18200.md b/news/2 Fixes/18200.md new file mode 100644 index 000000000000..814f056fe97a --- /dev/null +++ b/news/2 Fixes/18200.md @@ -0,0 +1 @@ +Ensure `conda info` command isn't run multiple times during startup when large number of conda interpreters are present. diff --git a/news/2 Fixes/18530.md b/news/2 Fixes/18530.md new file mode 100644 index 000000000000..306c991183ca --- /dev/null +++ b/news/2 Fixes/18530.md @@ -0,0 +1 @@ +If a conda environment is not returned via the `conda env list` command, consider it as unknown env type. diff --git a/news/2 Fixes/18722.md b/news/2 Fixes/18722.md new file mode 100644 index 000000000000..e185b6d0e32c --- /dev/null +++ b/news/2 Fixes/18722.md @@ -0,0 +1 @@ +Wrap file paths containg an ampersand in double quotation marks for running commands in a shell. diff --git a/news/2 Fixes/18835.md b/news/2 Fixes/18835.md new file mode 100644 index 000000000000..881380ff7fa8 --- /dev/null +++ b/news/2 Fixes/18835.md @@ -0,0 +1 @@ +Fixes regression with support for python binaries not following the standard names. diff --git a/news/2 Fixes/18847.md b/news/2 Fixes/18847.md new file mode 100644 index 000000000000..fe55719fdde8 --- /dev/null +++ b/news/2 Fixes/18847.md @@ -0,0 +1 @@ +Fix launch of Python Debugger when using conda environments. diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index ea3fc81327b8..6c6572bcde02 100644 --- a/src/client/common/extensions.ts +++ b/src/client/common/extensions.ts @@ -20,12 +20,12 @@ declare interface String { * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - toCommandArgument(): string; + toCommandArgumentForPythonExt(): string; /** * Appropriately formats a a file path so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - fileToCommandArgument(): string; + fileToCommandArgumentForPythonExt(): string; /** * String.format() implementation. * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. @@ -69,22 +69,24 @@ String.prototype.splitLines = function ( * E.g. if an argument contains a space, then it will be enclosed within double quotes. * @param {String} value. */ -String.prototype.toCommandArgument = function (this: string): string { +String.prototype.toCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return this.indexOf(' ') >= 0 && !this.startsWith('"') && !this.endsWith('"') ? `"${this}"` : this.toString(); + return (this.indexOf(' ') >= 0 || this.indexOf('&') >= 0) && !this.startsWith('"') && !this.endsWith('"') + ? `"${this}"` + : this.toString(); }; /** * Appropriately formats a a file path so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ -String.prototype.fileToCommandArgument = function (this: string): string { +String.prototype.fileToCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return this.toCommandArgument().replace(/\\/g, '/'); + return this.toCommandArgumentForPythonExt().replace(/\\/g, '/'); }; /** diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index 860c58bf755b..b440e6cba798 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -71,7 +71,11 @@ export class CondaInstaller extends ModuleInstaller { flags: ModuleInstallFlags = 0, ): Promise { const condaService = this.serviceContainer.get(ICondaService); - const condaFile = await condaService.getCondaFile(); + // Installation using `conda.exe` sometimes fails with a HTTP error on Windows: + // https://github.com/conda/conda/issues/11399 + // Execute in a shell which uses a `conda.bat` file instead, using which installation works. + const useShell = true; + const condaFile = await condaService.getCondaFile(useShell); const pythonPath = isResource(resource) ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath @@ -100,11 +104,11 @@ export class CondaInstaller extends ModuleInstaller { if (info && info.name) { // If we have the name of the conda environment, then use that. args.push('--name'); - args.push(info.name.toCommandArgument()); + args.push(info.name.toCommandArgumentForPythonExt()); } else if (info && info.path) { // Else provide the full path to the environment path. args.push('--prefix'); - args.push(info.path.fileToCommandArgument()); + args.push(info.path.fileToCommandArgumentForPythonExt()); } if (flags & ModuleInstallFlags.updateDependencies) { args.push('--update-deps'); @@ -117,8 +121,7 @@ export class CondaInstaller extends ModuleInstaller { return { args, execPath: condaFile, - // Execute in a shell as `conda` on windows refers to `conda.bat`, which requires a shell to work. - useShell: true, + useShell, }; } diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 38465ba53ad3..e859eeb2e900 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -217,7 +217,8 @@ export abstract class ModuleInstaller implements IModuleInstaller { const argv = [command, ...args]; // Concat these together to make a set of quoted strings const quoted = argv.reduce( - (p, c) => (p ? `${p} ${c.toCommandArgument()}` : `${c.toCommandArgument()}`), + (p, c) => + p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`, '', ); await processService.shellExec(quoted); diff --git a/src/client/common/process/internal/scripts/index.ts b/src/client/common/process/internal/scripts/index.ts index c8b3570ff2ba..fc275209a55a 100644 --- a/src/client/common/process/internal/scripts/index.ts +++ b/src/client/common/process/internal/scripts/index.ts @@ -94,7 +94,7 @@ export function normalizeSelection(): [string[], (out: string) => string] { // printEnvVariables.py export function printEnvVariables(): [string[], (out: string) => NodeJS.ProcessEnv] { - const script = path.join(SCRIPTS_DIR, 'printEnvVariables.py').fileToCommandArgument(); + const script = path.join(SCRIPTS_DIR, 'printEnvVariables.py').fileToCommandArgumentForPythonExt(); const args = [script]; function parse(out: string): NodeJS.ProcessEnv { @@ -113,11 +113,11 @@ export function shell_exec(command: string, lockfile: string, shellArgs: string[ // could be anything. return [ script, - command.fileToCommandArgument(), + command.fileToCommandArgumentForPythonExt(), // The shell args must come after the command // but before the lockfile. ...shellArgs, - lockfile.fileToCommandArgument(), + lockfile.fileToCommandArgumentForPythonExt(), ]; } diff --git a/src/client/common/process/logger.ts b/src/client/common/process/logger.ts index 7dc77e752df1..fad6f080bb71 100644 --- a/src/client/common/process/logger.ts +++ b/src/client/common/process/logger.ts @@ -22,7 +22,7 @@ export class ProcessLogger implements IProcessLogger { return; } let command = args - ? [fileOrCommand, ...args].map((e) => e.trimQuotes().toCommandArgument()).join(' ') + ? [fileOrCommand, ...args].map((e) => e.trimQuotes().toCommandArgumentForPythonExt()).join(' ') : fileOrCommand; const info = [`> ${this.getDisplayCommands(command)}`]; if (options && options.cwd) { diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts index 83a4c9bc353c..827201037570 100644 --- a/src/client/common/terminal/environmentActivationProviders/bash.ts +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -46,6 +46,6 @@ export class Bash extends VenvBaseActivationCommandProvider { if (!scriptFile) { return; } - return [`source ${scriptFile.fileToCommandArgument()}`]; + return [`source ${scriptFile.fileToCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts index b5695524a5ae..25ab46ca1fb4 100644 --- a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -66,12 +66,12 @@ export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvide } if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) { - return [scriptFile.fileToCommandArgument()]; + return [scriptFile.fileToCommandArgumentForPythonExt()]; } else if ( (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) && scriptFile.endsWith('Activate.ps1') ) { - return [`& ${scriptFile.fileToCommandArgument()}`]; + return [`& ${scriptFile.fileToCommandArgumentForPythonExt()}`]; } else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('Activate.ps1')) { // lets not try to run the powershell file from command prompt (user may not have powershell) return []; diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index 78bbf4210a47..bc223dba89d0 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -83,9 +83,11 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman const interpreterPath = await this.condaService.getInterpreterPathForEnvironment(envInfo); const condaPath = await this.condaService.getCondaFileFromInterpreter(interpreterPath, envInfo.name); if (condaPath) { - const activatePath = path.join(path.dirname(condaPath), 'activate').fileToCommandArgument(); + const activatePath = path + .join(path.dirname(condaPath), 'activate') + .fileToCommandArgumentForPythonExt(); const firstActivate = this.platform.isWindows ? activatePath : `source ${activatePath}`; - return [firstActivate, `conda activate ${condaEnv.toCommandArgument()}`]; + return [firstActivate, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; } } } @@ -116,7 +118,7 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman const condaScriptsPath: string = path.dirname(condaExePath); // prefix the cmd with the found path, and ensure it's quoted properly activateCmd = path.join(condaScriptsPath, activateCmd); - activateCmd = activateCmd.toCommandArgument(); + activateCmd = activateCmd.toCommandArgumentForPythonExt(); } return activateCmd; @@ -124,7 +126,7 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman public async getWindowsCommands(condaEnv: string): Promise { const activate = await this.getWindowsActivateCommand(); - return [`${activate} ${condaEnv.toCommandArgument()}`]; + return [`${activate} ${condaEnv.toCommandArgumentForPythonExt()}`]; } } @@ -135,16 +137,16 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman * Extension will not attempt to work around issues by trying to setup shell for user. */ export async function _getPowershellCommands(condaEnv: string): Promise { - return [`conda activate ${condaEnv.toCommandArgument()}`]; + return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; } async function getFishCommands(condaEnv: string, condaFile: string): Promise { // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 - return [`${condaFile.fileToCommandArgument()} activate ${condaEnv.toCommandArgument()}`]; + return [`${condaFile.fileToCommandArgumentForPythonExt()} activate ${condaEnv.toCommandArgumentForPythonExt()}`]; } async function getUnixCommands(condaEnv: string, condaFile: string): Promise { const condaDir = path.dirname(condaFile); const activateFile = path.join(condaDir, 'activate'); - return [`source ${activateFile.fileToCommandArgument()} ${condaEnv.toCommandArgument()}`]; + return [`source ${activateFile.fileToCommandArgumentForPythonExt()} ${condaEnv.toCommandArgumentForPythonExt()}`]; } diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts index 04f696d0b9fb..d097c759ec40 100644 --- a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -41,7 +41,7 @@ export class PipEnvActivationCommandProvider implements ITerminalActivationComma } } const execName = this.pipEnvExecution.executable; - return [`${execName.fileToCommandArgument()} shell`]; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } public async getActivationCommandsForInterpreter(pythonPath: string): Promise { @@ -51,6 +51,6 @@ export class PipEnvActivationCommandProvider implements ITerminalActivationComma } const execName = this.pipEnvExecution.executable; - return [`${execName.fileToCommandArgument()} shell`]; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } } diff --git a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts index 44fe5bcfd75e..91347f35ae95 100644 --- a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -26,7 +26,7 @@ export class PyEnvActivationCommandProvider implements ITerminalActivationComman return; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } public async getActivationCommandsForInterpreter( @@ -40,6 +40,6 @@ export class PyEnvActivationCommandProvider implements ITerminalActivationComman return; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts index 1be0a94aba31..304c98b4cd81 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -63,9 +63,9 @@ export class TerminalHelper implements ITerminalHelper { terminalShellType === TerminalShellType.powershell || terminalShellType === TerminalShellType.powershellCore; const commandPrefix = isPowershell ? '& ' : ''; - const formattedArgs = args.map((a) => a.toCommandArgument()); + const formattedArgs = args.map((a) => a.toCommandArgumentForPythonExt()); - return `${commandPrefix}${command.fileToCommandArgument()} ${formattedArgs.join(' ')}`.trim(); + return `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} ${formattedArgs.join(' ')}`.trim(); } public async getEnvironmentActivationCommands( terminalShellType: TerminalShellType, diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 37d2f669a3cf..a001ba2ad42c 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -16,8 +16,7 @@ import { IApplicationShell } from '../../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../../constants'; import { IInterpreterService } from '../../../interpreter/contracts'; import { traceLog, traceVerbose } from '../../../logging'; -import { Conda } from '../../../pythonEnvironments/common/environmentManagers/conda'; -import { EnvironmentType, PythonEnvironment } from '../../../pythonEnvironments/info'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; @@ -143,40 +142,8 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac return this.getExecutableCommand(interpreters[0]); } - private async getCondaCommand(): Promise { - const condaCommand = await Conda.getConda(); - const isCondaRunSupported = await condaCommand?.isCondaRunSupported(); - return isCondaRunSupported ? condaCommand : undefined; - } - private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { - if (interpreter.envType === EnvironmentType.Conda) { - const condaCommand = await this.getCondaCommand(); - if (condaCommand) { - if (interpreter.envName) { - return [ - condaCommand.command, - 'run', - '-n', - interpreter.envName, - '--no-capture-output', - '--live-stream', - 'python', - ]; - } else if (interpreter.envPath) { - return [ - condaCommand.command, - 'run', - '-p', - interpreter.envPath, - '--no-capture-output', - '--live-stream', - 'python', - ]; - } - } - } return interpreter.path.length > 0 ? [interpreter.path] : []; } return []; diff --git a/src/client/debugger/extension/adapter/remoteLaunchers.ts b/src/client/debugger/extension/adapter/remoteLaunchers.ts index de0362778a47..f42f101f8523 100644 --- a/src/client/debugger/extension/adapter/remoteLaunchers.ts +++ b/src/client/debugger/extension/adapter/remoteLaunchers.ts @@ -18,7 +18,12 @@ type RemoteDebugOptions = { export function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath: string = pathToDebugger) { const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait-for-client'] : []; - return [debuggerPath.fileToCommandArgument(), '--listen', `${options.host}:${options.port}`, ...waitArgs]; + return [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${options.host}:${options.port}`, + ...waitArgs, + ]; } export function getDebugpyPackagePath(): string { diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 81cdecd1843a..24007581daf5 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -174,7 +174,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi let command: string | undefined; let [args, parse] = internalScripts.printEnvVariables(); args.forEach((arg, i) => { - args[i] = arg.toCommandArgument(); + args[i] = arg.toCommandArgumentForPythonExt(); }); interpreter = interpreter ?? (await this.interpreterService.getActiveInterpreter(resource)); if (interpreter?.envType === EnvironmentType.Conda) { @@ -185,7 +185,7 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi }); if (pythonArgv) { // Using environment prefix isn't needed as the marker script already takes care of it. - command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgument()).join(' '); + command = [...pythonArgv, ...args].map((arg) => arg.toCommandArgumentForPythonExt()).join(' '); } } if (!command) { diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index f6e18caac883..c9cfb15bf57e 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -54,7 +54,7 @@ export const ICondaService = Symbol('ICondaService'); * Interface carries the properties which are not available via the discovery component interface. */ export interface ICondaService { - getCondaFile(): Promise; + getCondaFile(forShellExecution?: boolean): Promise; isCondaAvailable(): Promise; getCondaVersion(): Promise; getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise; diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts index bf321179941b..baf1eb873bc2 100644 --- a/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -37,7 +37,7 @@ async function buildEnvironmentInfoUsingCondaRun(env: PythonEnvInfo): Promise (p ? `${p} ${c.toCommandArgument()}` : `${c.toCommandArgument()}`), ''); + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); // Try shell execing the command, followed by the arguments. This will make node kill the process if it // takes too long. diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 1d8bbf9d73d0..218a6d5a8603 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -78,7 +78,7 @@ export class PythonEnvsResolver implements IResolvingLocator { ); } else if (seen[event.index] !== undefined) { const old = seen[event.index]; - seen[event.index] = await resolveBasicEnv(event.update); + seen[event.index] = await resolveBasicEnv(event.update, true); didUpdate.fire({ old, index: event.index, update: seen[event.index] }); this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); } else { @@ -92,7 +92,8 @@ export class PythonEnvsResolver implements IResolvingLocator { let result = await iterator.next(); while (!result.done) { - const currEnv = await resolveBasicEnv(result.value); + // Use cache from the current refresh where possible. + const currEnv = await resolveBasicEnv(result.value, true); seen.push(currEnv); yield currEnv; this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 3df77f61d794..abc08c0a7833 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -27,8 +27,8 @@ import { BasicEnvInfo } from '../../locator'; import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; -function getResolvers(): Map Promise> { - const resolvers = new Map Promise>(); +function getResolvers(): Map Promise> { + const resolvers = new Map Promise>(); Object.values(PythonEnvKind).forEach((k) => { resolvers.set(k, resolveGloballyInstalledEnv); }); @@ -46,11 +46,11 @@ function getResolvers(): Map Promise { +export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Promise { const { kind, source } = env; const resolvers = getResolvers(); const resolverForKind = resolvers.get(kind)!; - const resolvedEnv = await resolverForKind(env); + const resolvedEnv = await resolverForKind(env, useCache); resolvedEnv.searchLocation = getSearchLocation(resolvedEnv); resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) { @@ -137,13 +137,13 @@ async function resolveSimpleEnv(env: BasicEnvInfo): Promise { return envInfo; } -async function resolveCondaEnv(env: BasicEnvInfo): Promise { +async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise { const { executablePath } = env; const conda = await Conda.getConda(); if (conda === undefined) { traceWarn(`${executablePath} identified as Conda environment even though Conda is not installed`); } - const envs = (await conda?.getEnvList()) ?? []; + const envs = (await conda?.getEnvList(useCache)) ?? []; for (const { name, prefix } of envs) { let executable = await getInterpreterPathFromDir(prefix); const currEnv: BasicEnvInfo = { executablePath: executable ?? '', kind: PythonEnvKind.Conda, envPath: prefix }; @@ -173,6 +173,7 @@ async function resolveCondaEnv(env: BasicEnvInfo): Promise { } info' command`, ); // Environment could still be valid, resolve as a simple env. + env.kind = PythonEnvKind.Unknown; return resolveSimpleEnv(env); } diff --git a/src/client/pythonEnvironments/common/commonUtils.ts b/src/client/pythonEnvironments/common/commonUtils.ts index d8abedb8c89f..85462531e5e3 100644 --- a/src/client/pythonEnvironments/common/commonUtils.ts +++ b/src/client/pythonEnvironments/common/commonUtils.ts @@ -15,23 +15,19 @@ import { isFile, normCasePath } from './externalDependencies'; import * as posix from './posixUtils'; import * as windows from './windowsUtils'; -const matchPythonBinFilename = +const matchStandardPythonBinFilename = getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename; type FileFilterFunc = (filename: string) => boolean; /** - * Returns `true` if path provided is likely a python executable. + * Returns `true` if path provided is likely a python executable than a folder path. */ export async function isPythonExecutable(filePath: string): Promise { - const isMatch = matchPythonBinFilename(filePath); - if (!isMatch) { - return false; - } - // On Windows it's fair to assume a path ending with `.exe` denotes a file. - if (getOSType() === OSType.Windows) { + const isMatch = matchStandardPythonBinFilename(filePath); + if (isMatch && getOSType() === OSType.Windows) { + // On Windows it's fair to assume a path ending with `.exe` denotes a file. return true; } - // For other operating systems verify if it's a file. if (await isFile(filePath)) { return true; } @@ -83,7 +79,7 @@ export async function* iterPythonExecutablesInDir( ): AsyncIterableIterator { const readDirOpts = { ...opts, - filterFile: matchPythonBinFilename, + filterFile: matchStandardPythonBinFilename, }; const entries = await readDirEntries(dirname, readDirOpts); for (const entry of entries) { @@ -270,7 +266,7 @@ async function checkPythonExecutable( filterFile?: (f: string | DirEntry) => Promise; }, ): Promise { - const matchFilename = opts.matchFilename || matchPythonBinFilename; + const matchFilename = opts.matchFilename || matchStandardPythonBinFilename; const filename = typeof executable === 'string' ? executable : executable.filename; if (!matchFilename(filename)) { diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts index db026f4cda05..044fff6ac6b7 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/conda.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -10,6 +10,7 @@ import { readFile, shellExecute, onDidChangePythonSetting, + exec, } from '../externalDependencies'; import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; @@ -241,13 +242,21 @@ export class Conda { */ private static condaPromise: Promise | undefined; + private condaInfoCached: Promise | undefined; + + /** + * Carries path to conda binary to be used for shell execution. + */ + public readonly shellCommand: string; + /** * Creates a Conda service corresponding to the corresponding "conda" command. * * @param command - Command used to spawn conda. This has the same meaning as the * first argument of spawn() - i.e. it can be a full path, or just a binary name. */ - constructor(readonly command: string) { + constructor(readonly command: string, shellCommand?: string) { + this.shellCommand = shellCommand ?? command; onDidChangePythonSetting(CONDAPATH_SETTING_KEY, () => { Conda.condaPromise = undefined; }); @@ -377,7 +386,7 @@ export class Conda { if (condaBatFile) { const condaBat = new Conda(condaBatFile); await condaBat.getInfo(); - conda = condaBat; + conda = new Conda(condaPath, condaBatFile); } } catch (ex) { traceVerbose('Failed to spawn conda bat file', condaBatFile, ex); @@ -402,19 +411,20 @@ export class Conda { * Retrieves global information about this conda. * Corresponds to "conda info --json". */ - public async getInfo(): Promise { - return this.getInfoCached(this.command); + public async getInfo(useCache?: boolean): Promise { + if (!useCache || !this.condaInfoCached) { + this.condaInfoCached = this.getInfoImpl(this.command); + } + return this.condaInfoCached; } /** - * Cache result for this particular command. + * Temporarily cache result for this particular command. */ @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this - private async getInfoCached(command: string): Promise { - const quoted = [command.toCommandArgument(), 'info', '--json'].join(' '); - // Execute in a shell as `conda` on windows refers to `conda.bat`, which requires a shell to work. - const result = await shellExecute(quoted, { timeout: CONDA_GENERAL_TIMEOUT }); + private async getInfoImpl(command: string): Promise { + const result = await exec(command, ['info', '--json'], { timeout: CONDA_GENERAL_TIMEOUT }); traceVerbose(`conda info --json: ${result.stdout}`); return JSON.parse(result.stdout); } @@ -424,8 +434,8 @@ export class Conda { * Corresponds to "conda env list --json", but also computes environment names. */ @cache(30_000, true, 10_000) - public async getEnvList(): Promise { - const info = await this.getInfo(); + public async getEnvList(useCache?: boolean): Promise { + const info = await this.getInfo(useCache); const { envs } = info; if (envs === undefined) { return []; @@ -491,7 +501,7 @@ export class Conda { return undefined; } - public async getRunPythonArgs(env: CondaEnvInfo): Promise { + public async getRunPythonArgs(env: CondaEnvInfo, forShellExecution?: boolean): Promise { const condaVersion = await this.getCondaVersion(); if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { return undefined; @@ -502,7 +512,15 @@ export class Conda { } else { args.push('-p', env.prefix); } - return [this.command, 'run', ...args, '--no-capture-output', '--live-stream', 'python', OUTPUT_MARKER_SCRIPT]; + return [ + forShellExecution ? this.shellCommand : this.command, + 'run', + ...args, + '--no-capture-output', + '--live-stream', + 'python', + OUTPUT_MARKER_SCRIPT, + ]; } /** @@ -510,14 +528,12 @@ export class Conda { */ @cache(-1, true) public async getCondaVersion(): Promise { - const info = await this.getInfo().catch(() => undefined); + const info = await this.getInfo(true).catch(() => undefined); let versionString: string | undefined; if (info && info.conda_version) { versionString = info.conda_version; } else { - const quoted = `${this.command.toCommandArgument()} --version`; - // Execute in a shell as `conda` on windows refers to `conda.bat`, which requires a shell to work. - const stdOut = await shellExecute(quoted, { timeout: CONDA_GENERAL_TIMEOUT }) + const stdOut = await exec(this.command, ['--version'], { timeout: CONDA_GENERAL_TIMEOUT }) .then((result) => result.stdout.trim()) .catch(() => undefined); diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts index 8f302fd88253..aeac0df9de10 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -23,8 +23,11 @@ export class CondaService implements ICondaService { * Return the path to the "conda file". */ // eslint-disable-next-line class-methods-use-this - public async getCondaFile(): Promise { - return Conda.getConda().then((conda) => conda?.command ?? 'conda'); + public async getCondaFile(forShellExecution?: boolean): Promise { + return Conda.getConda().then((conda) => { + const command = forShellExecution ? conda?.shellCommand : conda?.command; + return command ?? 'conda'; + }); } // eslint-disable-next-line class-methods-use-this diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts index cba484ecfe48..cd8f62bf9a08 100644 --- a/src/client/pythonEnvironments/common/posixUtils.ts +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -17,9 +17,9 @@ export function matchBasicPythonBinFilename(filename: string): boolean { } /** - * Checks if a given path ends with python*.exe + * Checks if a given path matches pattern for standard non-windows python binary. * @param {string} interpreterPath : Path to python interpreter. - * @returns {boolean} : Returns true if the path matches pattern for windows python executable. + * @returns {boolean} : Returns true if the path matches pattern for non-windows python binary. */ export function matchPythonBinFilename(filename: string): boolean { /** diff --git a/src/client/pythonEnvironments/info/executable.ts b/src/client/pythonEnvironments/info/executable.ts index 659a62f7fa8f..a06055e67cbc 100644 --- a/src/client/pythonEnvironments/info/executable.ts +++ b/src/client/pythonEnvironments/info/executable.ts @@ -22,7 +22,10 @@ export async function getExecutablePath( const info = copyPythonExecInfo(python, args); const argv = [info.command, ...info.args]; // Concat these together to make a set of quoted strings - const quoted = argv.reduce((p, c) => (p ? `${p} ${c.toCommandArgument()}` : `${c.toCommandArgument()}`), ''); + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); const result = await shellExec(quoted, { timeout: timeout ?? 15000 }); const executable = parse(result.stdout.trim()); if (executable === '') { diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 381792a9a1d3..c70cd896b225 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -52,7 +52,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); - return copyPythonExecInfo(info, [managePyPath.fileToCommandArgument(), 'shell']); + return copyPythonExecInfo(info, [managePyPath.fileToCommandArgumentForPythonExt(), 'shell']); } public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 37770edb7b03..00c94bb70f7d 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -32,7 +32,9 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async executeFile(file: Uri) { await this.setCwdForFileExecution(file); - const { command, args } = await this.getExecuteFileArgs(file, [file.fsPath.fileToCommandArgument()]); + const x = file.fsPath; + const hello = x.fileToCommandArgumentForPythonExt(); + const { command, args } = await this.getExecuteFileArgs(file, [hello]); await this.getTerminalService(file).sendCommand(command, args); } @@ -106,7 +108,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.getTerminalService(file).sendText(`${fileDrive}:`); } } - await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgument()}`); + await this.getTerminalService(file).sendText(`cd ${fileDirPath.fileToCommandArgumentForPythonExt()}`); } } } diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts index fb6096ede07f..08b0281b4d54 100644 --- a/src/test/api.functional.test.ts +++ b/src/test/api.functional.test.ts @@ -84,7 +84,11 @@ suite('Extension API', () => { instance(serviceManager), instance(serviceContainer), ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); - const expectedArgs = [debuggerPath.fileToCommandArgument(), '--listen', `${debuggerHost}:${debuggerPort}`]; + const expectedArgs = [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${debuggerHost}:${debuggerPort}`, + ]; expect(args).to.be.deep.equal(expectedArgs); }); @@ -98,7 +102,7 @@ suite('Extension API', () => { instance(serviceContainer), ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); const expectedArgs = [ - debuggerPath.fileToCommandArgument(), + debuggerPath.fileToCommandArgumentForPythonExt(), '--listen', `${debuggerHost}:${debuggerPort}`, '--wait-for-client', diff --git a/src/test/common/extensions.unit.test.ts b/src/test/common/extensions.unit.test.ts index 133382b251fa..2e282cfc7d43 100644 --- a/src/test/common/extensions.unit.test.ts +++ b/src/test/common/extensions.unit.test.ts @@ -6,43 +6,47 @@ import { asyncFilter } from '../../client/common/utils/arrayUtils'; suite('String Extensions', () => { test('Should return empty string for empty arg', () => { const argTotest = ''; - expect(argTotest.toCommandArgument()).to.be.equal(''); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(''); }); test('Should quote an empty space', () => { const argTotest = ' '; - expect(argTotest.toCommandArgument()).to.be.equal('" "'); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal('" "'); }); test('Should not quote command arguments without spaces', () => { const argTotest = 'one.two.three'; - expect(argTotest.toCommandArgument()).to.be.equal(argTotest); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(argTotest); }); test('Should quote command arguments with spaces', () => { const argTotest = 'one two three'; - expect(argTotest.toCommandArgument()).to.be.equal(`"${argTotest}"`); + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); + }); + test('Should quote command arguments containing ampersand', () => { + const argTotest = 'one&twothree'; + expect(argTotest.toCommandArgumentForPythonExt()).to.be.equal(`"${argTotest}"`); }); test('Should return empty string for empty path', () => { const fileToTest = ''; - expect(fileToTest.fileToCommandArgument()).to.be.equal(''); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(''); }); test('Should not quote file argument without spaces', () => { const fileToTest = 'users/test/one'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(fileToTest); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(fileToTest); }); test('Should quote file argument with spaces', () => { const fileToTest = 'one two three'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest}"`); }); test('Should replace all back slashes with forward slashes (irrespective of OS)', () => { const fileToTest = 'c:\\users\\user\\conda\\scripts\\python.exe'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(fileToTest.replace(/\\/g, '/')); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(fileToTest.replace(/\\/g, '/')); }); test('Should replace all back slashes with forward slashes (irrespective of OS) and quoted when file has spaces', () => { const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); }); test('Should replace all back slashes with forward slashes (irrespective of OS) and quoted when file has spaces', () => { const fileToTest = 'c:\\users\\user namne\\conda path\\scripts\\python.exe'; - expect(fileToTest.fileToCommandArgument()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); + expect(fileToTest.fileToCommandArgumentForPythonExt()).to.be.equal(`"${fileToTest.replace(/\\/g, '/')}"`); }); test('Should leave string unchanged', () => { expect('something {0}'.format()).to.be.equal('something {0}'); diff --git a/src/test/common/installer/condaInstaller.unit.test.ts b/src/test/common/installer/condaInstaller.unit.test.ts index 6d1b2442d3ef..0124de62034b 100644 --- a/src/test/common/installer/condaInstaller.unit.test.ts +++ b/src/test/common/installer/condaInstaller.unit.test.ts @@ -99,7 +99,7 @@ suite('Common - Conda Installer', () => { when(configService.getSettings(uri)).thenReturn(instance(settings)); when(settings.pythonPath).thenReturn(pythonPath); - when(condaService.getCondaFile()).thenResolve(condaPath); + when(condaService.getCondaFile(true)).thenResolve(condaPath); when(condaLocatorService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); const execInfo = await installer.getExecutionInfo('abc', uri); @@ -122,13 +122,13 @@ suite('Common - Conda Installer', () => { when(configService.getSettings(uri)).thenReturn(instance(settings)); when(settings.pythonPath).thenReturn(pythonPath); - when(condaService.getCondaFile()).thenResolve(condaPath); + when(condaService.getCondaFile(true)).thenResolve(condaPath); when(condaLocatorService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); const execInfo = await installer.getExecutionInfo('abc', uri); assert.deepEqual(execInfo, { - args: ['install', '--prefix', condaEnv.path.fileToCommandArgument(), 'abc', '-y'], + args: ['install', '--prefix', condaEnv.path.fileToCommandArgumentForPythonExt(), 'abc', '-y'], execPath: condaPath, useShell: true, }); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index 952f10c0c60a..65e92ed4ace1 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -234,6 +234,9 @@ suite('Module Installer', () => { const condaService = TypeMoq.Mock.ofType(); condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaExecutable)); + condaService + .setup((c) => c.getCondaFile(true)) + .returns(() => Promise.resolve(condaExecutable)); const condaLocatorService = TypeMoq.Mock.ofType(); serviceContainer @@ -359,10 +362,14 @@ suite('Module Installer', () => { const expectedArgs = ['install']; if (condaEnvInfo && condaEnvInfo.name) { expectedArgs.push('--name'); - expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + expectedArgs.push( + condaEnvInfo.name.toCommandArgumentForPythonExt(), + ); } else if (condaEnvInfo && condaEnvInfo.path) { expectedArgs.push('--prefix'); - expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + expectedArgs.push( + condaEnvInfo.path.fileToCommandArgumentForPythonExt(), + ); } expectedArgs.push('"pylint<2.0.0"'); expectedArgs.push('-y'); @@ -402,10 +409,14 @@ suite('Module Installer', () => { const expectedArgs = ['install']; if (condaEnvInfo && condaEnvInfo.name) { expectedArgs.push('--name'); - expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + expectedArgs.push( + condaEnvInfo.name.toCommandArgumentForPythonExt(), + ); } else if (condaEnvInfo && condaEnvInfo.path) { expectedArgs.push('--prefix'); - expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + expectedArgs.push( + condaEnvInfo.path.fileToCommandArgumentForPythonExt(), + ); } expectedArgs.push('pylint'); expectedArgs.push('-y'); @@ -661,10 +672,12 @@ suite('Module Installer', () => { } if (condaEnvInfo && condaEnvInfo.name) { expectedArgs.push('--name'); - expectedArgs.push(condaEnvInfo.name.toCommandArgument()); + expectedArgs.push(condaEnvInfo.name.toCommandArgumentForPythonExt()); } else if (condaEnvInfo && condaEnvInfo.path) { expectedArgs.push('--prefix'); - expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); + expectedArgs.push( + condaEnvInfo.path.fileToCommandArgumentForPythonExt(), + ); } expectedArgs.push(moduleName); expectedArgs.push('-y'); diff --git a/src/test/common/terminals/activation.bash.unit.test.ts b/src/test/common/terminals/activation.bash.unit.test.ts index e523eb1e8de2..28bab49fd55a 100644 --- a/src/test/common/terminals/activation.bash.unit.test.ts +++ b/src/test/common/terminals/activation.bash.unit.test.ts @@ -114,7 +114,7 @@ suite('Terminal Environment Activation (bash)', () => { // Ensure it contains the name of the environment as an argument to the script file. expect(command).to.be.deep.equal( - [`source ${pathToScriptFile.fileToCommandArgument()}`.trim()], + [`source ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], 'Invalid command', ); } else { diff --git a/src/test/common/terminals/activation.commandPrompt.unit.test.ts b/src/test/common/terminals/activation.commandPrompt.unit.test.ts index f09e12e405b0..ed21d7625dab 100644 --- a/src/test/common/terminals/activation.commandPrompt.unit.test.ts +++ b/src/test/common/terminals/activation.commandPrompt.unit.test.ts @@ -109,7 +109,10 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { // Ensure the path is quoted if it contains any spaces. // Ensure it contains the name of the environment as an argument to the script file. - expect(commands).to.be.deep.equal([pathToScriptFile.fileToCommandArgument()], 'Invalid command'); + expect(commands).to.be.deep.equal( + [pathToScriptFile.fileToCommandArgumentForPythonExt()], + 'Invalid command', + ); }); test('Ensure batch files are not supported by powershell (on windows)', async () => { @@ -209,7 +212,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); expect(command).to.be.deep.equal( - [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], + [`& ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], 'Invalid command', ); }); @@ -225,7 +228,7 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); expect(command).to.be.deep.equal( - [`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], + [`& ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], 'Invalid command', ); }); diff --git a/src/test/common/terminals/activation.conda.unit.test.ts b/src/test/common/terminals/activation.conda.unit.test.ts index eb735b620aae..ed3a0983ba98 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -161,7 +161,9 @@ suite('Terminal Environment Activation conda', () => { ); condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.3.1', true)!)); - const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; + const expected = [ + `source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgumentForPythonExt()} EnvA`, + ]; const provider = new CondaActivationCommandProvider( condaService.object, @@ -190,7 +192,9 @@ suite('Terminal Environment Activation conda', () => { ); condaService.setup((c) => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); condaService.setup((c) => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.4.0', true)!)); - const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; + const expected = [ + `source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgumentForPythonExt()} EnvA`, + ]; const provider = new CondaActivationCommandProvider( condaService.object, @@ -311,7 +315,7 @@ suite('Terminal Environment Activation conda', () => { case TerminalShellType.powershellCore: case TerminalShellType.fish: { if (envName !== '') { - expectedActivationCommand = [`conda activate ${envName.toCommandArgument()}`]; + expectedActivationCommand = [`conda activate ${envName.toCommandArgumentForPythonExt()}`]; } else { expectedActivationCommand = [`conda activate ${expectEnvActivatePath}`]; } @@ -320,8 +324,8 @@ suite('Terminal Environment Activation conda', () => { default: { if (envName !== '') { expectedActivationCommand = isWindows - ? [`activate ${envName.toCommandArgument()}`] - : [`source activate ${envName.toCommandArgument()}`]; + ? [`activate ${envName.toCommandArgumentForPythonExt()}`] + : [`source activate ${envName.toCommandArgumentForPythonExt()}`]; } else { expectedActivationCommand = isWindows ? [`activate ${expectEnvActivatePath}`] diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index bdcfbb15db2d..cabf293ba958 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -127,7 +127,10 @@ suite('Activation of Environments in Terminal', () => { ): Promise { const terminal = vscode.window.createTerminal(); await sleep(consoleInitWaitMs); - terminal.sendText(`python ${pythonFile.toCommandArgument()} ${logFile.toCommandArgument()}`, true); + terminal.sendText( + `python ${pythonFile.toCommandArgumentForPythonExt()} ${logFile.toCommandArgumentForPythonExt()}`, + true, + ); await waitForCondition(() => fs.pathExists(logFile), logFileCreationWaitMs, `${logFile} file not created.`); return fs.readFile(logFile, 'utf-8'); diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index ef688ac2257d..59ac56ebf82e 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -120,7 +120,7 @@ suite('Terminal Service helpers', () => { item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()} 1 2`; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} 1 2`; const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); @@ -164,7 +164,7 @@ suite('Terminal Service helpers', () => { item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()}`; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()}`; const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); diff --git a/src/test/common/terminals/synchronousTerminalService.unit.test.ts b/src/test/common/terminals/synchronousTerminalService.unit.test.ts index ec1a67a4b302..f74c529ef470 100644 --- a/src/test/common/terminals/synchronousTerminalService.unit.test.ts +++ b/src/test/common/terminals/synchronousTerminalService.unit.test.ts @@ -105,7 +105,7 @@ suite('Terminal Service (synchronous)', () => { verify( terminalService.sendCommand( 'python', - deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]), + deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgumentForPythonExt()]), ), ).once(); }).timeout(1_000); @@ -141,7 +141,7 @@ suite('Terminal Service (synchronous)', () => { verify( terminalService.sendCommand( 'python', - deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgument()]), + deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgumentForPythonExt()]), ), ).once(); }).timeout(2_000); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index 789ffcf0974b..d50b2b5d5995 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -145,7 +145,7 @@ suite('Interpreters Activation - Python Environment Variables', () => { const expectedCommand = [ ...cmd, `echo '${getEnvironmentPrefix}'`, - `python ${printEnvPyFile.fileToCommandArgument()}`, + `python ${printEnvPyFile.fileToCommandArgumentForPythonExt()}`, ].join(' && '); expect(shellCmd).to.equal(expectedCommand); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index c6c441bc326d..aafe860aa0d5 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -297,17 +297,12 @@ suite('Python envs locator - Environments Resolver', () => { if (getOSType() !== OSType.Windows) { this.skip(); } - stubShellExec.restore(); sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (quoted: string) => { - const [command, ...args] = quoted.split(' '); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { return { stdout: JSON.stringify(condaInfo(path.join(envsWithoutPython, 'condaLackingPython'))) }; } - return { - stdout: - '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', - }; + throw new Error(`${command} is missing or is not executable`); }); const parentLocator = new SimpleLocator([]); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts index 73802c6fef6a..0e9f32ecffcb 100644 --- a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -247,8 +247,7 @@ suite('Resolver Utils', () => { test('resolveEnv (Windows)', async () => { sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (quoted: string) => { - const [command, ...args] = quoted.split(' '); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { return { stdout: JSON.stringify(condaInfo(condaPrefixWindows)) }; } @@ -263,8 +262,7 @@ suite('Resolver Utils', () => { test('resolveEnv (non-Windows)', async () => { sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (quoted: string) => { - const [command, ...args] = quoted.split(' '); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { return { stdout: JSON.stringify(condaInfo(condaPrefixNonWindows)) }; } @@ -280,9 +278,9 @@ suite('Resolver Utils', () => { ); }); - test('resolveEnv: If no conda binary found, resolve as a simple environment', async () => { + test('resolveEnv: If no conda binary found, resolve as an unknown environment', async () => { sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (command: string) => { + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { throw new Error(`${command} is missing or is not executable`); }); const actual = await resolveBasicEnv({ @@ -293,7 +291,7 @@ suite('Resolver Utils', () => { actual, createSimpleEnvInfo( path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), - PythonEnvKind.Conda, + PythonEnvKind.Unknown, undefined, 'conda1', path.join(TEST_LAYOUT_ROOT, 'conda1'), @@ -605,7 +603,7 @@ suite('Resolver Utils', () => { }); test('If data provided by registry is less informative than kind resolvers, do not use it to update environment', async () => { - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (command: string) => { + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { throw new Error(`${command} is missing or is not executable`); }); const interpreterPath = path.join(regTestRoot, 'conda3', 'python.exe'); @@ -616,8 +614,8 @@ suite('Resolver Utils', () => { }); const expected = buildEnvInfo({ location: path.join(regTestRoot, 'conda3'), - // Environment should already be marked as Conda. No need to update it to Global. - kind: PythonEnvKind.Conda, + // Environment is not marked as Conda, update it to Global. + kind: PythonEnvKind.OtherGlobal, executable: interpreterPath, // Registry does not provide the minor version, so keep version provided by Conda resolver instead. version: parseVersion('3.8.5'), diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts index 6387b4827e37..08d669298f6d 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts @@ -28,7 +28,7 @@ class Venvs { public async create(name: string): Promise<{ executable: string; envDir: string }> { const envName = this.resolve(name); - const argv = [PYTHON_PATH.fileToCommandArgument(), '-m', 'virtualenv', envName]; + const argv = [PYTHON_PATH.fileToCommandArgumentForPythonExt(), '-m', 'virtualenv', envName]; try { await run(argv, { cwd: this.root }); } catch (err) { diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts index a452cd2d2db7..b73f1153987f 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -185,8 +185,7 @@ suite('Conda and its environments are located correctly', () => { return contents; }); - sinon.stub(externalDependencies, 'shellExecute').callsFake(async (quoted: string) => { - const [command, ...args] = quoted.split(' '); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { for (const prefix of ['', ...execPath]) { const contents = getFile(path.join(prefix, command)); if (args[0] === 'info' && args[1] === '--json') { diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index f05869dafe69..6c6cf5baec76 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -154,7 +154,7 @@ suite('Terminal - Django Shell Code Execution', () => { const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); const expectedTerminalArgs = terminalArgs.concat( - `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt()}`, 'shell', ); @@ -168,7 +168,7 @@ suite('Terminal - Django Shell Code Execution', () => { const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); const expectedTerminalArgs = terminalArgs.concat( - path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt(), 'shell', ); @@ -183,7 +183,7 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); workspace.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); const expectedTerminalArgs = terminalArgs.concat( - `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt()}`, 'shell', ); @@ -198,7 +198,7 @@ suite('Terminal - Django Shell Code Execution', () => { workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); workspace.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); const expectedTerminalArgs = terminalArgs.concat( - path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt(), 'shell', ); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index f189521fb9e5..1f33b619fad0 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -236,7 +236,9 @@ suite('Terminal - Code Execution', () => { terminalService.verify( async (t) => - t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgument()}`)), + t.sendText( + TypeMoq.It.isValue(`cd ${path.dirname(file.fsPath).fileToCommandArgumentForPythonExt()}`), + ), TypeMoq.Times.once(), ); } @@ -259,7 +261,7 @@ suite('Terminal - Code Execution', () => { terminalSettings.setup((t) => t.launchArgs).returns(() => []); await executor.executeFile(file); - const dir = path.dirname(file.fsPath).fileToCommandArgument(); + const dir = path.dirname(file.fsPath).fileToCommandArgumentForPythonExt(); terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); } @@ -339,7 +341,7 @@ suite('Terminal - Code Execution', () => { await executor.executeFile(file); const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; - const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgument()); + const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgumentForPythonExt()); terminalService.verify( async (t) => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), @@ -408,7 +410,7 @@ suite('Terminal - Code Execution', () => { await executor.executeFile(file); - const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgument()]; + const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgumentForPythonExt()]; terminalService.verify( async (t) => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedArgs)), diff --git a/typings/extensions.d.ts b/typings/extensions.d.ts index 6a45fb979603..f524b63cc4df 100644 --- a/typings/extensions.d.ts +++ b/typings/extensions.d.ts @@ -2,10 +2,10 @@ // Licensed under the MIT License. /** -* @typedef {Object} SplitLinesOptions -* @property {boolean} [trim=true] - Whether to trim the lines. -* @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. -*/ + * @typedef {Object} SplitLinesOptions + * @property {boolean} [trim=true] - Whether to trim the lines. + * @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. + */ // https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript @@ -15,17 +15,17 @@ declare interface String { * By default lines are trimmed and empty lines are removed. * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. */ - splitLines(splitOptions?: { trim: boolean, removeEmptyEntries?: boolean }): string[]; + splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[]; /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - toCommandArgument(): string; + toCommandArgumentForPythonExt(): string; /** * Appropriately formats a a file path so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - fileToCommandArgument(): string; + fileToCommandArgumentForPythonExt(): string; /** * String.format() implementation. * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. @@ -38,7 +38,6 @@ declare interface String { trimQuotes(): string; } - declare interface Promise { /** * Catches task errors and ignores them.