Skip to content

Commit 4a1a7ef

Browse files
authored
Activate conda envs with fully qualified paths to activate script (#2801)
For #1882
1 parent 3a210e2 commit 4a1a7ef

File tree

7 files changed

+58
-41
lines changed

7 files changed

+58
-41
lines changed

src/client/common/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function error(title: string = '', message: any) {
3838
new Logger().logError(`${title}, ${message}`);
3939
}
4040
// tslint:disable-next-line:no-any
41-
export function warn(title: string = '', message: any) {
41+
export function warn(title: string = '', message: any = '') {
4242
new Logger().logWarning(`${title}, ${message}`);
4343
}
4444

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

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import { injectable } from 'inversify';
55
import * as path from 'path';
66
import { Uri } from 'vscode';
7-
import { compareVersion } from '../../../../utils/version';
87
import { ICondaService } from '../../../interpreter/contracts';
98
import { IServiceContainer } from '../../../ioc/types';
109
import '../../extensions';
@@ -70,7 +69,6 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
7069
default:
7170
return this.getUnixCommands(
7271
envInfo.name,
73-
await condaService.getCondaVersion() || '',
7472
await condaService.getCondaFile()
7573
);
7674
}
@@ -134,28 +132,12 @@ export class CondaActivationCommandProvider implements ITerminalActivationComman
134132

135133
public async getUnixCommands(
136134
envName: string,
137-
version: string,
138135
conda: string
139136
): Promise<string[] | undefined> {
140-
// Conda changed how activation works in the 4.4.0 release, so
141-
// we accommodate the two ways distinctly.
142-
if (version === '4.4.0' || compareVersion(version, '4.4.0') > 0) {
143-
// Note that this requires the user to have already followed
144-
// the conda instructions such that "conda" is on their
145-
// $PATH. While we *could* use "source <abs-path-to-activate>"
146-
// (after resolving the absolute path to the "activate"
147-
// script), we're going to avoid operating contrary to
148-
// conda's recommendations.
149-
return [
150-
`${conda.fileToCommandArgument()} activate ${envName.toCommandArgument()}`
151-
];
152-
} else {
153-
// tslint:disable-next-line:no-suspicious-comment
154-
// TODO: Handle pre-4.4 case where "activate" script not on $PATH.
155-
// (Locate script next to "conda" binary and use absolute path.
156-
return [
157-
`source activate ${envName.toCommandArgument()}`
158-
];
159-
}
137+
const condaDir = path.dirname(conda);
138+
const activateFile = path.join(condaDir, 'activate');
139+
return [
140+
`source ${activateFile.fileToCommandArgument()} ${envName.toCommandArgument()}`
141+
];
160142
}
161143
}

src/client/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ async function sendStartupTelemetry(activatedPromise: Promise<void>, serviceCont
217217
const condaLocator = serviceContainer.get<ICondaService>(ICondaService);
218218
const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService);
219219
const [condaVersion, interpreter, interpreters] = await Promise.all([
220-
condaLocator.getCondaVersion().catch(() => undefined),
220+
condaLocator.getCondaVersion().then(ver => ver ? ver.raw : '').catch<string>(() => ''),
221221
interpreterService.getActiveInterpreter().catch<PythonInterpreter | undefined>(() => undefined),
222222
interpreterService.getInterpreters().catch<PythonInterpreter[]>(() => [])
223223
]);

src/client/interpreter/contracts.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SemVer } from 'semver';
12
import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode';
23
import { InterpreterInfomation } from '../common/process/types';
34

@@ -36,6 +37,7 @@ export type CondaInfo = {
3637
'sys.prefix'?: string;
3738
'python_version'?: string;
3839
default_prefix?: string;
40+
conda_version?: string;
3941
};
4042

4143
export const ICondaService = Symbol('ICondaService');
@@ -44,7 +46,7 @@ export interface ICondaService {
4446
readonly condaEnvironmentsFile: string | undefined;
4547
getCondaFile(): Promise<string>;
4648
isCondaAvailable(): Promise<boolean>;
47-
getCondaVersion(): Promise<string | undefined>;
49+
getCondaVersion(): Promise<SemVer | undefined>;
4850
getCondaInfo(): Promise<CondaInfo | undefined>;
4951
getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>;
5052
getInterpreterPath(condaEnvironmentPath: string): string;

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { inject, injectable, named, optional } from 'inversify';
22
import * as path from 'path';
3+
import { parse, SemVer } from 'semver';
34
import { compareVersion } from '../../../../utils/version';
5+
import { warn } from '../../../common/logger';
46
import { IFileSystem, IPlatformService } from '../../../common/platform/types';
57
import { IProcessServiceFactory } from '../../../common/process/types';
68
import { IConfigurationService, ILogger, IPersistentStateFactory } from '../../../common/types';
@@ -74,19 +76,37 @@ export class CondaService implements ICondaService {
7476
return this.isAvailable;
7577
}
7678
return this.getCondaVersion()
77-
.then(version => this.isAvailable = typeof version === 'string')
79+
.then(version => this.isAvailable = version !== undefined)
7880
.catch(() => this.isAvailable = false);
7981
}
8082

8183
/**
8284
* Return the conda version.
8385
*/
84-
public async getCondaVersion(): Promise<string | undefined> {
86+
public async getCondaVersion(): Promise<SemVer | undefined> {
8587
const processService = await this.processServiceFactory.create();
86-
return this.getCondaFile()
87-
.then(condaFile => processService.exec(condaFile, ['--version'], {}))
88-
.then(result => result.stdout.trim())
89-
.catch(() => undefined);
88+
const info = await this.getCondaInfo().catch<CondaInfo | undefined>(() => undefined);
89+
let versionString: string | undefined;
90+
if (info && info.conda_version) {
91+
versionString = info.conda_version;
92+
} else {
93+
const stdOut = await this.getCondaFile()
94+
.then(condaFile => processService.exec(condaFile, ['--version'], {}))
95+
.then(result => result.stdout.trim())
96+
.catch<string | undefined>(() => undefined);
97+
98+
versionString = (stdOut && stdOut.startsWith('conda ')) ? stdOut.substring('conda '.length).trim() : stdOut;
99+
}
100+
if (!versionString) {
101+
return;
102+
}
103+
const version = parse(versionString, true);
104+
if (version) {
105+
return version;
106+
}
107+
// Use a bogus version, at least to indicate the fact that a version was returned.
108+
warn(`Unable to parse Version of Conda, ${versionString}`);
109+
return new SemVer('0.0.1');
90110
}
91111

92112
/**

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { expect } from 'chai';
77
import * as path from 'path';
8+
import { parse } from 'semver';
89
import * as TypeMoq from 'typemoq';
910
import { Disposable } from 'vscode';
1011
import '../../../client/common/extensions';
@@ -105,15 +106,19 @@ suite('Terminal Environment Activation conda', () => {
105106
test('Conda activation on bash uses "source" before 4.4.0', async () => {
106107
const envName = 'EnvA';
107108
const pythonPath = 'python3';
109+
const condaPath = path.join('a', 'b', 'c', 'conda');
108110
platformService.setup(p => p.isWindows).returns(() => false);
111+
condaService.reset();
109112
condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny()))
110113
.returns(() => Promise.resolve({
111114
name: envName,
112115
path: path.dirname(pythonPath)
113116
}));
117+
condaService.setup(c => c.getCondaFile())
118+
.returns(() => Promise.resolve(condaPath));
114119
condaService.setup(c => c.getCondaVersion())
115-
.returns(() => Promise.resolve('4.3.1'));
116-
const expected = ['source activate EnvA'];
120+
.returns(() => Promise.resolve(parse('4.3.1', true)!));
121+
const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`];
117122

118123
const provider = new CondaActivationCommandProvider(serviceContainer.object);
119124
const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash);
@@ -124,15 +129,19 @@ suite('Terminal Environment Activation conda', () => {
124129
test('Conda activation on bash uses "conda" after 4.4.0', async () => {
125130
const envName = 'EnvA';
126131
const pythonPath = 'python3';
132+
const condaPath = path.join('a', 'b', 'c', 'conda');
127133
platformService.setup(p => p.isWindows).returns(() => false);
134+
condaService.reset();
128135
condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny()))
129136
.returns(() => Promise.resolve({
130137
name: envName,
131138
path: path.dirname(pythonPath)
132139
}));
140+
condaService.setup(c => c.getCondaFile())
141+
.returns(() => Promise.resolve(condaPath));
133142
condaService.setup(c => c.getCondaVersion())
134-
.returns(() => Promise.resolve('4.4.0'));
135-
const expected = [`${conda} activate EnvA`];
143+
.returns(() => Promise.resolve(parse('4.4.0', true)!));
144+
const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`];
136145

137146
const provider = new CondaActivationCommandProvider(serviceContainer.object);
138147
const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash);

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as assert from 'assert';
33
import { expect } from 'chai';
44
import { EOL } from 'os';
55
import * as path from 'path';
6+
import { parse } from 'semver';
67
import * as TypeMoq from 'typemoq';
78
import { FileSystem } from '../../client/common/platform/fileSystem';
89
import { IFileSystem, IPlatformService } from '../../client/common/platform/types';
@@ -557,27 +558,30 @@ suite('Interpreters Conda Service', () => {
557558
});
558559

559560
test('isAvailable will return true if conda is available', async () => {
560-
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' }));
561+
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: '4.4.4' }));
561562
const isAvailable = await condaService.isCondaAvailable();
562563
assert.equal(isAvailable, true);
563564
});
564565

565566
test('isAvailable will return false if conda is not available', async () => {
567+
condaService.getCondaFile = () => Promise.resolve('conda');
566568
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('not found')));
567569
fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false));
568570
fileSystem.setup(fs => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([]));
569571
platformService.setup(p => p.isWindows).returns(() => false);
570-
572+
condaService.getCondaInfo = () => Promise.reject('Not Found');
571573
const isAvailable = await condaService.isCondaAvailable();
572574
assert.equal(isAvailable, false);
573575
});
574576

575577
test('Version info from conda process will be returned in getCondaVersion', async () => {
576-
const expectedVersion = new Date().toString();
577-
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: expectedVersion }));
578+
condaService.getCondaInfo = () => Promise.reject('Not Found');
579+
condaService.getCondaFile = () => Promise.resolve('conda');
580+
const expectedVersion = parse('4.4.4')!.raw;
581+
processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: '4.4.4' }));
578582

579583
const version = await condaService.getCondaVersion();
580-
assert.equal(version, expectedVersion);
584+
assert.equal(version!.raw, expectedVersion);
581585
});
582586

583587
test('isCondaInCurrentPath will return true if conda is available', async () => {

0 commit comments

Comments
 (0)