diff --git a/news/2 Fixes/1944.md b/news/2 Fixes/1944.md new file mode 100644 index 000000000000..c7d5a12c3271 --- /dev/null +++ b/news/2 Fixes/1944.md @@ -0,0 +1 @@ +Add a new "python.condaPath" to use if conda not found on PATH. diff --git a/package.json b/package.json index 7f752fdec403..4dfd43964d9f 100644 --- a/package.json +++ b/package.json @@ -1247,6 +1247,12 @@ "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", "scope": "resource" }, + "python.condaPath": { + "type": "string", + "default": "", + "description": "Path to the conda executable to use for activation (version 4.4+).", + "scope": "resource" + }, "python.sortImports.args": { "type": "array", "description": "Arguments passed in. Each argument is a separate item in the array.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 26311e9657be..5ad565254bc1 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -35,6 +35,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { public envFile = ''; public venvPath = ''; public venvFolders: string[] = []; + public condaPath = ''; public devOptions: string[] = []; public linting!: ILintingSettings; public formatting!: IFormattingSettings; diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index eb4b1ba54cb4..72c135f26e61 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -50,8 +50,9 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman `& cmd /k "activate ${envInfo.name.toCommandArgument().replace(/"/g, '""')} & ${powershellExe}"` ]; } else if (targetShell === TerminalShellType.fish) { + const conda = await condaService.getCondaFile(); // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 - return [`conda activate ${envInfo.name.toCommandArgument()}`]; + return [`${conda.fileToCommandArgument()} activate ${envInfo.name.toCommandArgument()}`]; } else if (isWindows) { return [`activate ${envInfo.name.toCommandArgument()}`]; } else { diff --git a/src/client/common/types.ts b/src/client/common/types.ts index c42889e9964e..070a057c8450 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -125,6 +125,7 @@ export interface IPythonSettings { readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly condaPath: string; readonly downloadLanguageServer: boolean; readonly jediEnabled: boolean; readonly jediPath: string; diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts index aed0d3384093..267ab07949e8 100644 --- a/src/client/interpreter/locators/services/condaService.ts +++ b/src/client/interpreter/locators/services/condaService.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { compareVersion } from '../../../../utils/version'; import { IFileSystem, IPlatformService } from '../../../common/platform/types'; import { IProcessServiceFactory } from '../../../common/process/types'; -import { ILogger, IPersistentStateFactory } from '../../../common/types'; +import { IConfigurationService, ILogger, IPersistentStateFactory } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { CondaInfo, ICondaService, IInterpreterLocatorService, InterpreterType, PythonInterpreter, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; import { CondaHelper } from './condaHelper'; @@ -223,6 +223,12 @@ export class CondaService implements ICondaService { * Return the path to the "conda file", if there is one (in known locations). */ private async getCondaFileImpl() { + const settings = this.serviceContainer.get(IConfigurationService).getSettings(); + const setting = settings.condaPath; + if (setting && setting !== '') { + return setting; + } + const isAvailable = await this.isCondaInCurrentPath(); if (isAvailable) { return 'conda'; diff --git a/src/test/common/terminals/activation.conda.unit.test.ts b/src/test/common/terminals/activation.conda.unit.test.ts index 3c8485daa7b4..d54ed9464f18 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -29,8 +29,10 @@ suite('Terminal Environment Activation conda', () => { let processService: TypeMoq.IMock; let procServiceFactory: TypeMoq.IMock; let condaService: TypeMoq.IMock; + let conda: string; setup(() => { + conda = 'conda'; serviceContainer = TypeMoq.Mock.ofType(); disposables = []; serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); @@ -39,6 +41,8 @@ suite('Terminal Environment Activation conda', () => { platformService = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); condaService = TypeMoq.Mock.ofType(); + condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(conda)); + processService.setup((x: any) => x.then).returns(() => undefined); procServiceFactory = TypeMoq.Mock.ofType(); procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); @@ -73,6 +77,20 @@ suite('Terminal Environment Activation conda', () => { expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); }); + test('Conda activation for fish escapes spaces in conda filename', async () => { + conda = 'path to conda'; + const envName = 'EnvA'; + const pythonPath = 'python3'; + platformService.setup(p => p.isWindows).returns(() => false); + condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); + const expected = ['"path to conda" activate EnvA']; + + const provider = new CondaActivationCommandProvider(serviceContainer.object); + const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.fish); + + expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); + }); + async function expectNoCondaActivationCommandForPowershell(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType, hasSpaceInEnvironmentName = false) { terminalSettings.setup(t => t.activateEnvironment).returns(() => true); platformService.setup(p => p.isLinux).returns(() => isLinux); diff --git a/src/test/interpreters/condaService.unit.test.ts b/src/test/interpreters/condaService.unit.test.ts index bf4fa4bc1d25..8e17d117d749 100644 --- a/src/test/interpreters/condaService.unit.test.ts +++ b/src/test/interpreters/condaService.unit.test.ts @@ -7,7 +7,7 @@ import * as TypeMoq from 'typemoq'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; -import { ILogger, IPersistentStateFactory } from '../../client/common/types'; +import { IConfigurationService, ILogger, IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { CondaService } from '../../client/interpreter/locators/services/condaService'; import { IServiceContainer } from '../../client/ioc/types'; @@ -35,16 +35,22 @@ suite('Interpreters Conda Service', () => { let platformService: TypeMoq.IMock; let condaService: CondaService; let fileSystem: TypeMoq.IMock; + let config: TypeMoq.IMock; + let settings: TypeMoq.IMock; let registryInterpreterLocatorService: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let procServiceFactory: TypeMoq.IMock; let logger: TypeMoq.IMock; + let condaPathSetting: string; setup(async () => { + condaPathSetting = ''; logger = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); platformService = TypeMoq.Mock.ofType(); registryInterpreterLocatorService = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); + config = TypeMoq.Mock.ofType(); + settings = TypeMoq.Mock.ofType(); procServiceFactory = TypeMoq.Mock.ofType(); processService.setup((x: any) => x.then).returns(() => undefined); procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); @@ -54,6 +60,9 @@ suite('Interpreters Conda Service', () => { serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())).returns(() => logger.object); serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => config.object); + config.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object); + settings.setup(p => p.condaPath).returns(() => condaPathSetting); condaService = new CondaService(serviceContainer.object, registryInterpreterLocatorService.object); fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1, p2) => { @@ -331,6 +340,22 @@ suite('Interpreters Conda Service', () => { assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); }); + test('Must use \'python.condaPath\' setting if set', async () => { + condaPathSetting = 'spam-spam-conda-spam-spam'; + // We ensure that conda would otherwise be found. + processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))) + .returns(() => Promise.resolve({ stdout: 'xyz' })) + .verifiable(TypeMoq.Times.never()); + + const condaExe = await condaService.getCondaFile(); + assert.equal(condaExe, 'spam-spam-conda-spam-spam', 'Failed to identify conda.exe'); + + // We should not try to call other unwanted methods. + processService.verifyAll(); + platformService.verify(p => p.isWindows, TypeMoq.Times.never()); + registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + test('Must use \'conda\' if is available in the current path', async () => { processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' }));