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

Update interpreter display #3

Merged
merged 5 commits into from
Nov 2, 2017
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
14 changes: 13 additions & 1 deletion src/client/interpreter/locators/services/conda.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { IS_WINDOWS } from "../../../common/utils";
import { IS_WINDOWS } from '../../../common/utils';

// where to find the Python binary within a conda env.
export const CONDA_RELATIVE_PY_PATH = IS_WINDOWS ? ['python.exe'] : ['bin', 'python'];
// tslint:disable-next-line:variable-name
export const AnacondaCompanyNames = ['Anaconda, Inc.', 'Continuum Analytics, Inc.'];
// tslint:disable-next-line:variable-name
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 type CondaInfo = {
envs?: string[];
'sys.version'?: string;
'python_version'?: string;
default_prefix?: string;
};
47 changes: 17 additions & 30 deletions src/client/interpreter/locators/services/condaEnvService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,35 @@ import * as path from 'path';
import { Uri } from 'vscode';
import { VersionUtils } from '../../../common/versionUtils';
import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts';
import { AnacondaCompanyName, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda';
import { AnacondaCompanyName, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda';
import { CondaHelper } from './condaHelper';

type CondaInfo = {
envs?: string[];
'sys.version'?: string;
default_prefix?: string;
};
export class CondaEnvService implements IInterpreterLocatorService {
private readonly condaHelper = new CondaHelper();
constructor(private registryLookupForConda?: IInterpreterLocatorService) {
}
public getInterpreters(resource?: Uri) {
public async getInterpreters(resource?: Uri) {
return this.getSuggestionsFromConda();
}
// tslint:disable-next-line:no-empty
public dispose() { }
public getCondaFile() {
public async getCondaFile() {
if (this.registryLookupForConda) {
return this.registryLookupForConda.getInterpreters()
.then(interpreters => interpreters.filter(this.isCondaEnvironment))
.then(condaInterpreters => this.getLatestVersion(condaInterpreters))
.then(condaInterpreter => {
return condaInterpreter ? path.join(path.dirname(condaInterpreter.path), 'conda.exe') : 'conda';
})
.then(condaPath => {
.then(async condaPath => {
return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda');
});
}
return Promise.resolve('conda');
}
public isCondaEnvironment(interpreter: PythonInterpreter) {
return (interpreter.displayName || '').toUpperCase().indexOf('ANACONDA') >= 0 ||
(interpreter.companyDisplayName || '').toUpperCase().indexOf('CONTINUUM') >= 0;
return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 ||
(interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0;
}
public getLatestVersion(interpreters: PythonInterpreter[]) {
const sortedInterpreters = interpreters.filter(interpreter => interpreter.version && interpreter.version.length > 0);
Expand All @@ -47,8 +44,7 @@ export class CondaEnvService implements IInterpreterLocatorService {
}
}
public async parseCondaInfo(info: CondaInfo) {
// "sys.version": "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]".
const displayName = this.getDisplayNameFromVersionInfo(info['sys.version'] || '');
const displayName = this.condaHelper.getDisplayName(info);

// The root of the conda environment is itself a Python interpreter
// envs reported as e.g.: /Users/bob/miniconda3/envs/someEnv.
Expand All @@ -69,47 +65,38 @@ export class CondaEnvService implements IInterpreterLocatorService {
};
return interpreter;
})
.map(env => fs.pathExists(env.path).then(exists => exists ? env : null));
.map(async env => fs.pathExists(env.path).then(exists => exists ? env : null));

return Promise.all(promises)
.then(interpreters => interpreters.filter(interpreter => interpreter !== null && interpreter !== undefined))
// tslint:disable-next-line:no-non-null-assertion
.then(interpreters => interpreters.map(interpreter => interpreter!));
}
private getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> {
return this.getCondaFile()
.then(condaFile => {
.then(async condaFile => {
return new Promise<PythonInterpreter[]>((resolve, reject) => {
// interrogate conda (if it's on the path) to find all environments.
child_process.execFile(condaFile, ['info', '--json'], (_, stdout) => {
if (stdout.length === 0) {
return resolve([]);
resolve([]);
return;
}

try {
const info = JSON.parse(stdout);
// tslint:disable-next-line:prefer-type-cast
const info = JSON.parse(stdout) as CondaInfo;
resolve(this.parseCondaInfo(info));
} catch (e) {
// Failed because either:
// 1. conda is not installed.
// 2. `conda info --json` has changed signature.
// 3. output of `conda info --json` has changed in structure.
// In all cases, we can't offer conda pythonPath suggestions.
return resolve([]);
resolve([]);
}
});
});
});
}
private getDisplayNameFromVersionInfo(versionInfo: string = '') {
if (!versionInfo) {
return AnacondaDisplayName;
}

const versionParts = versionInfo.split('|').map(item => item.trim());
if (versionParts.length > 1 && versionParts[1].indexOf('conda') >= 0) {
return versionParts[1];
}
return AnacondaDisplayName;
}
}
42 changes: 42 additions & 0 deletions src/client/interpreter/locators/services/condaHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { AnacondaDisplayName, AnacondaIdentfiers, CondaInfo } from './conda';

export class CondaHelper {
public getDisplayName(condaInfo: CondaInfo = {}): string {
const pythonVersion = this.getPythonVersion(condaInfo);

// Samples.
// "3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]".
// "3.6.2 |Anaconda, Inc.| (default, Sep 21 2017, 18:29:43) \n[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]".
const sysVersion = condaInfo['sys.version'];
if (!sysVersion) {
return pythonVersion ? `Python ${pythonVersion} : ${AnacondaDisplayName}` : AnacondaDisplayName;
}

// Take the first two parts of the sys.version.
const sysVersionParts = sysVersion.split('|').filter((_, index) => index < 2);
if (sysVersionParts.length > 0) {
if (pythonVersion && sysVersionParts[0].startsWith(pythonVersion)) {
sysVersionParts[0] = `Python ${sysVersionParts[0]}`;
} else {
// The first part is not the python version, hence remove this.
sysVersionParts.shift();
}
}

const displayName = sysVersionParts.map(item => item.trim()).join(' : ');
return this.isIdentifiableAsAnaconda(displayName) ? displayName : `${displayName} : ${AnacondaDisplayName}`;
}
private isIdentifiableAsAnaconda(value: string) {
const valueToSearch = value.toLowerCase();
return AnacondaIdentfiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1);
}
private getPythonVersion(condaInfo: CondaInfo): string | undefined {
// Sample.
// 3.6.2.final.0 (hence just take everything untill the third period).
const pythonVersion = condaInfo.python_version;
if (!pythonVersion) {
return undefined;
}
return pythonVersion.split('.').filter((_, index) => index < 3).join('.');
}
}
14 changes: 7 additions & 7 deletions src/test/interpreters/condaEnvService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Uri } from 'vscode';
import { PythonSettings } from '../../client/common/configSettings';
import { IS_WINDOWS } from '../../client/common/utils';
import { PythonInterpreter } from '../../client/interpreter/contracts';
import { AnacondaCompanyName } from '../../client/interpreter/locators/services/conda';
import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda';
import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService';
import { initialize, initializeTest } from '../initialize';
import { MockProvider } from './mocks';
Expand Down Expand Up @@ -55,33 +55,33 @@ suite('Interpreters from Conda Environments', () => {

const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python');
assert.equal(interpreters[0].path, path1, 'Incorrect path for first env');
assert.equal(interpreters[0].displayName, 'Anaconda (numpy)', 'Incorrect display name for first env');
assert.equal(interpreters[0].displayName, `Anaonda 4.4.0 (64-bit) : ${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env');
assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env');
});
test('Must use the default display name if sys.version is empty', async () => {
const condaProvider = new CondaEnvService();
const info = {
envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')],
envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')]
};
const interpreters = await condaProvider.parseCondaInfo(info);
assert.equal(interpreters.length, 1, 'Incorrect number of entries');

const path1 = path.join(info.envs[0], IS_WINDOWS ? 'python.exe' : 'bin/python');
assert.equal(interpreters[0].path, path1, 'Incorrect path for first env');
assert.equal(interpreters[0].displayName, 'Anaconda (numpy)', 'Incorrect display name for first env');
assert.equal(interpreters[0].displayName, `${AnacondaDisplayName} (numpy)`, 'Incorrect display name for first env');
assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env');
});
test('Must include the default_prefix into the list of interpreters', async () => {
const condaProvider = new CondaEnvService();
const info = {
default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy'),
default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy')
};
const interpreters = await condaProvider.parseCondaInfo(info);
assert.equal(interpreters.length, 1, 'Incorrect number of entries');

const path1 = path.join(info.default_prefix, IS_WINDOWS ? 'python.exe' : 'bin/python');
assert.equal(interpreters[0].path, path1, 'Incorrect path for first env');
assert.equal(interpreters[0].displayName, 'Anaconda', 'Incorrect display name for first env');
assert.equal(interpreters[0].displayName, AnacondaDisplayName, 'Incorrect display name for first env');
assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env');
});
test('Must exclude interpreters that do not exist on disc', async () => {
Expand Down Expand Up @@ -169,7 +169,7 @@ suite('Interpreters from Conda Environments', () => {
{ displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' },
{ displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0' },
{ displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' },
{ displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' },
{ displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' }
];
const mockRegistryProvider = new MockProvider(registryInterpreters);
const condaProvider = new CondaEnvService(mockRegistryProvider);
Expand Down
45 changes: 45 additions & 0 deletions src/test/interpreters/condaHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as assert from 'assert';
import { AnacondaDisplayName, CondaInfo } from '../../client/interpreter/locators/services/conda';
import { CondaHelper } from '../../client/interpreter/locators/services/condaHelper';
import { initialize, initializeTest } from '../initialize';

// tslint:disable-next-line:max-func-body-length
suite('Interpreters display name from Conda Environments', () => {
const condaHelper = new CondaHelper();
suiteSetup(initialize);
setup(initializeTest);
test('Must return default display name for invalid Conda Info', () => {
assert.equal(condaHelper.getDisplayName(), AnacondaDisplayName, 'Incorrect display name');
assert.equal(condaHelper.getDisplayName({}), AnacondaDisplayName, 'Incorrect display name');
});
test('Must return at least Python Version', () => {
const info: CondaInfo = {
python_version: '3.6.1.final.10'
};
const displayName = condaHelper.getDisplayName(info);
assert.equal(displayName, `Python 3.6.1 : ${AnacondaDisplayName}`, 'Incorrect display name');
});
test('Must return info without first part if not a python version', () => {
const info: CondaInfo = {
'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
};
const displayName = condaHelper.getDisplayName(info);
assert.equal(displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name');
});
test('Must return info prefixed with word \'Python\'', () => {
const info: CondaInfo = {
python_version: '3.6.1.final.10',
'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
};
const displayName = condaHelper.getDisplayName(info);
assert.equal(displayName, 'Python 3.6.1 : Anaconda 4.4.0 (64-bit)', 'Incorrect display name');
});
test('Must include Ananconda name if Company name not found', () => {
const info: CondaInfo = {
python_version: '3.6.1.final.10',
'sys.version': '3.6.1 |4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]'
};
const displayName = condaHelper.getDisplayName(info);
assert.equal(displayName, `Python 3.6.1 : 4.4.0 (64-bit) : ${AnacondaDisplayName}`, 'Incorrect display name');
});
});