diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bfcb80a350f7..fb0c28d572fd 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -65,6 +65,24 @@ "fileLocation": "relative" } ] + }, + { + "type": "gulp", + "task": "hygiene-watch", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + }, + "problemMatcher": [ + "$tsc", + { + "base": "$tslint5", + "fileLocation": "relative" + } + ] } ] } diff --git a/gulpfile.js b/gulpfile.js index f284ad9edc91..f23540ca044a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -12,6 +12,8 @@ const tsfmt = require('typescript-formatter'); const tslint = require('tslint'); const relative = require('relative'); const ts = require('gulp-typescript'); +const watch = require('gulp-watch'); +const cp = require('child_process'); /** * Hygiene works by creating cascading subsets of all our files and @@ -212,47 +214,51 @@ const hygiene = exports.hygiene = (some, options) => { gulp.task('hygiene', () => hygiene()); -// this allows us to run hygiene as a git pre-commit hook. -if (require.main === module) { - const cp = require('child_process'); +gulp.task('hygiene-watch', function () { + return watch(all, function () { + run(true, true); + }); +}); +function run(lintOnlyModifiedFiles, doNotExit) { + function exitProcessOnError(ex) { + console.error(); + console.error(ex); + if (!doNotExit) { + process.exit(1); + } + } process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - process.exit(1); + exitProcessOnError(); }); cp.exec('git config core.autocrlf', (err, out) => { const skipEOL = out.trim() === 'true'; - - if (process.argv.length > 2) { + if (!lintOnlyModifiedFiles && process.argv.length > 2) { return hygiene(process.argv.slice(2), { skipEOL: skipEOL - }).on('error', err => { - console.error(); - console.error(err); - process.exit(1); - }); + }).on('error', exitProcessOnError); } - cp.exec('git diff --cached --name-only', { + const cmd = lintOnlyModifiedFiles ? 'git diff --name-only' : 'git diff --cached --name-only'; + cp.exec(cmd, { maxBuffer: 2000 * 1024 }, (err, out) => { if (err) { - console.error(); - console.error(err); - process.exit(1); + exitProcessOnError(err); } - const some = out .split(/\r?\n/) .filter(l => !!l); + hygiene(some, { skipEOL: skipEOL - }).on('error', err => { - console.error(); - console.error(err); - process.exit(1); - }); + }).on('error', exitProcessOnError); }); }); } +// this allows us to run hygiene as a git pre-commit hook. +if (require.main === module) { + run(); +} diff --git a/package.json b/package.json index b483de2294ca..7602332878cd 100644 --- a/package.json +++ b/package.json @@ -1578,6 +1578,7 @@ "gulp": "^3.9.1", "gulp-filter": "^5.0.1", "gulp-typescript": "^3.2.2", + "gulp-watch": "^4.3.11", "husky": "^0.14.3", "ignore-loader": "^0.1.1", "mocha": "^2.3.3", @@ -1592,7 +1593,7 @@ "tslint-microsoft-contrib": "^5.0.1", "typescript": "^2.5.2", "typescript-formatter": "^6.0.0", - "webpack": "^1.13.2", - "vscode": "^1.1.5" + "vscode": "^1.1.5", + "webpack": "^1.13.2" } } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 24c11f906597..1bee73bb62b8 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -5,6 +5,10 @@ export interface IInterpreterLocatorService extends Disposable { getInterpreters(resource?: Uri): Promise; } +export interface ICondaLocatorService { + getCondaFile(): Promise; +} + export type PythonInterpreter = { path: string; companyDisplayName?: string; diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index f78df63a31f8..69103313a909 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -3,7 +3,7 @@ import { ConfigurationTarget, window, workspace } from 'vscode'; import { RegistryImplementation } from '../common/registry'; import { Is_64Bit, IS_WINDOWS } from '../common/utils'; import { WorkspacePythonPath } from './contracts'; -import { CondaEnvService } from './locators/services/condaEnvService'; +import { CondaLocatorService } from './locators/services/condaLocator'; import { WindowsRegistryService } from './locators/services/windowsRegistryService'; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { @@ -29,14 +29,10 @@ export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { return undefined; } export async function getCondaVersion() { - let condaService: CondaEnvService; - if (IS_WINDOWS) { - const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); - condaService = new CondaEnvService(windowsRegistryProvider); - } else { - condaService = new CondaEnvService(); - } - return condaService.getCondaFile() + const windowsRegistryProvider = IS_WINDOWS ? new WindowsRegistryService(new RegistryImplementation(), Is_64Bit) : undefined; + const condaLocator = new CondaLocatorService(IS_WINDOWS, windowsRegistryProvider); + + return condaLocator.getCondaFile() .then(async condaFile => { return new Promise((resolve, reject) => { child_process.execFile(condaFile, ['--version'], (_, stdout) => { diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index 198c37f25869..67f470e7e970 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -9,6 +9,7 @@ import { VirtualEnvironmentManager } from '../virtualEnvs'; import { fixInterpreterDisplayName, fixInterpreterPath } from './helpers'; import { CondaEnvFileService, getEnvironmentsFile as getCondaEnvFile } from './services/condaEnvFileService'; import { CondaEnvService } from './services/condaEnvService'; +import { CondaLocatorService } from './services/condaLocator'; import { CurrentPathService } from './services/currentPathService'; import { getKnownSearchPathsForInterpreters, KnownPathsService } from './services/KnownPathsService'; import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './services/virtualEnvService'; @@ -66,10 +67,12 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi // The order of the services is important. if (IS_WINDOWS) { const windowsRegistryProvider = new WindowsRegistryService(new RegistryImplementation(), Is_64Bit); + const condaLocator = new CondaLocatorService(IS_WINDOWS, windowsRegistryProvider); locators.push(windowsRegistryProvider); - locators.push(new CondaEnvService(windowsRegistryProvider)); + locators.push(new CondaEnvService(condaLocator)); } else { - locators.push(new CondaEnvService()); + const condaLocator = new CondaLocatorService(IS_WINDOWS); + locators.push(new CondaEnvService(condaLocator)); } // Supplements the above list of conda environments. locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index c6afce8abe0a..f6bab291c59e 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -4,33 +4,19 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { Uri } from 'vscode'; import { VersionUtils } from '../../../common/versionUtils'; -import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; import { AnacondaCompanyName, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; import { CondaHelper } from './condaHelper'; export class CondaEnvService implements IInterpreterLocatorService { private readonly condaHelper = new CondaHelper(); - constructor(private registryLookupForConda?: IInterpreterLocatorService) { + constructor(private condaLocator: ICondaLocatorService) { } public async getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); } // tslint:disable-next-line:no-empty public dispose() { } - 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(async condaPath => { - return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda'); - }); - } - return Promise.resolve('conda'); - } public isCondaEnvironment(interpreter: PythonInterpreter) { return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; @@ -73,7 +59,7 @@ export class CondaEnvService implements IInterpreterLocatorService { .then(interpreters => interpreters.map(interpreter => interpreter!)); } private async getSuggestionsFromConda(): Promise { - return this.getCondaFile() + return this.condaLocator.getCondaFile() .then(async condaFile => { return new Promise((resolve, reject) => { // interrogate conda (if it's on the path) to find all environments. diff --git a/src/client/interpreter/locators/services/condaLocator.ts b/src/client/interpreter/locators/services/condaLocator.ts new file mode 100644 index 000000000000..ff1db7269c50 --- /dev/null +++ b/src/client/interpreter/locators/services/condaLocator.ts @@ -0,0 +1,69 @@ +'use strict'; +import * as child_process from 'child_process'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { IS_WINDOWS } from '../../../common/utils'; +import { VersionUtils } from '../../../common/versionUtils'; +import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +const KNOWN_CONDA_LOCATIONS = ['~/anaconda/bin/conda', '~/miniconda/bin/conda', + '~/anaconda2/bin/conda', '~/miniconda2/bin/conda', + '~/anaconda3/bin/conda', '~/miniconda3/bin/conda']; + +export class CondaLocatorService implements ICondaLocatorService { + constructor(private isWindows: boolean, private registryLookupForConda?: IInterpreterLocatorService) { + } + // tslint:disable-next-line:no-empty + public dispose() { } + public async getCondaFile(): Promise { + const isAvailable = await this.isCondaInCurrentPath(); + if (isAvailable) { + return 'conda'; + } + if (this.isWindows && 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(async condaPath => { + return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda'); + }); + } + return this.getCondaFileFromKnownLocations(); + } + public isCondaEnvironment(interpreter: PythonInterpreter) { + 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); + // tslint:disable-next-line:no-non-null-assertion + sortedInterpreters.sort((a, b) => VersionUtils.compareVersion(a.version!, b.version!)); + if (sortedInterpreters.length > 0) { + return sortedInterpreters[sortedInterpreters.length - 1]; + } + } + public async isCondaInCurrentPath() { + return new Promise((resolve, reject) => { + child_process.execFile('conda', ['--version'], (_, stdout) => { + if (stdout && stdout.length > 0) { + resolve(true); + } else { + resolve(false); + } + }); + }); + } + private async getCondaFileFromKnownLocations(): Promise { + const condaFiles = await Promise.all(KNOWN_CONDA_LOCATIONS + .map(untildify) + .map(async (condaPath: string) => fs.pathExists(condaPath).then(exists => exists ? condaPath : ''))); + + const validCondaFiles = condaFiles.filter(condaPath => condaPath.length > 0); + return validCondaFiles.length === 0 ? 'conda' : validCondaFiles[0]; + } +} diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index 8ba4fcf5d43c..ae3dfba4c4ab 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -1,13 +1,13 @@ import * as assert from 'assert'; import * as path from 'path'; import { Uri } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { IS_WINDOWS } from '../../client/common/utils'; +import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; import { PythonInterpreter } from '../../client/interpreter/contracts'; import { AnacondaCompanyName, AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; +import { CondaLocatorService } from '../../client/interpreter/locators/services/condaLocator'; import { initialize, initializeTest } from '../initialize'; -import { MockProvider } from './mocks'; +import { MockCondaLocatorService, MockProvider } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); @@ -17,13 +17,13 @@ suite('Interpreters from Conda Environments', () => { suiteSetup(initialize); setup(initializeTest); test('Must return an empty list for empty json', async () => { - const condaProvider = new CondaEnvService(); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); // tslint:disable-next-line:no-any prefer-type-cast const interpreters = await condaProvider.parseCondaInfo({} as any); assert.equal(interpreters.length, 0, 'Incorrect number of entries'); }); test('Must extract display name from version info', async () => { - const condaProvider = new CondaEnvService(); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], @@ -44,7 +44,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[1].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must use the default display name if sys.version is invalid', async () => { - const condaProvider = new CondaEnvService(); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], default_prefix: '', @@ -59,7 +59,7 @@ suite('Interpreters from Conda Environments', () => { 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 condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] }; @@ -72,7 +72,7 @@ suite('Interpreters from Conda Environments', () => { 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 condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); const info = { default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy') }; @@ -85,7 +85,7 @@ suite('Interpreters from Conda Environments', () => { assert.equal(interpreters[0].companyDisplayName, AnacondaCompanyName, 'Incorrect company display name for first env'); }); test('Must exclude interpreters that do not exist on disc', async () => { - const condaProvider = new CondaEnvService(); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'path0', 'one.exe'), @@ -117,7 +117,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(mockRegistryProvider); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); @@ -140,7 +140,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(mockRegistryProvider); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); @@ -158,7 +158,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(mockRegistryProvider); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); @@ -172,7 +172,7 @@ suite('Interpreters from Conda Environments', () => { { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(mockRegistryProvider); + const condaProvider = new MockCondaLocatorService(true, mockRegistryProvider, false); const condaExe = await condaProvider.getCondaFile(); assert.equal(condaExe, path.join(path.dirname(condaPythonExePath), 'conda.exe'), 'Failed to identify conda.exe'); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index db4cfed0c474..be61eb9c52c9 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,12 +1,13 @@ import { Architecture, Hive, IRegistry } from '../../client/common/registry'; import { IInterpreterLocatorService, PythonInterpreter } from '../../client/interpreter/contracts'; import { IInterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { CondaLocatorService } from '../../client/interpreter/locators/services/condaLocator'; import { IVirtualEnvironment } from '../../client/interpreter/virtualEnvs/contracts'; export class MockProvider implements IInterpreterLocatorService { constructor(private suggestions: PythonInterpreter[]) { } - public getInterpreters(): Promise { + public async getInterpreters(): Promise { return Promise.resolve(this.suggestions); } // tslint:disable-next-line:no-empty @@ -17,9 +18,9 @@ export class MockRegistry implements IRegistry { constructor(private keys: { key: string, hive: Hive, arch?: Architecture, values: string[] }[], private values: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[]) { } - public getKeys(key: string, hive: Hive, arch?: Architecture): Promise { + public async getKeys(key: string, hive: Hive, arch?: Architecture): Promise { const items = this.keys.find(item => { - if (item.arch) { + if (typeof item.arch === 'number') { return item.key === key && item.hive === hive && item.arch === arch; } return item.key === key && item.hive === hive; @@ -27,12 +28,12 @@ export class MockRegistry implements IRegistry { return items ? Promise.resolve(items.values) : Promise.resolve([]); } - public getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { + public async getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { const items = this.values.find(item => { if (item.key !== key || item.hive !== hive) { return false; } - if (item.arch && item.arch !== arch) { + if (typeof item.arch === 'number' && item.arch !== arch) { return false; } if (name && item.name !== name) { @@ -48,7 +49,7 @@ export class MockRegistry implements IRegistry { export class MockVirtualEnv implements IVirtualEnvironment { constructor(private isDetected: boolean, public name: string) { } - public detect(pythonPath: string): Promise { + public async detect(pythonPath: string): Promise { return Promise.resolve(this.isDetected); } } @@ -57,12 +58,26 @@ export class MockVirtualEnv implements IVirtualEnvironment { export class MockInterpreterVersionProvider implements IInterpreterVersionService { constructor(private displayName: string, private useDefaultDisplayName: boolean = false, private pipVersionPromise?: Promise) { } - public getVersion(pythonPath: string, defaultDisplayName: string): Promise { + public async getVersion(pythonPath: string, defaultDisplayName: string): Promise { return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); } - public getPipVersion(pythonPath: string): Promise { - return this.pipVersionPromise; + public async getPipVersion(pythonPath: string): Promise { + // tslint:disable-next-line:no-non-null-assertion + return this.pipVersionPromise!; } // tslint:disable-next-line:no-empty public dispose() { } } + +// tslint:disable-next-line:max-classes-per-file +export class MockCondaLocatorService extends CondaLocatorService { + constructor(isWindows: boolean, registryLookupForConda?: IInterpreterLocatorService, private isCondaInEnv?: boolean) { + super(isWindows, registryLookupForConda); + } + public async isCondaInCurrentPath() { + if (typeof this.isCondaInEnv === 'boolean') { + return this.isCondaInEnv; + } + return super.isCondaInCurrentPath(); + } +} diff --git a/tslint.json b/tslint.json index 58b4037bd6a5..51c6b1a37c01 100644 --- a/tslint.json +++ b/tslint.json @@ -43,13 +43,13 @@ "PromiseLike" ], "completed-docs": false, + "no-unsafe-any": false, + "no-backbone-get-set-outside-model": false, + "underscore-consistent-invocation": false, "no-void-expression": false, "no-non-null-assertion": false, - "no-unsafe-any": false, "prefer-type-cast": false, "function-name": false, - "variable-name": false, - "no-backbone-get-set-outside-model": false, - "underscore-consistent-invocation": false + "variable-name": false } }