Skip to content

Commit 5232537

Browse files
ericsnowcurrentlybrettcannon
authored andcommitted
#1944: Support a new "python.condaPath" setting, in case conda not on PATH. (#2605)
1 parent 8d2b9ea commit 5232537

8 files changed

Lines changed: 62 additions & 3 deletions

File tree

news/2 Fixes/1944.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a new "python.condaPath" to use if conda not found on PATH.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,12 @@
12471247
"description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.",
12481248
"scope": "resource"
12491249
},
1250+
"python.condaPath": {
1251+
"type": "string",
1252+
"default": "",
1253+
"description": "Path to the conda executable to use for activation (version 4.4+).",
1254+
"scope": "resource"
1255+
},
12501256
"python.sortImports.args": {
12511257
"type": "array",
12521258
"description": "Arguments passed in. Each argument is a separate item in the array.",

src/client/common/configSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
3535
public envFile = '';
3636
public venvPath = '';
3737
public venvFolders: string[] = [];
38+
public condaPath = '';
3839
public devOptions: string[] = [];
3940
public linting!: ILintingSettings;
4041
public formatting!: IFormattingSettings;

src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
5050
`& cmd /k "activate ${envInfo.name.toCommandArgument().replace(/"/g, '""')} & ${powershellExe}"`
5151
];
5252
} else if (targetShell === TerminalShellType.fish) {
53+
const conda = await condaService.getCondaFile();
5354
// https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28
54-
return [`conda activate ${envInfo.name.toCommandArgument()}`];
55+
return [`${conda.fileToCommandArgument()} activate ${envInfo.name.toCommandArgument()}`];
5556
} else if (isWindows) {
5657
return [`activate ${envInfo.name.toCommandArgument()}`];
5758
} else {

src/client/common/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export interface IPythonSettings {
125125
readonly pythonPath: string;
126126
readonly venvPath: string;
127127
readonly venvFolders: string[];
128+
readonly condaPath: string;
128129
readonly downloadLanguageServer: boolean;
129130
readonly jediEnabled: boolean;
130131
readonly jediPath: string;

src/client/interpreter/locators/services/condaService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from 'path';
33
import { compareVersion } from '../../../../utils/version';
44
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
55
import { IProcessServiceFactory } from '../../../common/process/types';
6-
import { ILogger, IPersistentStateFactory } from '../../../common/types';
6+
import { IConfigurationService, ILogger, IPersistentStateFactory } from '../../../common/types';
77
import { IServiceContainer } from '../../../ioc/types';
88
import { CondaInfo, ICondaService, IInterpreterLocatorService, InterpreterType, PythonInterpreter, WINDOWS_REGISTRY_SERVICE } from '../../contracts';
99
import { CondaHelper } from './condaHelper';
@@ -223,6 +223,12 @@ export class CondaService implements ICondaService {
223223
* Return the path to the "conda file", if there is one (in known locations).
224224
*/
225225
private async getCondaFileImpl() {
226+
const settings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings();
227+
const setting = settings.condaPath;
228+
if (setting && setting !== '') {
229+
return setting;
230+
}
231+
226232
const isAvailable = await this.isCondaInCurrentPath();
227233
if (isAvailable) {
228234
return 'conda';

src/test/common/terminals/activation.conda.unit.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ suite('Terminal Environment Activation conda', () => {
2929
let processService: TypeMoq.IMock<IProcessService>;
3030
let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>;
3131
let condaService: TypeMoq.IMock<ICondaService>;
32+
let conda: string;
3233

3334
setup(() => {
35+
conda = 'conda';
3436
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
3537
disposables = [];
3638
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables);
@@ -39,6 +41,8 @@ suite('Terminal Environment Activation conda', () => {
3941
platformService = TypeMoq.Mock.ofType<IPlatformService>();
4042
processService = TypeMoq.Mock.ofType<IProcessService>();
4143
condaService = TypeMoq.Mock.ofType<ICondaService>();
44+
condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(conda));
45+
4246
processService.setup((x: any) => x.then).returns(() => undefined);
4347
procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>();
4448
procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object));
@@ -73,6 +77,20 @@ suite('Terminal Environment Activation conda', () => {
7377
expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined');
7478
});
7579

80+
test('Conda activation for fish escapes spaces in conda filename', async () => {
81+
conda = 'path to conda';
82+
const envName = 'EnvA';
83+
const pythonPath = 'python3';
84+
platformService.setup(p => p.isWindows).returns(() => false);
85+
condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) }));
86+
const expected = ['"path to conda" activate EnvA'];
87+
88+
const provider = new CondaActivationCommandProvider(serviceContainer.object);
89+
const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.fish);
90+
91+
expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command');
92+
});
93+
7694
async function expectNoCondaActivationCommandForPowershell(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType, hasSpaceInEnvironmentName = false) {
7795
terminalSettings.setup(t => t.activateEnvironment).returns(() => true);
7896
platformService.setup(p => p.isLinux).returns(() => isLinux);

src/test/interpreters/condaService.unit.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as TypeMoq from 'typemoq';
77
import { FileSystem } from '../../client/common/platform/fileSystem';
88
import { IFileSystem, IPlatformService } from '../../client/common/platform/types';
99
import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types';
10-
import { ILogger, IPersistentStateFactory } from '../../client/common/types';
10+
import { IConfigurationService, ILogger, IPersistentStateFactory, IPythonSettings } from '../../client/common/types';
1111
import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts';
1212
import { CondaService } from '../../client/interpreter/locators/services/condaService';
1313
import { IServiceContainer } from '../../client/ioc/types';
@@ -35,16 +35,22 @@ suite('Interpreters Conda Service', () => {
3535
let platformService: TypeMoq.IMock<IPlatformService>;
3636
let condaService: CondaService;
3737
let fileSystem: TypeMoq.IMock<IFileSystem>;
38+
let config: TypeMoq.IMock<IConfigurationService>;
39+
let settings: TypeMoq.IMock<IPythonSettings>;
3840
let registryInterpreterLocatorService: TypeMoq.IMock<IInterpreterLocatorService>;
3941
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
4042
let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>;
4143
let logger: TypeMoq.IMock<ILogger>;
44+
let condaPathSetting: string;
4245
setup(async () => {
46+
condaPathSetting = '';
4347
logger = TypeMoq.Mock.ofType<ILogger>();
4448
processService = TypeMoq.Mock.ofType<IProcessService>();
4549
platformService = TypeMoq.Mock.ofType<IPlatformService>();
4650
registryInterpreterLocatorService = TypeMoq.Mock.ofType<IInterpreterLocatorService>();
4751
fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
52+
config = TypeMoq.Mock.ofType<IConfigurationService>();
53+
settings = TypeMoq.Mock.ofType<IPythonSettings>();
4854
procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>();
4955
processService.setup((x: any) => x.then).returns(() => undefined);
5056
procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object));
@@ -54,6 +60,9 @@ suite('Interpreters Conda Service', () => {
5460
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object);
5561
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())).returns(() => logger.object);
5662
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object);
63+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => config.object);
64+
config.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object);
65+
settings.setup(p => p.condaPath).returns(() => condaPathSetting);
5766
condaService = new CondaService(serviceContainer.object, registryInterpreterLocatorService.object);
5867

5968
fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p1, p2) => {
@@ -331,6 +340,22 @@ suite('Interpreters Conda Service', () => {
331340
assert.equal(condaExe, 'conda', 'Failed to identify conda.exe');
332341
});
333342

343+
test('Must use \'python.condaPath\' setting if set', async () => {
344+
condaPathSetting = 'spam-spam-conda-spam-spam';
345+
// We ensure that conda would otherwise be found.
346+
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version'])))
347+
.returns(() => Promise.resolve({ stdout: 'xyz' }))
348+
.verifiable(TypeMoq.Times.never());
349+
350+
const condaExe = await condaService.getCondaFile();
351+
assert.equal(condaExe, 'spam-spam-conda-spam-spam', 'Failed to identify conda.exe');
352+
353+
// We should not try to call other unwanted methods.
354+
processService.verifyAll();
355+
platformService.verify(p => p.isWindows, TypeMoq.Times.never());
356+
registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never());
357+
});
358+
334359
test('Must use \'conda\' if is available in the current path', async () => {
335360
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' }));
336361

0 commit comments

Comments
 (0)