diff --git a/src/client/interpreter/locators/services/conda.ts b/src/client/interpreter/locators/services/conda.ts index 6eabc7151175..808a3d6ea049 100644 --- a/src/client/interpreter/locators/services/conda.ts +++ b/src/client/interpreter/locators/services/conda.ts @@ -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; +}; diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index 0b7b281efff1..c6afce8abe0a 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -5,22 +5,19 @@ 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)) @@ -28,15 +25,15 @@ export class CondaEnvService implements IInterpreterLocatorService { .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); @@ -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. @@ -69,25 +65,27 @@ 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 { + private async getSuggestionsFromConda(): Promise { return this.getCondaFile() - .then(condaFile => { + .then(async condaFile => { return new Promise((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: @@ -95,21 +93,10 @@ export class CondaEnvService implements IInterpreterLocatorService { // 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; - } } diff --git a/src/client/interpreter/locators/services/condaHelper.ts b/src/client/interpreter/locators/services/condaHelper.ts new file mode 100644 index 000000000000..ea8276c6392f --- /dev/null +++ b/src/client/interpreter/locators/services/condaHelper.ts @@ -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('.'); + } +} diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 59f9d44c78c6..8ba4fcf5d43c 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -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'; @@ -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 () => { @@ -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); diff --git a/src/test/interpreters/condaHelper.test.ts b/src/test/interpreters/condaHelper.test.ts new file mode 100644 index 000000000000..45d89c3410df --- /dev/null +++ b/src/test/interpreters/condaHelper.test.ts @@ -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'); + }); +});