Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activate conda environment using path when name is not available #8397

Merged
merged 3 commits into from
Nov 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1 Enhancements/3834.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Activate conda environment using path when name is not available.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
@inject(ICondaService) private readonly condaService: ICondaService,
@inject(IPlatformService) private platform: IPlatformService,
@inject(IConfigurationService) private configService: IConfigurationService
) { }
) {}

/**
* Is the given shell supported for activating a conda env?
Expand All @@ -53,43 +53,44 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
return;
}

const condaEnv = envInfo.name.length > 0 ? envInfo.name : envInfo.path;

// Algorithm differs based on version
// Old version, just call activate directly.
// New version, call activate from the same path as our python path, then call it again to activate our environment.
// -- note that the 'default' conda location won't allow activate to work for the environment sometimes.
const versionInfo = await this.condaService.getCondaVersion();
if (versionInfo && versionInfo.major >= CondaRequiredMajor) {
// Conda added support for powershell in 4.6.
if (versionInfo.minor >= CondaRequiredMinorForPowerShell &&
(targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore)) {
return this.getPowershellCommands(envInfo.name);
if (versionInfo.minor >= CondaRequiredMinorForPowerShell && (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore)) {
return this.getPowershellCommands(condaEnv);
}
if (versionInfo.minor >= CondaRequiredMinor) {
// New version.
const interpreterPath = await this.condaService.getCondaFileFromInterpreter(pythonPath, envInfo.name);
if (interpreterPath) {
const activatePath = path.join(path.dirname(interpreterPath), 'activate').fileToCommandArgument();
const firstActivate = this.platform.isWindows ? activatePath : `source ${activatePath}`;
return [firstActivate, `conda activate ${envInfo.name.toCommandArgument()}`];
return [firstActivate, `conda activate ${condaEnv.toCommandArgument()}`];
}
}
}

switch (targetShell) {
case TerminalShellType.powershell:
case TerminalShellType.powershellCore:
return this.getPowershellCommands(envInfo.name);
return this.getPowershellCommands(condaEnv);

// tslint:disable-next-line:no-suspicious-comment
// TODO: Do we really special-case fish on Windows?
case TerminalShellType.fish:
return this.getFishCommands(envInfo.name, await this.condaService.getCondaFile());
return this.getFishCommands(condaEnv, await this.condaService.getCondaFile());

default:
if (this.platform.isWindows) {
return this.getWindowsCommands(envInfo.name);
return this.getWindowsCommands(condaEnv);
} else {
return this.getUnixCommands(envInfo.name, await this.condaService.getCondaFile());
return this.getUnixCommands(condaEnv, await this.condaService.getCondaFile());
}
}
}
Expand All @@ -109,9 +110,9 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
return activateCmd;
}

public async getWindowsCommands(envName: string): Promise<string[] | undefined> {
public async getWindowsCommands(condaEnv: string): Promise<string[] | undefined> {
const activate = await this.getWindowsActivateCommand();
return [`${activate} ${envName.toCommandArgument()}`];
return [`${activate} ${condaEnv.toCommandArgument()}`];
}
/**
* The expectation is for the user to configure Powershell for Conda.
Expand All @@ -123,18 +124,18 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
* @returns {(Promise<string[] | undefined>)}
* @memberof CondaActivationCommandProvider
*/
public async getPowershellCommands(envName: string): Promise<string[] | undefined> {
return [`conda activate ${envName.toCommandArgument()}`];
public async getPowershellCommands(condaEnv: string): Promise<string[] | undefined> {
return [`conda activate ${condaEnv.toCommandArgument()}`];
}

public async getFishCommands(envName: string, conda: string): Promise<string[] | undefined> {
public async getFishCommands(condaEnv: string, condaFile: string): Promise<string[] | undefined> {
// https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28
return [`${conda.fileToCommandArgument()} activate ${envName.toCommandArgument()}`];
return [`${condaFile.fileToCommandArgument()} activate ${condaEnv.toCommandArgument()}`];
}

public async getUnixCommands(envName: string, conda: string): Promise<string[] | undefined> {
const condaDir = path.dirname(conda);
public async getUnixCommands(condaEnv: string, condaFile: string): Promise<string[] | undefined> {
const condaDir = path.dirname(condaFile);
const activateFile = path.join(condaDir, 'activate');
return [`source ${activateFile.fileToCommandArgument()} ${envName.toCommandArgument()}`];
return [`source ${activateFile.fileToCommandArgument()} ${condaEnv.toCommandArgument()}`];
}
}
2 changes: 1 addition & 1 deletion src/client/interpreter/locators/services/conda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export const AnacondaCompanyName = 'Anaconda, Inc.';
// tslint:disable-next-line:variable-name
export const AnacondaDisplayName = 'Anaconda';
// tslint:disable-next-line:variable-name
export const AnacondaIdentfiers = ['Anaconda', 'Conda', 'Continuum'];
export const AnacondaIdentifiers = ['Anaconda', 'Conda', 'Continuum'];
11 changes: 5 additions & 6 deletions src/client/interpreter/locators/services/condaHelper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as path from 'path';
import '../../../common/extensions';
import { CondaInfo } from '../../contracts';
import { AnacondaDisplayName, AnacondaIdentfiers } from './conda';
import { AnacondaDisplayName, AnacondaIdentifiers } from './conda';

export type EnvironmentPath = string;
export type EnvironmentName = string;
Expand All @@ -13,7 +12,6 @@ export type EnvironmentName = string;
* Helpers for conda.
*/
export class CondaHelper {

/**
* Return the string to display for the conda interpreter.
*/
Expand Down Expand Up @@ -51,6 +49,8 @@ export class CondaHelper {
* py27 /Users/donjayamanne/anaconda3/envs/py27
* py36 /Users/donjayamanne/anaconda3/envs/py36
* three /Users/donjayamanne/anaconda3/envs/three
* /Users/donjayamanne/anaconda3/envs/four
* /Users/donjayamanne/anaconda3/envs/five 5
* @param {string} condaEnvironmentList
* @param {CondaInfo} condaInfo
* @returns {{ name: string, path: string }[] | undefined}
Expand All @@ -73,8 +73,7 @@ export class CondaHelper {
name = name.substring(0, name.length - 1).trim();
}
const envPath = line.substring(pathStartIndex).trim();
name = name.length === 0 ? path.basename(envPath) : name;
if (name.length > 0 && envPath.length > 0) {
if (envPath.length > 0) {
envs.push({ name, path: envPath });
}
});
Expand All @@ -87,6 +86,6 @@ export class CondaHelper {
*/
private isIdentifiableAsAnaconda(value: string) {
const valueToSearch = value.toLowerCase();
return AnacondaIdentfiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1);
return AnacondaIdentifiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1);
}
}
74 changes: 50 additions & 24 deletions src/test/common/terminals/activation.conda.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,18 @@ suite('Terminal Environment Activation conda', () => {
envName: environmentNameHasSpaces,
expectedResult: ['source path/to/activate', 'conda activate "Env with spaces"'],
isWindows: false
},
{
testName: 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, and no env name',
envName: '',
expectedResult: ['path/to/activate', `conda activate .`],
isWindows: true
},
{
testName: 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, and no env name',
envName: '',
expectedResult: ['source path/to/activate', `conda activate .`],
isWindows: false
}
];

Expand All @@ -215,79 +227,93 @@ suite('Terminal Environment Activation conda', () => {
});
});

async function testCondaActivationCommands(
isWindows: boolean,
isOsx: boolean,
isLinux: boolean,
pythonPath: string,
shellType: TerminalShellType,
hasSpaceInEnvironmentName = false
) {
async function testCondaActivationCommands(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType, envName: string) {
terminalSettings.setup(t => t.activateEnvironment).returns(() => true);
platformService.setup(p => p.isLinux).returns(() => isLinux);
platformService.setup(p => p.isWindows).returns(() => isWindows);
platformService.setup(p => p.isMac).returns(() => isOsx);
condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true));
pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath);
const envName = hasSpaceInEnvironmentName ? 'EnvA' : 'Env A';
condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) }));

const activationCommands = await new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object).getActivationCommands(
undefined,
shellType
);
let expectedActivationCommamnd: string[] | undefined;
let expectedActivationCommand: string[] | undefined;
const expectEnvActivatePath = path.dirname(pythonPath);
switch (shellType) {
case TerminalShellType.powershell:
case TerminalShellType.powershellCore: {
expectedActivationCommamnd = [`conda activate ${envName.toCommandArgument()}`];
break;
}
case TerminalShellType.powershellCore:
case TerminalShellType.fish: {
expectedActivationCommamnd = [`conda activate ${envName.toCommandArgument()}`];
if (envName !== '') {
expectedActivationCommand = [`conda activate ${envName.toCommandArgument()}`];
} else {
expectedActivationCommand = [`conda activate ${expectEnvActivatePath}`];
}
break;
}
default: {
expectedActivationCommamnd = isWindows ? [`activate ${envName.toCommandArgument()}`] : [`source activate ${envName.toCommandArgument()}`];
if (envName !== '') {
expectedActivationCommand = isWindows ? [`activate ${envName.toCommandArgument()}`] : [`source activate ${envName.toCommandArgument()}`];
} else {
expectedActivationCommand = isWindows ? [`activate ${expectEnvActivatePath}`] : [`source activate ${expectEnvActivatePath}`];
}
break;
}
}
if (expectedActivationCommamnd) {
expect(activationCommands).to.deep.equal(expectedActivationCommamnd, 'Incorrect Activation command');
if (expectedActivationCommand) {
expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command');
} else {
expect(activationCommands).to.equal(undefined, 'Incorrect Activation command');
}
}
getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => {
test(`Conda activation command for shell ${shellType.name} on (windows)`, async () => {
const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe');
await testCondaActivationCommands(true, false, false, pythonPath, shellType.value);
await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env');
});

test(`Conda activation command for shell ${shellType.name} on (linux)`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, false, true, pythonPath, shellType.value);
await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env');
});

test(`Conda activation command for shell ${shellType.name} on (mac)`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, true, false, pythonPath, shellType.value);
await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env');
});
});
getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => {
test(`Conda activation command for shell ${shellType.name} on (windows), containing spaces in environment name`, async () => {
const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe');
await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, true);
await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env A');
});

test(`Conda activation command for shell ${shellType.name} on (linux), containing spaces in environment name`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, true);
await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env A');
});

test(`Conda activation command for shell ${shellType.name} on (mac), containing spaces in environment name`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, true);
await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env A');
});
});
getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => {
test(`Conda activation command for shell ${shellType.name} on (windows), containing no environment name`, async () => {
const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe');
await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, '');
});

test(`Conda activation command for shell ${shellType.name} on (linux), containing no environment name`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, '');
});

test(`Conda activation command for shell ${shellType.name} on (mac), containing no environment name`, async () => {
const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python');
await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, '');
});
});
async function expectCondaActivationCommand(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string) {
Expand Down
4 changes: 2 additions & 2 deletions src/test/interpreters/condaHelper.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ three3 /Users/donjayamanne/anaconda3/envs/three
{ name: 'one1', path: '/Users/donjayamanne/anaconda3/envs/one' },
{ name: 'two2 2', path: '/Users/donjayamanne/anaconda3/envs/two 2' },
{ name: 'three3', path: '/Users/donjayamanne/anaconda3/envs/three' },
{ name: 'four', path: '/Users/donjayamanne/anaconda3/envs/four' },
{ name: 'five 5', path: '/Users/donjayamanne/anaconda3/envs/five 5' }
{ name: '', path: '/Users/donjayamanne/anaconda3/envs/four' },
{ name: '', path: '/Users/donjayamanne/anaconda3/envs/five 5' }
];

const list = condaHelper.parseCondaEnvironmentNames(environments);
Expand Down