From b0fb5c0aaea5c0105de1dde2a6189b233878b908 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 11 Dec 2017 13:06:51 -0800 Subject: [PATCH] Ability to install modules using pip or conda (#380) * Add support for pip and/or conda when installing modules * Fix display versions for environments (fixes #378) * Refactor installer to use the pip/conda module installer for installation of modules * Performance improvement of detecting virtual env and setting it (fixes #372) * Code refactor to ensure interpreter locators use the new DI framework * Fixes #266 --- .vscode/tasks.json | 24 - gulpfile.js | 422 +++++++++--------- package.json | 4 +- requirements.txt | 1 + src/client/common/configSettings.ts | 2 +- src/client/common/errors/errorUtils.ts | 9 + .../common/errors/moduleNotInstalledError.ts | 8 + src/client/common/helpers.ts | 17 +- src/client/common/installer.ts | 383 ---------------- src/client/common/installer/condaInstaller.ts | 105 +++++ src/client/common/installer/installer.ts | 314 +++++++++++++ .../common/installer/moduleInstaller.ts | 28 ++ src/client/common/installer/pipInstaller.ts | 47 ++ src/client/common/installer/types.ts | 12 + src/client/common/platform/constants.ts | 3 + src/client/common/platform/registry.ts | 29 +- src/client/common/platform/types.ts | 18 + src/client/common/process/proc.ts | 2 +- src/client/common/process/pythonProcess.ts | 14 +- src/client/common/process/types.ts | 2 +- src/client/common/serviceRegistry.ts | 26 +- src/client/common/terminal/service.ts | 67 +++ src/client/common/terminal/types.ts | 8 + src/client/common/types.ts | 17 +- .../variables/environmentVariablesProvider.ts | 4 +- src/client/extension.ts | 59 +-- src/client/formatters/autoPep8Formatter.ts | 9 +- src/client/formatters/baseFormatter.ts | 32 +- src/client/formatters/dummyFormatter.ts | 9 +- src/client/formatters/helper.ts | 28 ++ src/client/formatters/serviceRegistry.ts | 11 + src/client/formatters/types.ts | 19 + src/client/formatters/yapfFormatter.ts | 11 +- .../configuration/pythonPathUpdaterService.ts | 2 +- .../configuration/setInterpreterProvider.ts | 12 +- src/client/interpreter/contracts.ts | 35 +- src/client/interpreter/display/index.ts | 20 +- src/client/interpreter/helpers.ts | 22 - src/client/interpreter/index.ts | 29 +- src/client/interpreter/interpreterVersion.ts | 9 +- src/client/interpreter/locators/index.ts | 78 ++-- .../locators/services/KnownPathsService.ts | 15 +- .../locators/services/condaEnvFileService.ts | 20 +- .../locators/services/condaEnvService.ts | 50 ++- .../locators/services/condaHelper.ts | 23 +- .../locators/services/condaLocator.ts | 33 +- .../locators/services/currentPathService.ts | 16 +- .../locators/services/virtualEnvService.ts | 19 +- .../services/windowsRegistryService.ts | 30 +- src/client/interpreter/serviceRegistry.ts | 60 +++ .../interpreter/virtualEnvs/contracts.ts | 4 - src/client/interpreter/virtualEnvs/index.ts | 10 +- src/client/interpreter/virtualEnvs/types.ts | 15 + src/client/interpreter/virtualEnvs/venv.ts | 14 +- .../interpreter/virtualEnvs/virtualEnv.ts | 14 +- src/client/linters/baseLinter.ts | 25 +- .../linters/errorHandlers/baseErrorHandler.ts | 3 +- src/client/linters/errorHandlers/main.ts | 9 +- .../linters/errorHandlers/notInstalled.ts | 5 +- src/client/linters/errorHandlers/standard.ts | 3 +- src/client/linters/flake8.ts | 3 +- src/client/linters/helper.ts | 6 +- src/client/linters/mypy.ts | 3 +- src/client/linters/pep8Linter.ts | 3 +- src/client/linters/prospector.ts | 4 +- src/client/linters/pydocstyle.ts | 3 +- src/client/linters/pylama.ts | 3 +- src/client/linters/pylint.ts | 3 +- src/client/linters/types.ts | 3 +- src/client/providers/formatProvider.ts | 17 +- src/client/providers/renameProvider.ts | 24 +- .../providers/simpleRefactorProvider.ts | 34 +- src/client/telemetry/index.ts | 3 + .../common/managers/baseTestManager.ts | 5 +- src/client/unittests/common/runner.ts | 37 +- .../common/services/configSettingService.ts | 8 +- .../common/services/storageService.ts | 4 +- .../common/services/testManagerService.ts | 11 +- .../services/workspaceTestManagerService.ts | 4 +- src/client/unittests/common/testUtils.ts | 43 +- src/client/unittests/common/types.ts | 11 +- src/client/unittests/nosetest/main.ts | 6 +- src/client/unittests/pytest/main.ts | 6 +- .../pytest/services/parserService.ts | 2 +- src/client/unittests/unittest/main.ts | 10 +- src/client/workspaceSymbols/main.ts | 18 +- src/test/common/common.test.ts | 24 +- src/test/common/installer.multiroot.test.ts | 23 +- src/test/common/installer.test.ts | 116 +++-- src/test/common/moduleInstaller.test.ts | 161 +++++++ src/test/common/process/proc.exec.test.ts | 1 - .../common/process/proc.observable.test.ts | 21 +- .../pythonProc.simple.multiroot.test.ts | 4 +- .../envVarsProvider.multiroot.test.ts | 4 +- src/test/format/extension.format.test.ts | 4 +- src/test/interpreters/condaEnvService.test.ts | 87 ++-- src/test/interpreters/condaHelper.test.ts | 8 +- src/test/interpreters/display.test.ts | 19 +- src/test/interpreters/mocks.ts | 24 +- .../windowsRegistryService.test.ts | 170 +++---- src/test/linters/lint.test.ts | 2 +- src/test/mocks/condaLocator.ts | 14 + src/test/mocks/moduleInstaller.ts | 16 + src/test/mocks/terminalService.ts | 18 + src/test/serviceRegistry.ts | 8 +- src/test/unittests/mocks.ts | 3 +- .../unittests/stoppingDiscoverAndTest.test.ts | 2 +- 107 files changed, 2050 insertions(+), 1272 deletions(-) create mode 100644 src/client/common/errors/errorUtils.ts create mode 100644 src/client/common/errors/moduleNotInstalledError.ts delete mode 100644 src/client/common/installer.ts create mode 100644 src/client/common/installer/condaInstaller.ts create mode 100644 src/client/common/installer/installer.ts create mode 100644 src/client/common/installer/moduleInstaller.ts create mode 100644 src/client/common/installer/pipInstaller.ts create mode 100644 src/client/common/installer/types.ts create mode 100644 src/client/common/platform/types.ts create mode 100644 src/client/common/terminal/service.ts create mode 100644 src/client/common/terminal/types.ts create mode 100644 src/client/formatters/helper.ts create mode 100644 src/client/formatters/serviceRegistry.ts create mode 100644 src/client/formatters/types.ts create mode 100644 src/client/interpreter/serviceRegistry.ts delete mode 100644 src/client/interpreter/virtualEnvs/contracts.ts create mode 100644 src/client/interpreter/virtualEnvs/types.ts create mode 100644 src/test/common/moduleInstaller.test.ts create mode 100644 src/test/mocks/condaLocator.ts create mode 100644 src/test/mocks/moduleInstaller.ts create mode 100644 src/test/mocks/terminalService.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e9a646b57d1c..a5a125a4339e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -66,30 +66,6 @@ "fileLocation": "relative" } ] - }, - { - "label": "Hygiene (staged)", - "type": "gulp", - "task": "hygiene-staged", - "problemMatcher": [ - "$tsc", - { - "base": "$tslint5", - "fileLocation": "relative" - } - ] - }, - { - "label": "Hygiene (all)", - "type": "gulp", - "task": "hygiene", - "problemMatcher": [ - "$tsc", - { - "base": "$tslint5", - "fileLocation": "relative" - } - ] } ] } diff --git a/gulpfile.js b/gulpfile.js index 61c47b6ae155..683a54d66a25 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -28,27 +28,27 @@ const debounce = require('debounce'); */ const all = [ - 'src/**/*', - 'src/client/**/*', + 'src/**/*', + 'src/client/**/*', ]; const indentationFilter = [ - 'src/**/*.ts', - '!**/typings/**/*', + 'src/**/*.ts', + '!**/typings/**/*', ]; const tslintFilter = [ - 'src/**/*.ts', - 'test/**/*.ts', - '!**/node_modules/**', - '!out/**/*', - '!images/**/*', - '!.vscode/**/*', - '!pythonFiles/**/*', - '!resources/**/*', - '!snippets/**/*', - '!syntaxes/**/*', - '!**/typings/**/*', + 'src/**/*.ts', + 'test/**/*.ts', + '!**/node_modules/**', + '!out/**/*', + '!images/**/*', + '!.vscode/**/*', + '!pythonFiles/**/*', + '!resources/**/*', + '!snippets/**/*', + '!syntaxes/**/*', + '!**/typings/**/*', ]; gulp.task('hygiene', () => run({ mode: 'all', skipFormatCheck: true, skipIndentationCheck: true })); @@ -61,17 +61,6 @@ gulp.task('hygiene-watch', () => gulp.watch(all, debounce(() => run({ mode: 'cha gulp.task('hygiene-modified', ['compile'], () => run({ mode: 'changes' })); -function reportFailures(failures) { - failures.forEach(failure => { - const name = failure.name || failure.fileName; - const position = failure.startPosition; - const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; - const character = position.lineAndCharacter ? position.lineAndCharacter.character : position.character; - - // Output in format similar to tslint for the linter to pickup. - console.error(`ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${character + 1}]: ${failure.failure}`); - }); -} /** * @typedef {Object} hygieneOptions - creates a new type named 'SpecialType' @@ -87,151 +76,178 @@ function reportFailures(failures) { * @returns {NodeJS.ReadWriteStream} */ const hygiene = (options) => { - options = options || {}; - let errorCount = 0; - - const indentation = es.through(function (file) { - file.contents - .toString('utf8') - .split(/\r\n|\r|\n/) - .forEach((line, i) => { - if (/^\s*$/.test(line) || /^\S+.*$/.test(line)) { - // Empty or whitespace lines are OK. - } else if (/^(\s\s\s\s)+.*/.test(line)) { - // Good indent. - } else if (/^[\t]+.*/.test(line)) { - console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation (use 4 spaces instead of tabs or other)'); - errorCount++; - } - }); - - this.emit('data', file); - }); - - const formatOptions = { verify: true, tsconfig: true, tslint: true, editorconfig: true, tsfmt: true }; - const formatting = es.map(function (file, cb) { - tsfmt.processString(file.path, file.contents.toString('utf8'), formatOptions) - .then(result => { - if (result.error) { - let message = result.message.trim(); - let formattedMessage = ''; - if (message.startsWith(__dirname)) { - message = message.substr(__dirname.length); - message = message.startsWith(path.sep) ? message.substr(1) : message; - const index = message.indexOf('.ts '); - if (index === -1) { - formattedMessage = colors.red(message); - } else { - const file = message.substr(0, index + 3); - const errorMessage = message.substr(index + 4).trim(); - formattedMessage = `${colors.red(file)} ${errorMessage}`; - } - } else { - formattedMessage = colors.red(message); - } - console.error(formattedMessage); - errorCount++; - } - cb(null, file); - }) - .catch(cb); - }); - - const configuration = tslint.Configuration.findConfiguration(null, '.'); - const program = tslint.Linter.createProgram('./tsconfig.json'); - const linter = new tslint.Linter({ formatter: 'json' }, program); - const tsl = es.through(function (file) { - const contents = file.contents.toString('utf8'); - // Don't print anything to the console, we'll do that. - // Yes this is a hack, but tslinter doesn't provide an option to prevent this. - const oldWarn = console.warn; - console.warn = () => { }; - linter.lint(file.relative, contents, configuration.results); - console.warn = oldWarn; - const result = linter.getResult(); - if (result.failureCount > 0 || result.errorCount > 0) { - reportFailures(result.failures); - if (result.failureCount) { - errorCount += result.failureCount; - } - if (result.errorCount) { - errorCount += result.errorCount; - } - } - this.emit('data', file); - }); - - const tsFiles = []; - const tscFilesTracker = es.through(function (file) { - tsFiles.push(file.path.replace(/\\/g, '/')); - tsFiles.push(file.path); - this.emit('data', file); - }); - - const tsOptions = options.mode === 'compile' ? undefined : { strict: true, noImplicitAny: false, noImplicitThis: false }; - const tsProject = ts.createProject('tsconfig.json', tsOptions); - - const tsc = function () { - function customReporter() { - return { - error: function (error) { - const fullFilename = error.fullFilename || ''; - const relativeFilename = error.relativeFilename || ''; - if (tsFiles.findIndex(file => fullFilename === file || relativeFilename === file) === -1) { - return; - } - errorCount += 1; - console.error(error.message); - }, - finish: function () { - // forget the summary. - } - }; - } - const reporter = customReporter(); - return tsProject(reporter); - } - - const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(options); - const dest = options.mode === 'compile' ? './out' : '.'; - let result = files - .pipe(filter(f => !f.stat.isDirectory())); - - if (!options.skipIndentationCheck) { - result = result.pipe(filter(indentationFilter)) - .pipe(indentation); - } - - result = result - .pipe(filter(tslintFilter)); - - if (!options.skipFormatCheck) { - result = result - .pipe(formatting); - } - - if (!options.skipLinter) { - result = result - .pipe(tsl); - } - - result = result - .pipe(tscFilesTracker) - .pipe(tsc()) - .js.pipe(gulp.dest(dest)) - .pipe(es.through(null, function () { - if (errorCount > 0) { - const errorMessage = `Hygiene failed with ${colors.yellow(errorCount)} errors 👎 . Check 'gulpfile.js'.`; - console.error(colors.red(errorMessage)); - exitHandler(options); - } else { - console.log(colors.green('Hygiene passed with 0 errors 👍.')); - } - // Reset error counter. - errorCount = 0; - this.emit('end'); - })) - .on('error', exitHandler.bind(this, options)); + options = options || {}; + let errorCount = 0; + + const indentation = es.through(function (file) { + file.contents + .toString('utf8') + .split(/\r\n|\r|\n/) + .forEach((line, i) => { + if (/^\s*$/.test(line) || /^\S+.*$/.test(line)) { + // Empty or whitespace lines are OK. + } else if (/^(\s\s\s\s)+.*/.test(line)) { + // Good indent. + } else if (/^[\t]+.*/.test(line)) { + console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation (use 4 spaces instead of tabs or other)'); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const formatOptions = { verify: true, tsconfig: true, tslint: true, editorconfig: true, tsfmt: true }; + const formatting = es.map(function (file, cb) { + tsfmt.processString(file.path, file.contents.toString('utf8'), formatOptions) + .then(result => { + if (result.error) { + let message = result.message.trim(); + let formattedMessage = ''; + if (message.startsWith(__dirname)) { + message = message.substr(__dirname.length); + message = message.startsWith(path.sep) ? message.substr(1) : message; + const index = message.indexOf('.ts '); + if (index === -1) { + formattedMessage = colors.red(message); + } else { + const file = message.substr(0, index + 3); + const errorMessage = message.substr(index + 4).trim(); + formattedMessage = `${colors.red(file)} ${errorMessage}`; + } + } else { + formattedMessage = colors.red(message); + } + console.error(formattedMessage); + errorCount++; + } + cb(null, file); + }) + .catch(cb); + }); + + let reportedLinterFailures = []; + /** + * Report the linter failures + * @param {any[]} failures + */ + function reportLinterFailures(failures) { + failures + .map(failure => { + const name = failure.name || failure.fileName; + const position = failure.startPosition; + const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; + const character = position.lineAndCharacter ? position.lineAndCharacter.character : position.character; + + // Output in format similar to tslint for the linter to pickup. + const message = `ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${character + 1}]: ${failure.failure}`; + if (reportedLinterFailures.indexOf(message) === -1) { + console.error(message); + reportedLinterFailures.push(message); + return true; + } else { + return false; + } + }) + .filter(reported => reported === true) + .length > 0; + } + const configuration = tslint.Configuration.findConfiguration(null, '.'); + const program = tslint.Linter.createProgram('./tsconfig.json'); + const linter = new tslint.Linter({ formatter: 'json' }, program); + const tsl = es.through(function (file) { + const contents = file.contents.toString('utf8'); + // Don't print anything to the console, we'll do that. + // Yes this is a hack, but tslinter doesn't provide an option to prevent this. + const oldWarn = console.warn; + console.warn = () => { }; + linter.lint(file.relative, contents, configuration.results); + console.warn = oldWarn; + const result = linter.getResult(); + if (result.failureCount > 0 || result.errorCount > 0) { + const reported = reportLinterFailures(result.failures); + if (result.failureCount && reported) { + errorCount += result.failureCount; + } + if (result.errorCount && reported) { + errorCount += result.errorCount; + } + } + this.emit('data', file); + }); + + const tsFiles = []; + const tscFilesTracker = es.through(function (file) { + tsFiles.push(file.path.replace(/\\/g, '/')); + tsFiles.push(file.path); + this.emit('data', file); + }); + + const tsOptions = options.mode === 'compile' ? undefined : { strict: true, noImplicitAny: false, noImplicitThis: false }; + const tsProject = ts.createProject('tsconfig.json', tsOptions); + + const tsc = function () { + function customReporter() { + return { + error: function (error) { + const fullFilename = error.fullFilename || ''; + const relativeFilename = error.relativeFilename || ''; + if (tsFiles.findIndex(file => fullFilename === file || relativeFilename === file) === -1) { + return; + } + errorCount += 1; + console.error(error.message); + }, + finish: function () { + // forget the summary. + } + }; + } + const reporter = customReporter(); + return tsProject(reporter); + } + + const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(options); + const dest = options.mode === 'compile' ? './out' : '.'; + let result = files + .pipe(filter(f => !f.stat.isDirectory())); + + if (!options.skipIndentationCheck) { + result = result.pipe(filter(indentationFilter)) + .pipe(indentation); + } + + result = result + .pipe(filter(tslintFilter)); + + if (!options.skipFormatCheck) { + // result = result + // .pipe(formatting); + } + + if (!options.skipLinter) { + result = result + .pipe(tsl); + } + + result = result + .pipe(tscFilesTracker) + .pipe(tsc()) + .js.pipe(gulp.dest(dest)) + .pipe(es.through(null, function () { + if (errorCount > 0) { + const errorMessage = `Hygiene failed with ${colors.yellow(errorCount)} errors 👎 . Check 'gulpfile.js'.`; + console.error(colors.red(errorMessage)); + exitHandler(options); + } else { + console.log(colors.green('Hygiene passed with 0 errors 👍.')); + } + // Reset error counter. + errorCount = 0; + reportedLinterFailures = []; + this.emit('end'); + })) + .on('error', exitHandler.bind(this, options)); }; /** @@ -251,15 +267,15 @@ const hygiene = (options) => { * @param {Error} ex */ function exitHandler(options, ex) { - console.error(); - if (ex) { - console.error(ex); - console.error(colors.red(ex)); - } - if (options.exitOnError) { - console.log('exit'); - process.exit(1); - } + console.error(); + if (ex) { + console.error(ex); + console.error(colors.red(ex)); + } + if (options.exitOnError) { + console.log('exit'); + process.exit(1); + } } /** @@ -267,45 +283,45 @@ function exitHandler(options, ex) { * @param {runOptions} options */ function run(options) { - options = options ? options : {}; - process.once('unhandledRejection', (reason, p) => { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - exitHandler(options); - }); + options = options ? options : {}; + process.once('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + exitHandler(options); + }); - return hygiene(options); + return hygiene(options); } function getStagedFilesSync() { - const out = cp.execSync('git diff --cached --name-only', { encoding: 'utf8' }); - const some = out - .split(/\r?\n/) - .filter(l => !!l); - return some; + const out = cp.execSync('git diff --cached --name-only', { encoding: 'utf8' }); + const some = out + .split(/\r?\n/) + .filter(l => !!l); + return some; } /** * @param {hygieneOptions} options */ function getFilesToProcess(options) { - const mode = options ? options.mode : 'all'; - const gulpSrcOptions = { base: '.' }; + const mode = options ? options.mode : 'all'; + const gulpSrcOptions = { base: '.' }; - // If we need only modified files, then filter the glob. - if (options && options.mode === 'changes') { - return gulp.src(all, gulpSrcOptions) - .pipe(gitmodified('M', 'A', 'D', 'R', 'C', 'U', '??')); - } + // If we need only modified files, then filter the glob. + if (options && options.mode === 'changes') { + return gulp.src(all, gulpSrcOptions) + .pipe(gitmodified('M', 'A', 'D', 'R', 'C', 'U', '??')); + } - if (options && options.mode === 'staged') { - return gulp.src(getStagedFilesSync(), gulpSrcOptions); - } + if (options && options.mode === 'staged') { + return gulp.src(getStagedFilesSync(), gulpSrcOptions); + } - return gulp.src(all, gulpSrcOptions); + return gulp.src(all, gulpSrcOptions); } exports.hygiene = hygiene; // this allows us to run hygiene as a git pre-commit hook. if (require.main === module) { - run({ exitOnError: true, mode: 'staged' }); + run({ exitOnError: true, mode: 'staged' }); } diff --git a/package.json b/package.json index 96291ee6fb21..9fa8ea10dfa5 100644 --- a/package.json +++ b/package.json @@ -1347,8 +1347,8 @@ }, "python.unitTest.pyTestPath": { "type": "string", - "default": "py.test", - "description": "Path to pytest (py.test), you can use a custom version of pytest by modifying this setting to include the full path.", + "default": "pytest", + "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", "scope": "resource" }, "python.unitTest.nosetestArgs": { diff --git a/requirements.txt b/requirements.txt index edb5e9a94873..0b17d7594a72 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ nose pytest fabric numba +rope diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 92d1d294ce12..33347d2c1774 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -335,7 +335,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { nosetestArgs: [], pyTestArgs: [], unittestArgs: [], promptToConfigure: true, debugPort: 3000, nosetestsEnabled: false, pyTestEnabled: false, unittestEnabled: false, - nosetestPath: 'nosetests', pyTestPath: 'py.test' + nosetestPath: 'nosetests', pyTestPath: 'pytest' } as IUnitTestSettings; } } diff --git a/src/client/common/errors/errorUtils.ts b/src/client/common/errors/errorUtils.ts new file mode 100644 index 000000000000..e039ed870c83 --- /dev/null +++ b/src/client/common/errors/errorUtils.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable-next-line:no-stateless-class +export class ErrorUtils { + public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { + return content && (content!.indexOf(`No module named ${moduleName}`) > 0 || content!.indexOf(`No module named '${moduleName}'`) > 0); + } +} diff --git a/src/client/common/errors/moduleNotInstalledError.ts b/src/client/common/errors/moduleNotInstalledError.ts new file mode 100644 index 000000000000..60ec06781e76 --- /dev/null +++ b/src/client/common/errors/moduleNotInstalledError.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class ModuleNotInstalledError extends Error { + constructor(moduleName: string) { + super(`Module '${moduleName} not installed.`); + } +} diff --git a/src/client/common/helpers.ts b/src/client/common/helpers.ts index 1f7b28a34b83..fb8163a4087c 100644 --- a/src/client/common/helpers.ts +++ b/src/client/common/helpers.ts @@ -1,3 +1,5 @@ +import { ModuleNotInstalledError } from './errors/moduleNotInstalledError'; +// tslint:disable-next-line:no-require-imports no-var-requires const tmp = require('tmp'); export function isNotInstalledError(error: Error): boolean { @@ -7,10 +9,15 @@ export function isNotInstalledError(error: Error): boolean { if (!isError) { return false; } + if (error instanceof ModuleNotInstalledError) { + return true; + } + const isModuleNoInstalledError = errorObj.code === 1 && error.message.indexOf('No module named') >= 0; return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; } +// tslint:disable-next-line:interface-name export interface Deferred { readonly promise: Promise; readonly resolved: boolean; @@ -36,12 +43,12 @@ class DeferredImpl implements Deferred { this._reject = rej; }); } - resolve(value?: T | PromiseLike) { + public resolve(value?: T | PromiseLike) { this._resolve.apply(this.scope ? this.scope : this, arguments); this._resolved = true; } // tslint:disable-next-line:no-any - reject(reason?: any) { + public reject(reason?: any) { this._reject.apply(this.scope ? this.scope : this, arguments); this._rejected = true; } @@ -58,8 +65,8 @@ class DeferredImpl implements Deferred { return this._rejected || this._resolved; } } - // tslint:disable-next-line:no-any - export function createDeferred(scope: any = null): Deferred { +// tslint:disable-next-line:no-any +export function createDeferred(scope: any = null): Deferred { return new DeferredImpl(scope); } @@ -71,7 +78,7 @@ export function createTemporaryFile(extension: string, temporaryDirectory?: stri } return new Promise<{ filePath: string, cleanupCallback: Function }>((resolve, reject) => { - tmp.file(options, function _tempFileCreated(err, tmpFile, fd, cleanupCallback) { + tmp.file(options, (err, tmpFile, fd, cleanupCallback) => { if (err) { return reject(err); } diff --git a/src/client/common/installer.ts b/src/client/common/installer.ts deleted file mode 100644 index 6aa6adb1ace3..000000000000 --- a/src/client/common/installer.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { inject, injectable, named } from 'inversify'; -import * as os from 'os'; -import 'reflect-metadata'; -import 'reflect-metadata'; -import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; -import * as vscode from 'vscode'; -import * as settings from './configSettings'; -import { STANDARD_OUTPUT_CHANNEL } from './constants'; -import { isNotInstalledError } from './helpers'; -import { IInstaller, InstallerResponse, IOutputChannel, Product } from './types'; -import { execPythonFile, getFullyQualifiedPythonInterpreterPath, IS_WINDOWS } from './utils'; - -export { Product } from './types'; - -// tslint:disable-next-line:variable-name -const ProductInstallScripts = new Map(); -ProductInstallScripts.set(Product.autopep8, ['-m', 'pip', 'install', 'autopep8']); -ProductInstallScripts.set(Product.flake8, ['-m', 'pip', 'install', 'flake8']); -ProductInstallScripts.set(Product.mypy, ['-m', 'pip', 'install', 'mypy']); -ProductInstallScripts.set(Product.nosetest, ['-m', 'pip', 'install', 'nose']); -ProductInstallScripts.set(Product.pep8, ['-m', 'pip', 'install', 'pep8']); -ProductInstallScripts.set(Product.pylama, ['-m', 'pip', 'install', 'pylama']); -ProductInstallScripts.set(Product.prospector, ['-m', 'pip', 'install', 'prospector']); -ProductInstallScripts.set(Product.pydocstyle, ['-m', 'pip', 'install', 'pydocstyle']); -ProductInstallScripts.set(Product.pylint, ['-m', 'pip', 'install', 'pylint']); -ProductInstallScripts.set(Product.pytest, ['-m', 'pip', 'install', '-U', 'pytest']); -ProductInstallScripts.set(Product.yapf, ['-m', 'pip', 'install', 'yapf']); -ProductInstallScripts.set(Product.rope, ['-m', 'pip', 'install', 'rope']); - -// tslint:disable-next-line:variable-name -const ProductUninstallScripts = new Map(); -ProductUninstallScripts.set(Product.autopep8, ['-m', 'pip', 'uninstall', 'autopep8', '--yes']); -ProductUninstallScripts.set(Product.flake8, ['-m', 'pip', 'uninstall', 'flake8', '--yes']); -ProductUninstallScripts.set(Product.mypy, ['-m', 'pip', 'uninstall', 'mypy', '--yes']); -ProductUninstallScripts.set(Product.nosetest, ['-m', 'pip', 'uninstall', 'nose', '--yes']); -ProductUninstallScripts.set(Product.pep8, ['-m', 'pip', 'uninstall', 'pep8', '--yes']); -ProductUninstallScripts.set(Product.pylama, ['-m', 'pip', 'uninstall', 'pylama', '--yes']); -ProductUninstallScripts.set(Product.prospector, ['-m', 'pip', 'uninstall', 'prospector', '--yes']); -ProductUninstallScripts.set(Product.pydocstyle, ['-m', 'pip', 'uninstall', 'pydocstyle', '--yes']); -ProductUninstallScripts.set(Product.pylint, ['-m', 'pip', 'uninstall', 'pylint', '--yes']); -ProductUninstallScripts.set(Product.pytest, ['-m', 'pip', 'uninstall', 'pytest', '--yes']); -ProductUninstallScripts.set(Product.yapf, ['-m', 'pip', 'uninstall', 'yapf', '--yes']); -ProductUninstallScripts.set(Product.rope, ['-m', 'pip', 'uninstall', 'rope', '--yes']); - -// tslint:disable-next-line:variable-name -export const ProductExecutableAndArgs = new Map(); -ProductExecutableAndArgs.set(Product.mypy, { executable: 'python', args: ['-m', 'mypy'] }); -ProductExecutableAndArgs.set(Product.nosetest, { executable: 'python', args: ['-m', 'nose'] }); -ProductExecutableAndArgs.set(Product.pylama, { executable: 'python', args: ['-m', 'pylama'] }); -ProductExecutableAndArgs.set(Product.prospector, { executable: 'python', args: ['-m', 'prospector'] }); -ProductExecutableAndArgs.set(Product.pylint, { executable: 'python', args: ['-m', 'pylint'] }); -ProductExecutableAndArgs.set(Product.pytest, { executable: 'python', args: ['-m', 'pytest'] }); -ProductExecutableAndArgs.set(Product.autopep8, { executable: 'python', args: ['-m', 'autopep8'] }); -ProductExecutableAndArgs.set(Product.pep8, { executable: 'python', args: ['-m', 'pep8'] }); -ProductExecutableAndArgs.set(Product.pydocstyle, { executable: 'python', args: ['-m', 'pydocstyle'] }); -ProductExecutableAndArgs.set(Product.yapf, { executable: 'python', args: ['-m', 'yapf'] }); -ProductExecutableAndArgs.set(Product.flake8, { executable: 'python', args: ['-m', 'flake8'] }); - -switch (os.platform()) { - case 'win32': { - // Nothing - break; - } - case 'darwin': { - ProductInstallScripts.set(Product.ctags, ['brew install ctags']); - } - default: { - ProductInstallScripts.set(Product.ctags, ['sudo apt-get install exuberant-ctags']); - } -} - -// tslint:disable-next-line:variable-name -export const Linters: Product[] = [ - Product.flake8, - Product.pep8, - Product.pylama, - Product.prospector, - Product.pylint, - Product.mypy, - Product.pydocstyle -]; - -// tslint:disable-next-line:variable-name -const ProductNames = new Map(); -ProductNames.set(Product.autopep8, 'autopep8'); -ProductNames.set(Product.flake8, 'flake8'); -ProductNames.set(Product.mypy, 'mypy'); -ProductNames.set(Product.nosetest, 'nosetest'); -ProductNames.set(Product.pep8, 'pep8'); -ProductNames.set(Product.pylama, 'pylama'); -ProductNames.set(Product.prospector, 'prospector'); -ProductNames.set(Product.pydocstyle, 'pydocstyle'); -ProductNames.set(Product.pylint, 'pylint'); -ProductNames.set(Product.pytest, 'py.test'); -ProductNames.set(Product.yapf, 'yapf'); -ProductNames.set(Product.rope, 'rope'); - -// tslint:disable-next-line:variable-name -export const SettingToDisableProduct = new Map(); -SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled'); -SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled'); -SettingToDisableProduct.set(Product.nosetest, 'unitTest.nosetestsEnabled'); -SettingToDisableProduct.set(Product.pep8, 'linting.pep8Enabled'); -SettingToDisableProduct.set(Product.pylama, 'linting.pylamaEnabled'); -SettingToDisableProduct.set(Product.prospector, 'linting.prospectorEnabled'); -SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled'); -SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled'); -SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled'); - -// tslint:disable-next-line:variable-name -const ProductInstallationPrompt = new Map(); -ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols'); - -enum ProductType { - Linter, - Formatter, - TestFramework, - RefactoringLibrary, - WorkspaceSymbols -} - -// tslint:disable-next-line:variable-name -const ProductTypeNames = new Map(); -ProductTypeNames.set(ProductType.Formatter, 'Formatter'); -ProductTypeNames.set(ProductType.Linter, 'Linter'); -ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); -ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); -ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); - -// tslint:disable-next-line:variable-name -const ProductTypes = new Map(); -ProductTypes.set(Product.flake8, ProductType.Linter); -ProductTypes.set(Product.mypy, ProductType.Linter); -ProductTypes.set(Product.pep8, ProductType.Linter); -ProductTypes.set(Product.prospector, ProductType.Linter); -ProductTypes.set(Product.pydocstyle, ProductType.Linter); -ProductTypes.set(Product.pylama, ProductType.Linter); -ProductTypes.set(Product.pylint, ProductType.Linter); -ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); -ProductTypes.set(Product.nosetest, ProductType.TestFramework); -ProductTypes.set(Product.pytest, ProductType.TestFramework); -ProductTypes.set(Product.unittest, ProductType.TestFramework); -ProductTypes.set(Product.autopep8, ProductType.Formatter); -ProductTypes.set(Product.yapf, ProductType.Formatter); -ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); - -const IS_POWERSHELL = /powershell.exe$/i; - -@injectable() -export class Installer implements IInstaller { - private static terminal: vscode.Terminal | undefined | null; - private disposables: vscode.Disposable[] = []; - constructor( @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel?: vscode.OutputChannel) { - this.disposables.push(vscode.window.onDidCloseTerminal(term => { - if (term === Installer.terminal) { - Installer.terminal = null; - } - })); - } - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - private shouldDisplayPrompt(product: Product) { - // tslint:disable-next-line:no-non-null-assertion - const productName = ProductNames.get(product)!; - const pythonConfig = workspace.getConfiguration('python'); - // tslint:disable-next-line:prefer-type-cast - const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); - return disablePromptForFeatures.indexOf(productName) === -1; - } - - // tslint:disable-next-line:member-ordering - public async promptToInstall(product: Product, resource?: Uri): Promise { - // tslint:disable-next-line:no-non-null-assertion - const productType = ProductTypes.get(product)!; - // tslint:disable-next-line:no-non-null-assertion - const productTypeName = ProductTypeNames.get(productType)!; - // tslint:disable-next-line:no-non-null-assertion - const productName = ProductNames.get(product)!; - - if (!this.shouldDisplayPrompt(product)) { - const message = `${productTypeName} '${productName}' not installed.`; - if (this.outputChannel) { - this.outputChannel.appendLine(message); - } else { - console.warn(message); - } - return InstallerResponse.Ignore; - } - - // tslint:disable-next-line:no-non-null-assertion - const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product)! : `Install ${productName}`; - const disableOption = `Disable ${productTypeName}`; - const dontShowAgain = 'Don\'t show this prompt again'; - const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; - const useOtherFormatter = `Use '${alternateFormatter}' formatter`; - const options: string[] = []; - options.push(installOption); - if (productType === ProductType.Formatter) { - options.push(...[useOtherFormatter]); - } - if (SettingToDisableProduct.has(product)) { - options.push(...[disableOption, dontShowAgain]); - } - const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options); - if (!item) { - return InstallerResponse.Ignore; - } - switch (item) { - case installOption: { - return this.install(product, resource); - } - case disableOption: { - if (Linters.indexOf(product) >= 0) { - return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); - } else { - // tslint:disable-next-line:no-non-null-assertion - const settingToDisable = SettingToDisableProduct.get(product)!; - return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); - } - } - case useOtherFormatter: { - return this.updateSetting('formatting.provider', alternateFormatter, resource) - .then(() => InstallerResponse.Installed); - } - case dontShowAgain: { - const pythonConfig = workspace.getConfiguration('python'); - // tslint:disable-next-line:prefer-type-cast - const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); - features.push(productName); - return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); - } - default: { - throw new Error('Invalid selection'); - } - } - } - // tslint:disable-next-line:member-ordering - public async install(product: Product, resource?: Uri): Promise { - if (!this.outputChannel && !Installer.terminal) { - Installer.terminal = window.createTerminal('Python Installer'); - } - - if (product === Product.ctags && settings.IS_WINDOWS) { - if (this.outputChannel) { - this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); - this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); - this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.'); - this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); - this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); - this.outputChannel.show(); - } else { - window.showInformationMessage('Install Universal Ctags and set it in your path or define the path in your python.workspaceSymbols.ctagsPath settings'); - } - return InstallerResponse.Ignore; - } - - // tslint:disable-next-line:no-non-null-assertion - let installArgs = ProductInstallScripts.get(product)!; - const pipIndex = installArgs.indexOf('pip'); - if (pipIndex > 0) { - installArgs = installArgs.slice(); - const proxy = vscode.workspace.getConfiguration('http').get('proxy', ''); - if (proxy.length > 0) { - installArgs.splice(2, 0, proxy); - installArgs.splice(2, 0, '--proxy'); - } - } - // tslint:disable-next-line:no-any - let installationPromise: Promise; - if (this.outputChannel && installArgs[0] === '-m') { - // Errors are just displayed to the user - this.outputChannel.show(); - installationPromise = execPythonFile(resource, settings.PythonSettings.getInstance(resource).pythonPath, - // tslint:disable-next-line:no-non-null-assertion - installArgs, getCwdForInstallScript(resource), true, (data) => { this.outputChannel!.append(data); }); - } else { - // When using terminal get the fully qualitified path - // Cuz people may launch vs code from terminal when they have activated the appropriate virtual env - // Problem is terminal doesn't use the currently activated virtual env - // Must have something to do with the process being launched in the terminal - installationPromise = getFullyQualifiedPythonInterpreterPath(resource) - .then(pythonPath => { - let installScript = installArgs.join(' '); - - if (installArgs[0] === '-m') { - if (pythonPath.indexOf(' ') >= 0) { - installScript = `"${pythonPath}" ${installScript}`; - } else { - installScript = `${pythonPath} ${installScript}`; - } - } - if (this.terminalIsPowershell(resource)) { - installScript = `& ${installScript}`; - } - - // tslint:disable-next-line:no-non-null-assertion - Installer.terminal!.sendText(installScript); - // tslint:disable-next-line:no-non-null-assertion - Installer.terminal!.show(false); - }); - } - - return installationPromise - .then(async () => this.isInstalled(product)) - .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); - } - - // tslint:disable-next-line:member-ordering - public async isInstalled(product: Product, resource?: Uri): Promise { - return isProductInstalled(product, resource); - } - - // tslint:disable-next-line:member-ordering no-any - public async uninstall(product: Product, resource?: Uri): Promise { - return uninstallproduct(product, resource); - } - // tslint:disable-next-line:member-ordering - public async disableLinter(product: Product, resource?: Uri) { - if (resource && workspace.getWorkspaceFolder(resource)) { - // tslint:disable-next-line:no-non-null-assertion - const settingToDisable = SettingToDisableProduct.get(product)!; - const pythonConfig = workspace.getConfiguration('python', resource); - const isMultiroot = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; - const configTarget = isMultiroot ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - return pythonConfig.update(settingToDisable, false, configTarget); - } else { - const pythonConfig = workspace.getConfiguration('python'); - return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); - } - } - private terminalIsPowershell(resource?: Uri) { - if (!IS_WINDOWS) { - return false; - } - // tslint:disable-next-line:no-backbone-get-set-outside-model - const terminal = workspace.getConfiguration('terminal.integrated.shell', resource).get('windows'); - return typeof terminal === 'string' && IS_POWERSHELL.test(terminal); - } - // tslint:disable-next-line:no-any - private updateSetting(setting: string, value: any, resource?: Uri) { - if (resource && !workspace.getWorkspaceFolder(resource)) { - const pythonConfig = workspace.getConfiguration('python', resource); - return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); - } else { - const pythonConfig = workspace.getConfiguration('python'); - return pythonConfig.update(setting, value, true); - } - } -} - -function getCwdForInstallScript(resource?: Uri) { - const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - if (workspaceFolder) { - return workspaceFolder.uri.fsPath; - } - if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - return workspace.workspaceFolders[0].uri.fsPath; - } - return __dirname; -} - -async function isProductInstalled(product: Product, resource?: Uri): Promise { - if (!ProductExecutableAndArgs.has(product)) { - return; - } - // tslint:disable-next-line:no-non-null-assertion - const prodExec = ProductExecutableAndArgs.get(product)!; - const cwd = getCwdForInstallScript(resource); - return execPythonFile(resource, prodExec.executable, prodExec.args.concat(['--version']), cwd, false) - .then(() => true) - .catch(reason => !isNotInstalledError(reason)); -} - -// tslint:disable-next-line:no-any -async function uninstallproduct(product: Product, resource?: Uri): Promise { - if (!ProductUninstallScripts.has(product)) { - return Promise.resolve(); - } - // tslint:disable-next-line:no-non-null-assertion - const uninstallArgs = ProductUninstallScripts.get(product)!; - return execPythonFile(resource, 'python', uninstallArgs, getCwdForInstallScript(resource), false); -} diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts new file mode 100644 index 000000000000..ed1c523d09e8 --- /dev/null +++ b/src/client/common/installer/condaInstaller.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import 'reflect-metadata'; +import { Uri } from 'vscode'; +import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; +import { CONDA_RELATIVE_PY_PATH } from '../../interpreter/locators/services/conda'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonSettings } from '../configSettings'; +import { IPythonExecutionFactory } from '../process/types'; +import { ExecutionInfo } from '../types'; +import { arePathsSame } from '../utils'; +import { ModuleInstaller } from './moduleInstaller'; +import { IModuleInstaller } from './types'; + +@injectable() +export class CondaInstaller extends ModuleInstaller implements IModuleInstaller { + private isCondaAvailable: boolean | undefined; + public get displayName() { + return 'Conda'; + } + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + /** + * Checks whether we can use Conda as module installer for a given resource. + * We need to perform two checks: + * 1. Ensure we have conda. + * 2. Check if the current environment is a conda environment. + * @param {Uri} [resource=] Resource used to identify the workspace. + * @returns {Promise} Whether conda is supported as a module installer or not. + */ + public async isSupported(resource?: Uri): Promise { + if (typeof this.isCondaAvailable === 'boolean') { + return this.isCondaAvailable!; + } + const condaLocator = this.serviceContainer.get(ICondaLocatorService); + const available = await condaLocator.isCondaAvailable(); + + if (!available) { + return false; + } + + // Now we need to check if the current environment is a conda environment or not. + return this.isCurrentEnvironmentACondaEnvironment(resource); + } + protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { + const condaLocator = this.serviceContainer.get(ICondaLocatorService); + const condaFile = await condaLocator.getCondaFile(); + + const info = await this.getCurrentInterpreterInfo(resource); + const args = ['install']; + + if (info.envName) { + // If we have the name of the conda environment, then use that. + args.push('--name'); + args.push(info.envName!); + } else { + // Else provide the full path to the environment path. + args.push('--prefix'); + args.push(info.envPath); + } + args.push(moduleName); + return { + args, + execPath: condaFile, + moduleName: '' + }; + } + private async getCurrentPythonPath(resource?: Uri): Promise { + const pythonPath = PythonSettings.getInstance(resource).pythonPath; + if (path.basename(pythonPath) === pythonPath) { + const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + return pythonProc.getExecutablePath().catch(() => pythonPath); + } else { + return pythonPath; + } + } + private isCurrentEnvironmentACondaEnvironment(resource?: Uri) { + return this.getCurrentInterpreterInfo(resource) + .then(info => info && info.isConda === true).catch(() => false); + } + private async getCurrentInterpreterInfo(resource?: Uri) { + // Use this service, though it returns everything it is cached. + const interpreterLocator = this.serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); + const interpretersPromise = interpreterLocator.getInterpreters(resource); + const pythonPathPromise = this.getCurrentPythonPath(resource); + const [interpreters, currentPythonPath] = await Promise.all([interpretersPromise, pythonPathPromise]); + + // Check if we have the info about the current python path. + const pathToCompareWith = path.dirname(currentPythonPath); + const info = interpreters.find(item => arePathsSame(path.dirname(item.path), pathToCompareWith)); + // tslint:disable-next-line:prefer-array-literal + const pathsToRemove = new Array(CONDA_RELATIVE_PY_PATH.length).fill('..') as string[]; + const envPath = path.join(path.dirname(currentPythonPath), ...pathsToRemove); + return { + isConda: info && info!.type === InterpreterType.Conda, + pythonPath: currentPythonPath, + envPath, + envName: info ? info!.envName : undefined + }; + } +} diff --git a/src/client/common/installer/installer.ts b/src/client/common/installer/installer.ts new file mode 100644 index 000000000000..a78405d0670a --- /dev/null +++ b/src/client/common/installer/installer.ts @@ -0,0 +1,314 @@ +import { inject, injectable, named } from 'inversify'; +import * as os from 'os'; +import * as path from 'path'; +import 'reflect-metadata'; +import { ConfigurationTarget, QuickPickItem, Uri, window, workspace } from 'vscode'; +import * as vscode from 'vscode'; +import { IFormatterHelper } from '../../formatters/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ILinterHelper } from '../../linters/types'; +import { ITestsHelper } from '../../unittests/common/types'; +import { PythonSettings } from '../configSettings'; +import { STANDARD_OUTPUT_CHANNEL } from '../constants'; +import { IProcessService, IPythonExecutionFactory } from '../process/types'; +import { ITerminalService } from '../terminal/types'; +import { IInstaller, ILogger, InstallerResponse, IOutputChannel, IsWindows, ModuleNamePurpose, Product } from '../types'; +import { IModuleInstaller } from './types'; + +export { Product } from '../types'; + +const CTagsInsllationScript = os.platform() === 'darwin' ? 'brew install ctags' : 'sudo apt-get install exuberant-ctags'; + +// tslint:disable-next-line:variable-name +const ProductNames = new Map(); +ProductNames.set(Product.autopep8, 'autopep8'); +ProductNames.set(Product.flake8, 'flake8'); +ProductNames.set(Product.mypy, 'mypy'); +ProductNames.set(Product.nosetest, 'nosetest'); +ProductNames.set(Product.pep8, 'pep8'); +ProductNames.set(Product.pylama, 'pylama'); +ProductNames.set(Product.prospector, 'prospector'); +ProductNames.set(Product.pydocstyle, 'pydocstyle'); +ProductNames.set(Product.pylint, 'pylint'); +ProductNames.set(Product.pytest, 'pytest'); +ProductNames.set(Product.yapf, 'yapf'); +ProductNames.set(Product.rope, 'rope'); + +export const SettingToDisableProduct = new Map(); +SettingToDisableProduct.set(Product.flake8, 'linting.flake8Enabled'); +SettingToDisableProduct.set(Product.mypy, 'linting.mypyEnabled'); +SettingToDisableProduct.set(Product.nosetest, 'unitTest.nosetestsEnabled'); +SettingToDisableProduct.set(Product.pep8, 'linting.pep8Enabled'); +SettingToDisableProduct.set(Product.pylama, 'linting.pylamaEnabled'); +SettingToDisableProduct.set(Product.prospector, 'linting.prospectorEnabled'); +SettingToDisableProduct.set(Product.pydocstyle, 'linting.pydocstyleEnabled'); +SettingToDisableProduct.set(Product.pylint, 'linting.pylintEnabled'); +SettingToDisableProduct.set(Product.pytest, 'unitTest.pyTestEnabled'); + +// tslint:disable-next-line:variable-name +const ProductInstallationPrompt = new Map(); +ProductInstallationPrompt.set(Product.ctags, 'Install CTags to enable Python workspace symbols'); + +enum ProductType { + Linter, + Formatter, + TestFramework, + RefactoringLibrary, + WorkspaceSymbols +} + +const ProductTypeNames = new Map(); +ProductTypeNames.set(ProductType.Formatter, 'Formatter'); +ProductTypeNames.set(ProductType.Linter, 'Linter'); +ProductTypeNames.set(ProductType.RefactoringLibrary, 'Refactoring library'); +ProductTypeNames.set(ProductType.TestFramework, 'Test Framework'); +ProductTypeNames.set(ProductType.WorkspaceSymbols, 'Workspace Symbols'); + +const ProductTypes = new Map(); +ProductTypes.set(Product.flake8, ProductType.Linter); +ProductTypes.set(Product.mypy, ProductType.Linter); +ProductTypes.set(Product.pep8, ProductType.Linter); +ProductTypes.set(Product.prospector, ProductType.Linter); +ProductTypes.set(Product.pydocstyle, ProductType.Linter); +ProductTypes.set(Product.pylama, ProductType.Linter); +ProductTypes.set(Product.pylint, ProductType.Linter); +ProductTypes.set(Product.ctags, ProductType.WorkspaceSymbols); +ProductTypes.set(Product.nosetest, ProductType.TestFramework); +ProductTypes.set(Product.pytest, ProductType.TestFramework); +ProductTypes.set(Product.unittest, ProductType.TestFramework); +ProductTypes.set(Product.autopep8, ProductType.Formatter); +ProductTypes.set(Product.yapf, ProductType.Formatter); +ProductTypes.set(Product.rope, ProductType.RefactoringLibrary); + +@injectable() +export class Installer implements IInstaller { + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: vscode.OutputChannel, + @inject(IsWindows) private isWindows: boolean) { + } + // tslint:disable-next-line:no-empty + public dispose() { } + public async promptToInstall(product: Product, resource?: Uri): Promise { + const productType = ProductTypes.get(product)!; + const productTypeName = ProductTypeNames.get(productType)!; + const productName = ProductNames.get(product)!; + + if (!this.shouldDisplayPrompt(product)) { + const message = `${productTypeName} '${productName}' not installed.`; + this.outputChannel.appendLine(message); + return InstallerResponse.Ignore; + } + + const installOption = ProductInstallationPrompt.has(product) ? ProductInstallationPrompt.get(product)! : `Install ${productName}`; + const disableOption = `Disable ${productTypeName}`; + const dontShowAgain = 'Don\'t show this prompt again'; + const alternateFormatter = product === Product.autopep8 ? 'yapf' : 'autopep8'; + const useOtherFormatter = `Use '${alternateFormatter}' formatter`; + const options: string[] = []; + options.push(installOption); + if (productType === ProductType.Formatter) { + options.push(...[useOtherFormatter]); + } + if (SettingToDisableProduct.has(product)) { + options.push(...[disableOption, dontShowAgain]); + } + const item = await window.showErrorMessage(`${productTypeName} ${productName} is not installed`, ...options); + if (!item) { + return InstallerResponse.Ignore; + } + switch (item) { + case installOption: { + return this.install(product, resource); + } + case disableOption: { + if (ProductTypes.has(product) && ProductTypes.get(product)! === ProductType.Linter) { + return this.disableLinter(product, resource).then(() => InstallerResponse.Disabled); + } else { + const settingToDisable = SettingToDisableProduct.get(product)!; + return this.updateSetting(settingToDisable, false, resource).then(() => InstallerResponse.Disabled); + } + } + case useOtherFormatter: { + return this.updateSetting('formatting.provider', alternateFormatter, resource) + .then(() => InstallerResponse.Installed); + } + case dontShowAgain: { + const pythonConfig = workspace.getConfiguration('python'); + const features = pythonConfig.get('disablePromptForFeatures', [] as string[]); + features.push(productName); + return pythonConfig.update('disablePromptForFeatures', features, true).then(() => InstallerResponse.Ignore); + } + default: { + throw new Error('Invalid selection'); + } + } + } + public translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string { + switch (product) { + case Product.mypy: return 'mypy'; + case Product.nosetest: { + return purpose === ModuleNamePurpose.install ? 'nose' : 'nosetests'; + } + case Product.pylama: return 'pylama'; + case Product.prospector: return 'prospector'; + case Product.pylint: return 'pylint'; + case Product.pytest: return 'pytest'; + case Product.autopep8: return 'autopep8'; + case Product.pep8: return 'pep8'; + case Product.pydocstyle: return 'pydocstyle'; + case Product.yapf: return 'yapf'; + case Product.flake8: return 'flake8'; + case Product.unittest: return 'unittest'; + case Product.rope: return 'rope'; + default: { + throw new Error(`Product ${product} cannot be installed as a Python Module.`); + } + } + } + public async install(product: Product, resource?: Uri): Promise { + if (product === Product.unittest) { + return InstallerResponse.Installed; + } + if (product === Product.ctags) { + return this.installCTags(); + } + const installer = await this.getInstallationChannel(product, resource); + if (!installer) { + return InstallerResponse.Ignore; + } + + const moduleName = this.translateProductToModuleName(product, ModuleNamePurpose.install); + const logger = this.serviceContainer.get(ILogger); + await installer.installModule(moduleName) + .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); + + return this.isInstalled(product) + .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); + } + public async isInstalled(product: Product, resource?: Uri): Promise { + if (product === Product.unittest) { + return true; + } + let moduleName: string | undefined; + try { + moduleName = this.translateProductToModuleName(product, ModuleNamePurpose.run); + // tslint:disable-next-line:no-empty + } catch { } + + // User may have customized the module name or provided the fully qualifieid path. + const executableName = this.getExecutableNameFromSettings(product, resource); + + const isModule = typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; + // Prospector is an exception, it can be installed as a module, but not run as one. + if (product !== Product.prospector && isModule) { + const pythonProcess = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); + return pythonProcess.isModuleInstalled(executableName); + } else { + const process = this.serviceContainer.get(IProcessService); + const prospectorPath = PythonSettings.getInstance(resource).linting.prospectorPath; + return process.exec(prospectorPath, ['--version'], { mergeStdOutErr: true }) + .then(() => true) + .catch(() => false); + } + } + public async disableLinter(product: Product, resource?: Uri) { + if (resource && workspace.getWorkspaceFolder(resource)) { + const settingToDisable = SettingToDisableProduct.get(product)!; + const pythonConfig = workspace.getConfiguration('python', resource); + const isMultiroot = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; + const configTarget = isMultiroot ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; + return pythonConfig.update(settingToDisable, false, configTarget); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update('linting.enabledWithoutWorkspace', false, true); + } + } + private shouldDisplayPrompt(product: Product) { + const productName = ProductNames.get(product)!; + const pythonConfig = workspace.getConfiguration('python'); + const disablePromptForFeatures = pythonConfig.get('disablePromptForFeatures', [] as string[]); + return disablePromptForFeatures.indexOf(productName) === -1; + } + private installCTags() { + if (this.isWindows) { + this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); + this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); + this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.'); + this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); + this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); + this.outputChannel.show(); + } else { + const terminalService = this.serviceContainer.get(ITerminalService); + const logger = this.serviceContainer.get(ILogger); + terminalService.sendCommand(CTagsInsllationScript, []) + .catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`)); + } + return InstallerResponse.Ignore; + } + private async getInstallationChannel(product: Product, resource?: Uri): Promise { + const productName = ProductNames.get(product)!; + const channels = await this.getInstallationChannels(resource); + if (channels.length === 0) { + window.showInformationMessage(`No installers available to install ${productName}.`); + return; + } + if (channels.length === 1) { + return channels[0]; + } + const placeHolder = `Select an option to install ${productName}`; + const options = channels.map(installer => { + return { + label: `Install using ${installer.displayName}`, + description: '', + installer + } as QuickPickItem & { installer: IModuleInstaller }; + }); + const selection = await window.showQuickPick(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); + return selection ? selection.installer : undefined; + } + private async getInstallationChannels(resource?: Uri): Promise { + const installers = this.serviceContainer.getAll(IModuleInstaller); + const supportedInstallers = await Promise.all(installers.map(async installer => installer.isSupported(resource).then(supported => supported ? installer : undefined))); + return supportedInstallers.filter(installer => installer !== undefined).map(installer => installer!); + } + // tslint:disable-next-line:no-any + private updateSetting(setting: string, value: any, resource?: Uri) { + if (resource && workspace.getWorkspaceFolder(resource)) { + const pythonConfig = workspace.getConfiguration('python', resource); + return pythonConfig.update(setting, value, ConfigurationTarget.Workspace); + } else { + const pythonConfig = workspace.getConfiguration('python'); + return pythonConfig.update(setting, value, true); + } + } + private getExecutableNameFromSettings(product: Product, resource?: Uri): string { + const settings = PythonSettings.getInstance(resource); + const productType = ProductTypes.get(product)!; + switch (productType) { + case ProductType.WorkspaceSymbols: return settings.workspaceSymbols.ctagsPath; + case ProductType.TestFramework: { + const testHelper = this.serviceContainer.get(ITestsHelper); + const settingsPropNames = testHelper.getSettingsPropertyNames(product); + if (!settingsPropNames.pathName) { + // E.g. in the case of UnitTests we don't allow customizing the paths. + return this.translateProductToModuleName(product, ModuleNamePurpose.run); + } + return settings.unitTest[settingsPropNames.pathName] as string; + } + case ProductType.Formatter: { + const formatHelper = this.serviceContainer.get(IFormatterHelper); + const settingsPropNames = formatHelper.getSettingsPropertyNames(product); + return settings.formatting[settingsPropNames.pathName] as string; + } + case ProductType.RefactoringLibrary: return this.translateProductToModuleName(product, ModuleNamePurpose.run); + case ProductType.Linter: { + const linterHelper = this.serviceContainer.get(ILinterHelper); + const settingsPropNames = linterHelper.getSettingsPropertyNames(product); + return settings.linting[settingsPropNames.pathName] as string; + } + default: { + throw new Error(`Unrecognized Product '${product}'`); + } + } + } +} diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts new file mode 100644 index 000000000000..f20e6f498602 --- /dev/null +++ b/src/client/common/installer/moduleInstaller.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import 'reflect-metadata'; +import { Uri } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonSettings } from '../configSettings'; +import { ITerminalService } from '../terminal/types'; +import { ExecutionInfo } from '../types'; + +@injectable() +export abstract class ModuleInstaller { + constructor(protected serviceContainer: IServiceContainer) { } + public async installModule(name: string, resource?: Uri): Promise { + const executionInfo = await this.getExecutionInfo(name, resource); + const terminalService = this.serviceContainer.get(ITerminalService); + + if (executionInfo.moduleName) { + const pythonPath = PythonSettings.getInstance(resource).pythonPath; + await terminalService.sendCommand(pythonPath, ['-m', 'pip'].concat(executionInfo.args)); + } else { + await terminalService.sendCommand(executionInfo.execPath!, executionInfo.args); + } + } + public abstract isSupported(resource?: Uri): Promise; + protected abstract getExecutionInfo(moduleName: string, resource?: Uri): Promise; +} diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts new file mode 100644 index 000000000000..6379453bc1d9 --- /dev/null +++ b/src/client/common/installer/pipInstaller.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import 'reflect-metadata'; +import { Uri, workspace } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IPythonExecutionFactory } from '../process/types'; +import { ExecutionInfo } from '../types'; +import { ModuleInstaller } from './moduleInstaller'; +import { IModuleInstaller } from './types'; + +@injectable() +export class PipInstaller extends ModuleInstaller implements IModuleInstaller { + private isCondaAvailable: boolean | undefined; + public get displayName() { + return 'Pip'; + } + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer) { + super(serviceContainer); + } + public isSupported(resource?: Uri): Promise { + const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); + return pythonExecutionFactory.create(resource) + .then(proc => proc.isModuleInstalled('pip')) + .catch(() => false); + } + protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { + const proxyArgs = []; + const proxy = workspace.getConfiguration('http').get('proxy', ''); + if (proxy.length > 0) { + proxyArgs.push('--proxy'); + proxyArgs.push(proxy); + } + return { + args: [...proxyArgs, 'install', '-U', moduleName], + execPath: '', + moduleName: 'pip' + }; + } + private isPipAvailable(resource?: Uri) { + const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); + return pythonExecutionFactory.create(resource) + .then(proc => proc.isModuleInstalled('pip')) + .catch(() => false); + } +} diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts new file mode 100644 index 000000000000..0d95992254fa --- /dev/null +++ b/src/client/common/installer/types.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; + +export const IModuleInstaller = Symbol('IModuleInstaller'); + +export interface IModuleInstaller { + readonly displayName: string; + installModule(name: string): Promise; + isSupported(resource?: Uri): Promise; +} diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts index b93ccbd82c91..ae04c7dcdbd7 100644 --- a/src/client/common/platform/constants.ts +++ b/src/client/common/platform/constants.ts @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as os from 'os'; + export const WINDOWS_PATH_VARIABLE_NAME = 'Path'; export const NON_WINDOWS_PATH_VARIABLE_NAME = 'PATH'; export const IS_WINDOWS = /^win/.test(process.platform); +export const IS_64_BIT = os.arch() === 'x64'; diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts index 1814f8bdc5ba..f82e95713976 100644 --- a/src/client/common/platform/registry.ts +++ b/src/client/common/platform/registry.ts @@ -1,28 +1,19 @@ +import { injectable } from 'inversify'; +import 'reflect-metadata'; import * as Registry from 'winreg'; +import { Architecture, IRegistry, RegistryHive } from './types'; + enum RegistryArchitectures { x86 = 'x86', x64 = 'x64' } -export enum Architecture { - Unknown = 1, - x86 = 2, - x64 = 3 -} -export enum Hive { - HKCU, HKLM -} - -export interface IRegistry { - getKeys(key: string, hive: Hive, arch?: Architecture): Promise; - getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise; -} - +@injectable() export class RegistryImplementation implements IRegistry { - public async getKeys(key: string, hive: Hive, arch?: Architecture) { + public async getKeys(key: string, hive: RegistryHive, arch?: Architecture) { return getRegistryKeys({ hive: translateHive(hive)!, arch: translateArchitecture(arch), key }); } - public async getValue(key: string, hive: Hive, arch?: Architecture, name: string = '') { + public async getValue(key: string, hive: RegistryHive, arch?: Architecture, name: string = '') { return getRegistryValue({ hive: translateHive(hive)!, arch: translateArchitecture(arch), key }, name); } } @@ -69,11 +60,11 @@ function translateArchitecture(arch?: Architecture): RegistryArchitectures | und return; } } -function translateHive(hive: Hive): string | undefined { +function translateHive(hive: RegistryHive): string | undefined { switch (hive) { - case Hive.HKCU: + case RegistryHive.HKCU: return Registry.HKCU; - case Hive.HKLM: + case RegistryHive.HKLM: return Registry.HKLM; default: return; diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts new file mode 100644 index 000000000000..b84cc7d9727f --- /dev/null +++ b/src/client/common/platform/types.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export enum Architecture { + Unknown = 1, + x86 = 2, + x64 = 3 +} +export enum RegistryHive { + HKCU, HKLM +} + +export const IRegistry = Symbol('IRegistry'); + +export interface IRegistry { + getKeys(key: string, hive: RegistryHive, arch?: Architecture): Promise; + getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise; +} diff --git a/src/client/common/process/proc.ts b/src/client/common/process/proc.ts index ec8db186c526..5bd3b816e956 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -48,7 +48,6 @@ export class ProcessService implements IProcessService { if (source === 'stderr' && options.throwOnStdErr) { subscriber.error(new StdErrError(out)); } else { - source = options.mergeStdOutErr ? 'stdout' : source; subscriber.next({ source, out: out }); } }; @@ -103,6 +102,7 @@ export class ProcessService implements IProcessService { on(proc.stderr, 'data', (data: Buffer) => { if (options.mergeStdOutErr) { stdoutBuffers.push(data); + stderrBuffers.push(data); } else { stderrBuffers.push(data); } diff --git a/src/client/common/process/pythonProcess.ts b/src/client/common/process/pythonProcess.ts index 0c84b25e94e6..6221597ab61a 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -3,6 +3,8 @@ import { injectable } from 'inversify'; import 'reflect-metadata'; +import { ErrorUtils } from '../errors/errorUtils'; +import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; import { EnvironmentVariables } from '../variables/types'; import { ExecutionResult, IProcessService, IPythonExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; @@ -48,6 +50,16 @@ export class PythonExecutionService implements IPythonExecutionService { if (this.envVars) { opts.env = this.envVars; } - return this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); + const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); + + // If a module is not installed we'll have something in stderr. + if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName!, result.stderr)) { + const isInstalled = await this.isModuleInstalled(moduleName!); + if (!isInstalled) { + throw new ModuleNotInstalledError(moduleName!); + } + } + + return result; } } diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 69f9c871181d..93666ae179ae 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -30,7 +30,7 @@ export type SpawnOptions = ChildProcessSpawnOptions & { export type ExecutionResult = { stdout: T; - stderr?: string; + stderr?: T; }; export const IProcessService = Symbol('IProcessService'); diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index f27763665503..3368f4df0ff7 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -2,22 +2,34 @@ // Licensed under the MIT License. import 'reflect-metadata'; -import { Disposable } from 'vscode'; import { IServiceManager } from '../ioc/types'; -import { Installer } from './installer'; +import { CondaInstaller } from './installer/condaInstaller'; +import { Installer } from './installer/installer'; +import { PipInstaller } from './installer/pipInstaller'; +import { IModuleInstaller } from './installer/types'; import { Logger } from './logger'; import { PersistentStateFactory } from './persistentState'; -import { IS_WINDOWS as isWindows } from './platform/constants'; +import { IS_64_BIT, IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; -import { IDiposableRegistry, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows } from './types'; +import { RegistryImplementation } from './platform/registry'; +import { IRegistry } from './platform/types'; +import { TerminalService } from './terminal/service'; +import { ITerminalService } from './terminal/types'; +import { IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingletonInstance(IsWindows, isWindows); + serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + serviceManager.addSingletonInstance(Is64Bit, IS_64_BIT); + serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); serviceManager.addSingleton(IInstaller, Installer); + serviceManager.addSingleton(IModuleInstaller, CondaInstaller); + serviceManager.addSingleton(IModuleInstaller, PipInstaller); serviceManager.addSingleton(ILogger, Logger); + serviceManager.addSingleton(ITerminalService, TerminalService); serviceManager.addSingleton(IPathUtils, PathUtils); - const disposableRegistry = serviceManager.get(IDiposableRegistry); - disposableRegistry.push(serviceManager.get(IInstaller)); + if (IS_WINDOWS) { + serviceManager.addSingleton(IRegistry, RegistryImplementation); + } } diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts new file mode 100644 index 000000000000..116632d6b8ec --- /dev/null +++ b/src/client/common/terminal/service.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import 'reflect-metadata'; +import { Disposable, Terminal, Uri, window, workspace } from 'vscode'; +import { IServiceContainer } from '../../ioc/types'; +import { IDisposableRegistry, IsWindows } from '../types'; +import { ITerminalService } from './types'; + +const IS_POWERSHELL = /powershell.exe$/i; + +@injectable() +export class TerminalService implements ITerminalService { + private terminal?: Terminal; + private textPreviouslySentToTerminal: boolean = false; + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + public async sendCommand(command: string, args: string[]): Promise { + const text = this.buildTerminalText(command, args); + const term = await this.getTerminal(); + term.show(false); + term.sendText(text, true); + this.textPreviouslySentToTerminal = true; + } + + private async getTerminal() { + if (this.terminal) { + return this.terminal!; + } + this.terminal = window.createTerminal('Python'); + this.terminal.show(false); + + // Sometimes the terminal takes some time to start up before it can start accepting input. + // However if we have already sent text to the terminal, then no need to wait. + if (!this.textPreviouslySentToTerminal) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + const handler = window.onDidCloseTerminal((term) => { + if (term === this.terminal) { + this.terminal = undefined; + } + }); + + const disposables = this.serviceContainer.get(IDisposableRegistry); + disposables.push(this.terminal); + disposables.push(handler); + + return this.terminal; + } + + private buildTerminalText(command: string, args: string[]) { + const executable = command.indexOf(' ') ? `"${command}"` : command; + const commandPrefix = this.terminalIsPowershell() ? '& ' : ''; + return `${commandPrefix}${executable} ${args.join(' ')}`.trim(); + } + + private terminalIsPowershell(resource?: Uri) { + const isWindows = this.serviceContainer.get(IsWindows); + if (!isWindows) { + return false; + } + // tslint:disable-next-line:no-backbone-get-set-outside-model + const terminalName = workspace.getConfiguration('terminal.integrated.shell', resource).get('windows'); + return typeof terminalName === 'string' && IS_POWERSHELL.test(terminalName); + } +} diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts new file mode 100644 index 000000000000..1f03fa45a1dd --- /dev/null +++ b/src/client/common/terminal/types.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export const ITerminalService = Symbol('ITerminalCommandService'); + +export interface ITerminalService { + sendCommand(command: string, args: string[]): Promise; +} diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 1bf65f885674..8d3d191f264b 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -1,12 +1,13 @@ + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable, Uri } from 'vscode'; - +import { Uri } from 'vscode'; export const IOutputChannel = Symbol('IOutputChannel'); export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export const IsWindows = Symbol('IS_WINDOWS'); -export const IDiposableRegistry = Symbol('IDiposableRegistry'); +export const Is64Bit = Symbol('Is64Bit'); +export const IDisposableRegistry = Symbol('IDiposableRegistry'); export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); @@ -23,7 +24,7 @@ export interface IPersistentStateFactory { } export type ExecutionInfo = { - execPath: string; + execPath?: string; moduleName?: string; args: string[]; product?: Product; @@ -59,13 +60,19 @@ export enum Product { rope = 14 } +export enum ModuleNamePurpose { + install = 1, + run = 2 +} + export const IInstaller = Symbol('IInstaller'); -export interface IInstaller extends Disposable { +export interface IInstaller { promptToInstall(product: Product, resource?: Uri): Promise; install(product: Product, resource?: Uri): Promise; isInstalled(product: Product, resource?: Uri): Promise; disableLinter(product: Product, resource?: Uri): Promise; + translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string; } export const IPathUtils = Symbol('IPathUtils'); diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index 13b76dd62ff0..0648ca5b9f60 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -6,7 +6,7 @@ import 'reflect-metadata'; import { Disposable, FileSystemWatcher, Uri, workspace } from 'vscode'; import { PythonSettings } from '../configSettings'; import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from '../platform/constants'; -import { IDiposableRegistry, IsWindows } from '../types'; +import { IDisposableRegistry, IsWindows } from '../types'; import { EnvironmentVariables, IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; @injectable() @@ -16,7 +16,7 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid private disposables: Disposable[] = []; constructor( @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, - @inject(IDiposableRegistry) disposableRegistry: Disposable[], @inject(IsWindows) private isWidows: boolean) { + @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IsWindows) private isWidows: boolean) { disposableRegistry.push(this); } diff --git a/src/client/extension.ts b/src/client/extension.ts index 5f161fca3c3e..80714ecf4713 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -10,14 +10,16 @@ import { FeatureDeprecationManager } from './common/featureDeprecationManager'; import { createDeferred } from './common/helpers'; import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; -import { GLOBAL_MEMENTO, IDiposableRegistry, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; +import { GLOBAL_MEMENTO, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPersistentStateFactory, WORKSPACE_MEMENTO } from './common/types'; import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; import { SimpleConfigurationProvider } from './debugger'; +import { registerTypes as formattersRegisterTypes } from './formatters/serviceRegistry'; import { InterpreterManager } from './interpreter'; import { SetInterpreterProvider } from './interpreter/configuration/setInterpreterProvider'; +import { ICondaLocatorService } from './interpreter/contracts'; import { ShebangCodeLensProvider } from './interpreter/display/shebangCodeLensProvider'; -import { getCondaVersion } from './interpreter/helpers'; import { InterpreterVersionService } from './interpreter/interpreterVersion'; +import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; import { ServiceContainer } from './ioc/container'; import { ServiceManager } from './ioc/serviceManager'; import { IServiceContainer } from './ioc/types'; @@ -52,17 +54,13 @@ const PYTHON: vscode.DocumentFilter = { language: 'python' }; const activationDeferred = createDeferred(); export const activated = activationDeferred.promise; -let cont: Container; -let serviceManager: ServiceManager; -let serviceContainer: ServiceContainer; - // tslint:disable-next-line:max-func-body-length export async function activate(context: vscode.ExtensionContext) { - cont = new Container(); - serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + const serviceContainer = new ServiceContainer(cont); serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IDiposableRegistry, context.subscriptions); + serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); @@ -76,14 +74,18 @@ export async function activate(context: vscode.ExtensionContext) { variableRegisterTypes(serviceManager); unitTestsRegisterTypes(serviceManager); lintersRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + formattersRegisterTypes(serviceManager); const persistentStateFactory = serviceManager.get(IPersistentStateFactory); const pythonSettings = settings.PythonSettings.getInstance(); - sendStartupTelemetry(activated); + sendStartupTelemetry(activated, serviceContainer); sortImports.activate(context, standardOutputChannel); - const interpreterManager = new InterpreterManager(); + const interpreterManager = new InterpreterManager(serviceContainer); + // This must be completed before we can continue. await interpreterManager.autoSetInterpreter(); + interpreterManager.refresh() .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); context.subscriptions.push(interpreterManager); @@ -91,7 +93,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService)); context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); - activateSimplePythonRefactorProvider(context, standardOutputChannel); + activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); const jediFactory = new JediFactory(context.asAbsolutePath('.')); context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); @@ -118,7 +120,7 @@ export async function activate(context: vscode.ExtensionContext) { }); context.subscriptions.push(jediFactory); - context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(standardOutputChannel))); + context.subscriptions.push(vscode.languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer))); const definitionProvider = new PythonDefinitionProvider(jediFactory); context.subscriptions.push(vscode.languages.registerDefinitionProvider(PYTHON, definitionProvider)); context.subscriptions.push(vscode.languages.registerHoverProvider(PYTHON, new PythonHoverProvider(jediFactory))); @@ -132,7 +134,7 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.languages.registerSignatureHelpProvider(PYTHON, new PythonSignatureProvider(jediFactory), '(', ',')); } if (pythonSettings.formatting.provider !== 'none') { - const formatProvider = new PythonFormattingEditProvider(context, standardOutputChannel); + const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); context.subscriptions.push(vscode.languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); } @@ -158,7 +160,7 @@ export async function activate(context: vscode.ExtensionContext) { } tests.activate(context, unitTestOutChannel, symbolProvider, serviceContainer); - context.subscriptions.push(new WorkspaceSymbols(standardOutputChannel)); + context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); context.subscriptions.push(vscode.languages.registerOnTypeFormattingEditProvider(PYTHON, new BlockFormatProviders(), ':')); // In case we have CR LF @@ -176,18 +178,17 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(new FeatureDeprecationManager(persistentStateFactory, !!jupyterExtInstalled)); } -function sendStartupTelemetry(activatedPromise: Promise) { +async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { const stopWatch = new StopWatch(); - activatedPromise - .then(async () => { - const duration = stopWatch.elapsedTime; - let condaVersion: string | undefined; - try { - condaVersion = await getCondaVersion(); - // tslint:disable-next-line:no-empty - } catch { } - const props = condaVersion ? { condaVersion } : undefined; - sendTelemetryEvent(EDITOR_LOAD, duration, props); - }) - .catch(ex => console.error('Python Extension: sendStartupTelemetry', ex)); + const logger = serviceContainer.get(ILogger); + try { + await activatedPromise; + const duration = stopWatch.elapsedTime; + const condaLocator = serviceContainer.get(ICondaLocatorService); + const condaVersion = await condaLocator.getCondaVersion().catch(() => undefined); + const props = condaVersion ? { condaVersion } : undefined; + sendTelemetryEvent(EDITOR_LOAD, duration, props); + } catch (ex) { + logger.logError('sendStartupTelemetry failed.', ex); + } } diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts index 0da76fd8d129..1fe1a6b08218 100644 --- a/src/client/formatters/autoPep8Formatter.ts +++ b/src/client/formatters/autoPep8Formatter.ts @@ -1,16 +1,15 @@ -'use strict'; - import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; +import { Product } from '../common/installer/installer'; +import { IServiceContainer } from '../ioc/types'; import { sendTelemetryWhenDone } from '../telemetry'; import { FORMAT } from '../telemetry/constants'; import { StopWatch } from '../telemetry/stopWatch'; import { BaseFormatter } from './baseFormatter'; export class AutoPep8Formatter extends BaseFormatter { - constructor(outputChannel: vscode.OutputChannel) { - super('autopep8', Product.autopep8, outputChannel); + constructor(serviceContainer: IServiceContainer) { + super('autopep8', Product.autopep8, serviceContainer); } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts index 68e6a76bfdb8..0f16c0a8cfc4 100644 --- a/src/client/formatters/baseFormatter.ts +++ b/src/client/formatters/baseFormatter.ts @@ -1,20 +1,18 @@ -'use strict'; - import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { Uri } from 'vscode'; +import { OutputChannel, Uri } from 'vscode'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { isNotInstalledError } from '../common/helpers'; -import { Installer, Product } from '../common/installer'; -import * as settings from './../common/configSettings'; +import { IInstaller, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; import { getTempFileWithDocumentContents, getTextEditsFromPatch } from './../common/editor'; import { execPythonFile } from './../common/utils'; - export abstract class BaseFormatter { - private installer: Installer; - constructor(public Id: string, private product: Product, protected outputChannel: vscode.OutputChannel) { - this.installer = new Installer(); + protected readonly outputChannel: OutputChannel; + constructor(public Id: string, private product: Product, private serviceContainer: IServiceContainer) { + this.outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); } public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable; @@ -34,7 +32,7 @@ export abstract class BaseFormatter { } return vscode.Uri.file(__dirname); } - protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, command: string, args: string[], cwd: string = null): Thenable { + protected provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, command: string, args: string[], cwd?: string): Thenable { this.outputChannel.clear(); if (typeof cwd !== 'string' || cwd.length === 0) { cwd = this.getWorkspaceUri(document).fsPath; @@ -44,13 +42,13 @@ export abstract class BaseFormatter { // However they don't support returning the diff of the formatted text when reading data from the input stream // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have // to be done here in node (extension), i.e. extension cpu, i.e. les responsive solution - let tmpFileCreated = document.isDirty; - let filePromise = tmpFileCreated ? getTempFileWithDocumentContents(document) : Promise.resolve(document.fileName); + const tmpFileCreated = document.isDirty; + const filePromise = tmpFileCreated ? getTempFileWithDocumentContents(document) : Promise.resolve(document.fileName); const promise = filePromise.then(filePath => { if (token && token.isCancellationRequested) { return [filePath, '']; } - return Promise.all([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd)]); + return Promise.all([Promise.resolve(filePath), execPythonFile(document.uri, command, args.concat([filePath]), cwd!)]); }).then(data => { // Delete the temporary file created if (tmpFileCreated) { @@ -74,19 +72,21 @@ export abstract class BaseFormatter { if (isNotInstalledError(error)) { // Check if we have some custom arguments such as "pylint --load-plugins pylint_django" // Such settings are no longer supported - let stuffAfterFileName = fileName.substring(fileName.toUpperCase().lastIndexOf(expectedFileName) + expectedFileName.length); + const stuffAfterFileName = fileName.substring(fileName.toUpperCase().lastIndexOf(expectedFileName) + expectedFileName.length); // Ok if we have a space after the file name, this means we have some arguments defined and this isn't supported if (stuffAfterFileName.trim().indexOf(' ') > 0) { + // tslint:disable-next-line:prefer-template customError = `Formatting failed, custom arguments in the 'python.formatting.${this.Id}Path' is not supported.\n` + `Custom arguments to the formatter can be defined in 'python.formatter.${this.Id}Args' setting of settings.json.`; } else { + const installer = this.serviceContainer.get(IInstaller); customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - this.installer.promptToInstall(this.product, resource) + installer.promptToInstall(this.product, resource) .catch(ex => console.error('Python Extension: promptToInstall', ex)); } } - this.outputChannel.appendLine(`\n${customError}\n${error + ''}`); + this.outputChannel.appendLine(`\n${customError}\n${error}`); } } diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts index 481b57c69ae6..b4fe65547d60 100644 --- a/src/client/formatters/dummyFormatter.ts +++ b/src/client/formatters/dummyFormatter.ts @@ -1,12 +1,11 @@ -'use strict'; - import * as vscode from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; import { BaseFormatter } from './baseFormatter'; -import { Product } from '../common/installer'; export class DummyFormatter extends BaseFormatter { - constructor(outputChannel: vscode.OutputChannel) { - super('none', Product.yapf, outputChannel); + constructor(serviceContainer: IServiceContainer) { + super('none', Product.yapf, serviceContainer); } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts new file mode 100644 index 000000000000..180ed3433932 --- /dev/null +++ b/src/client/formatters/helper.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import 'reflect-metadata'; +import { IFormattingSettings } from '../common/configSettings'; +import { Product } from '../common/types'; +import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; + +@injectable() +export class FormatterHelper implements IFormatterHelper { + public translateToId(formatter: Product): FormatterId { + switch (formatter) { + case Product.autopep8: return 'autopep8'; + case Product.yapf: return 'yapf'; + default: { + throw new Error(`Unrecognized Formatter '${formatter}'`); + } + } + } + public getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames { + const id = this.translateToId(formatter); + return { + argsName: `${id}Args` as keyof IFormattingSettings, + pathName: `${id}Path` as keyof IFormattingSettings + }; + } +} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts new file mode 100644 index 000000000000..a4d12e313582 --- /dev/null +++ b/src/client/formatters/serviceRegistry.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import 'reflect-metadata'; +import { IServiceManager } from '../ioc/types'; +import { FormatterHelper } from './helper'; +import { IFormatterHelper } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IFormatterHelper, FormatterHelper); +} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts new file mode 100644 index 000000000000..93d9aad58652 --- /dev/null +++ b/src/client/formatters/types.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IFormattingSettings } from '../common/configSettings'; +import { Product } from '../common/types'; + +export const IFormatterHelper = Symbol('IFormatterHelper'); + +export type FormatterId = 'autopep8' | 'yapf'; + +export type FormatterSettingsPropertyNames = { + argsName: keyof IFormattingSettings; + pathName: keyof IFormattingSettings; +}; + +export interface IFormatterHelper { + translateToId(formatter: Product): FormatterId; + getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; +} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts index 4f6d2474b717..497629f25efc 100644 --- a/src/client/formatters/yapfFormatter.ts +++ b/src/client/formatters/yapfFormatter.ts @@ -1,16 +1,15 @@ -'use strict'; - import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; -import { sendTelemetryWhenDone} from '../telemetry'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryWhenDone } from '../telemetry'; import { FORMAT } from '../telemetry/constants'; import { StopWatch } from '../telemetry/stopWatch'; import { BaseFormatter } from './baseFormatter'; export class YapfFormatter extends BaseFormatter { - constructor(outputChannel: vscode.OutputChannel) { - super('yapf', Product.yapf, outputChannel); + constructor(serviceContainer: IServiceContainer) { + super('yapf', Product.yapf, serviceContainer); } public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { diff --git a/src/client/interpreter/configuration/pythonPathUpdaterService.ts b/src/client/interpreter/configuration/pythonPathUpdaterService.ts index f20adb79fe7b..ff74f3112bdf 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -4,7 +4,7 @@ import { sendTelemetryEvent } from '../../telemetry'; import { PYTHON_INTERPRETER } from '../../telemetry/constants'; import { StopWatch } from '../../telemetry/stopWatch'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; -import { IInterpreterVersionService } from '../interpreterVersion'; +import { IInterpreterVersionService } from '../contracts'; import { IPythonPathUpdaterServiceFactory } from './types'; export class PythonPathUpdaterService { diff --git a/src/client/interpreter/configuration/setInterpreterProvider.ts b/src/client/interpreter/configuration/setInterpreterProvider.ts index 418c12550c5c..22a776f97941 100644 --- a/src/client/interpreter/configuration/setInterpreterProvider.ts +++ b/src/client/interpreter/configuration/setInterpreterProvider.ts @@ -1,11 +1,9 @@ -'use strict'; import * as path from 'path'; import { commands, ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri, window, workspace } from 'vscode'; import { InterpreterManager } from '../'; import * as settings from '../../common/configSettings'; -import { PythonInterpreter, WorkspacePythonPath } from '../contracts'; +import { IInterpreterVersionService, PythonInterpreter, WorkspacePythonPath } from '../contracts'; import { ShebangCodeLensProvider } from '../display/shebangCodeLensProvider'; -import { IInterpreterVersionService } from '../interpreterVersion'; import { PythonPathUpdaterService } from './pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './pythonPathUpdaterServiceFactory'; @@ -62,7 +60,7 @@ export class SetInterpreterProvider implements Disposable { private async setInterpreter() { const setInterpreterGlobally = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; let configTarget = ConfigurationTarget.Global; - let wkspace: Uri; + let wkspace: Uri | undefined; if (!setInterpreterGlobally) { const targetConfig = await this.getWorkspaceToSetPythonPath(); if (!targetConfig) { @@ -90,13 +88,13 @@ export class SetInterpreterProvider implements Disposable { } private async setShebangInterpreter(): Promise { - const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor.document); + const shebang = await ShebangCodeLensProvider.detectShebang(window.activeTextEditor!.document); if (!shebang) { return; } const isGlobalChange = !Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0; - const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + const workspaceFolder = workspace.getWorkspaceFolder(window.activeTextEditor!.document.uri); const isWorkspaceChange = Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length === 1; if (isGlobalChange) { @@ -105,7 +103,7 @@ export class SetInterpreterProvider implements Disposable { } if (isWorkspaceChange || !workspaceFolder) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', workspace.workspaceFolders[0].uri); + await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', workspace.workspaceFolders![0].uri); return; } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index f1371688ccc6..271dd203e1f4 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,12 +1,43 @@ import { ConfigurationTarget, Disposable, Uri } from 'vscode'; -import { Architecture } from '../common/platform/registry'; +import { Architecture } from '../common/platform/types'; + +export const INTERPRETER_LOCATOR_SERVICE = 'IInterpreterLocatorService'; +export const WINDOWS_REGISTRY_SERVICE = 'WindowsRegistryService'; +export const CONDA_ENV_FILE_SERVICE = 'CondaEnvFileService'; +export const CONDA_ENV_SERVICE = 'CondaEnvService'; +export const CURRENT_PATH_SERVICE = 'CurrentPathService'; +export const KNOWN_PATH_SERVICE = 'KnownPathsService'; +export const VIRTUAL_ENV_SERVICE = 'VirtualEnvService'; + +export const IInterpreterVersionService = Symbol('IInterpreterVersionService'); +export interface IInterpreterVersionService { + getVersion(pythonPath: string, defaultValue: string): Promise; + getPipVersion(pythonPath: string): Promise; +} + +export const ICondaEnvironmentFile = Symbol('ICondaEnvironmentFile'); +export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); +export const IKnownSearchPathsForVirtualEnvironments = Symbol('IKnownSearchPathsForVirtualEnvironments'); + +export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); export interface IInterpreterLocatorService extends Disposable { getInterpreters(resource?: Uri): Promise; } +export const ICondaLocatorService = Symbol('ICondaLocatorService'); + export interface ICondaLocatorService { getCondaFile(): Promise; + isCondaAvailable(): Promise; + getCondaVersion(): Promise; +} + +export enum InterpreterType { + Unknown = 1, + Conda = 2, + VirtualEnv = 4, + VEnv = 8 } export type PythonInterpreter = { @@ -15,6 +46,8 @@ export type PythonInterpreter = { displayName?: string; version?: string; architecture?: Architecture; + type: InterpreterType; + envName?: string; }; export type WorkspacePythonPath = { diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index b104f4953216..74efb2fd11fa 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,20 +1,18 @@ -'use strict'; import * as child_process from 'child_process'; import { EOL } from 'os'; import * as path from 'path'; -import { Disposable, StatusBarItem } from 'vscode'; +import { Disposable, StatusBarItem, Uri } from 'vscode'; import { PythonSettings } from '../../common/configSettings'; import * as utils from '../../common/utils'; -import { IInterpreterLocatorService } from '../contracts'; +import { IInterpreterLocatorService, IInterpreterVersionService } from '../contracts'; import { getActiveWorkspaceUri, getFirstNonEmptyLineFromMultilineString } from '../helpers'; -import { IInterpreterVersionService } from '../interpreterVersion'; -import { VirtualEnvironmentManager } from '../virtualEnvs/index'; +import { IVirtualEnvironmentManager } from '../virtualEnvs/types'; // tslint:disable-next-line:completed-docs export class InterpreterDisplay implements Disposable { constructor(private statusBar: StatusBarItem, private interpreterLocator: IInterpreterLocatorService, - private virtualEnvMgr: VirtualEnvironmentManager, + private virtualEnvMgr: IVirtualEnvironmentManager, private versionProvider: IInterpreterVersionService) { this.statusBar.command = 'python.setInterpreter'; @@ -28,13 +26,13 @@ export class InterpreterDisplay implements Disposable { return; } const pythonPath = await this.getFullyQualifiedPathToInterpreter(PythonSettings.getInstance(wkspc.folderUri).pythonPath); - await this.updateDisplay(pythonPath); + await this.updateDisplay(pythonPath, wkspc.folderUri); } - private async getInterpreters() { - return this.interpreterLocator.getInterpreters(); + private async getInterpreters(resource?: Uri) { + return this.interpreterLocator.getInterpreters(resource); } - private async updateDisplay(pythonPath: string) { - const interpreters = await this.getInterpreters(); + private async updateDisplay(pythonPath: string, resource?: Uri) { + const interpreters = await this.getInterpreters(resource); const interpreter = interpreters.find(i => utils.arePathsSame(i.path, pythonPath)); this.statusBar.color = ''; diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index dce82ad331be..dc8308344d2f 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,10 +1,5 @@ -import * as child_process from 'child_process'; import { ConfigurationTarget, window, workspace } from 'vscode'; -import { RegistryImplementation } from '../common/platform/registry'; -import { Is_64Bit, IS_WINDOWS } from '../common/utils'; import { WorkspacePythonPath } from './contracts'; -import { CondaLocatorService } from './locators/services/condaLocator'; -import { WindowsRegistryService } from './locators/services/windowsRegistryService'; export function getFirstNonEmptyLineFromMultilineString(stdout: string) { if (!stdout) { @@ -28,20 +23,3 @@ export function getActiveWorkspaceUri(): WorkspacePythonPath | undefined { } return undefined; } -export async function getCondaVersion() { - 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) => { - if (stdout && stdout.length > 0) { - resolve(getFirstNonEmptyLineFromMultilineString(stdout)); - } else { - reject(); - } - }); - }); - }); -} diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index 4f9973010575..d52614431228 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -1,37 +1,35 @@ -'use strict'; import * as path from 'path'; import { ConfigurationTarget, Disposable, StatusBarAlignment, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { IServiceContainer } from '../ioc/types'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; +import { IInterpreterLocatorService, IInterpreterVersionService, INTERPRETER_LOCATOR_SERVICE } from './contracts'; import { InterpreterDisplay } from './display'; import { getActiveWorkspaceUri } from './helpers'; -import { InterpreterVersionService } from './interpreterVersion'; import { PythonInterpreterLocatorService } from './locators'; -import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { VEnv } from './virtualEnvs/venv'; -import { VirtualEnv } from './virtualEnvs/virtualEnv'; +import { VirtualEnvService } from './locators/services/virtualEnvService'; +import { IVirtualEnvironmentManager } from './virtualEnvs/types'; export class InterpreterManager implements Disposable { private disposables: Disposable[] = []; private display: InterpreterDisplay | null | undefined; private interpreterProvider: PythonInterpreterLocatorService; private pythonPathUpdaterService: PythonPathUpdaterService; - constructor() { - const virtualEnvMgr = new VirtualEnvironmentManager([new VEnv(), new VirtualEnv()]); + constructor(private serviceContainer: IServiceContainer) { + const virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); const statusBar = window.createStatusBarItem(StatusBarAlignment.Left); - this.interpreterProvider = new PythonInterpreterLocatorService(virtualEnvMgr); - const versionService = new InterpreterVersionService(); + this.interpreterProvider = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); + const versionService = serviceContainer.get(IInterpreterVersionService); this.display = new InterpreterDisplay(statusBar, this.interpreterProvider, virtualEnvMgr, versionService); - const interpreterVersionService = new InterpreterVersionService(); - this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), interpreterVersionService); + this.pythonPathUpdaterService = new PythonPathUpdaterService(new PythonPathUpdaterServiceFactory(), versionService); PythonSettings.getInstance().addListener('change', () => this.onConfigChanged()); this.disposables.push(window.onDidChangeActiveTextEditor(() => this.refresh())); this.disposables.push(statusBar); - this.disposables.push(this.display); + this.disposables.push(this.display!); } public async refresh() { - return this.display.refresh(); + return this.display!.refresh(); } public getInterpreters(resource?: Uri) { return this.interpreterProvider.getInterpreters(resource); @@ -44,6 +42,9 @@ export class InterpreterManager implements Disposable { if (!activeWorkspace) { return; } + const virtualEnvMgr = this.serviceContainer.get(IVirtualEnvironmentManager); + const versionService = this.serviceContainer.get(IInterpreterVersionService); + const virtualEnvInterpreterProvider = new VirtualEnvService([activeWorkspace.folderUri.fsPath], virtualEnvMgr, versionService); const interpreters = await this.interpreterProvider.getInterpreters(activeWorkspace.folderUri); const workspacePathUpper = activeWorkspace.folderUri.fsPath.toUpperCase(); const interpretersInWorkspace = interpreters.filter(interpreter => interpreter.path.toUpperCase().startsWith(workspacePathUpper)); @@ -88,7 +89,7 @@ export class InterpreterManager implements Disposable { } private onConfigChanged() { if (this.display) { - this.display.refresh() + this.display!.refresh() .catch(ex => console.error('Python Extension: display.refresh', ex)); } } diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts index 0f7ef485e388..3d4c11ebc4d0 100644 --- a/src/client/interpreter/interpreterVersion.ts +++ b/src/client/interpreter/interpreterVersion.ts @@ -1,13 +1,12 @@ import * as child_process from 'child_process'; +import { injectable } from 'inversify'; +import 'reflect-metadata'; import { getInterpreterVersion } from '../common/utils'; - -export interface IInterpreterVersionService { - getVersion(pythonPath: string, defaultValue: string): Promise; - getPipVersion(pythonPath: string): Promise; -} +import { IInterpreterVersionService } from './contracts'; const PIP_VERSION_REGEX = '\\d\\.\\d(\\.\\d)+'; +@injectable() export class InterpreterVersionService implements IInterpreterVersionService { public async getVersion(pythonPath: string, defaultValue: string): Promise { return getInterpreterVersion(pythonPath) diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts index ac8b0a451d3c..de90e042a01f 100644 --- a/src/client/interpreter/locators/index.ts +++ b/src/client/interpreter/locators/index.ts @@ -1,37 +1,40 @@ -'use strict'; +import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; import { Disposable, Uri, workspace } from 'vscode'; -import { RegistryImplementation } from '../../common/platform/registry'; -import { arePathsSame, Is_64Bit, IS_WINDOWS } from '../../common/utils'; -import { IInterpreterLocatorService, PythonInterpreter } from '../contracts'; -import { InterpreterVersionService } from '../interpreterVersion'; -import { VirtualEnvironmentManager } from '../virtualEnvs'; +import { IDisposableRegistry, IsWindows } from '../../common/types'; +import { arePathsSame } from '../../common/utils'; +import { IServiceContainer } from '../../ioc/types'; +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + IInterpreterLocatorService, + InterpreterType, + KNOWN_PATH_SERVICE, + PythonInterpreter, + VIRTUAL_ENV_SERVICE, + WINDOWS_REGISTRY_SERVICE +} from '../contracts'; import { fixInterpreterDisplayName } 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'; -import { WindowsRegistryService } from './services/windowsRegistryService'; +@injectable() export class PythonInterpreterLocatorService implements IInterpreterLocatorService { - private interpretersPerResource: Map; + private interpretersPerResource: Map>; private disposables: Disposable[] = []; - constructor(private virtualEnvMgr: VirtualEnvironmentManager) { - this.interpretersPerResource = new Map(); + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IsWindows) private isWindows: boolean) { + this.interpretersPerResource = new Map>(); this.disposables.push(workspace.onDidChangeConfiguration(this.onConfigChanged, this)); + serviceContainer.get(IDisposableRegistry).push(this); } public async getInterpreters(resource?: Uri) { const resourceKey = this.getResourceKey(resource); if (!this.interpretersPerResource.has(resourceKey)) { - const interpreters = await this.getInterpretersPerResource(resource); - this.interpretersPerResource.set(resourceKey, interpreters); + this.interpretersPerResource.set(resourceKey, this.getInterpretersPerResource(resource)); } - // tslint:disable-next-line:no-non-null-assertion - return this.interpretersPerResource.get(resourceKey)!; + return await this.interpretersPerResource.get(resourceKey)!; } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); @@ -56,37 +59,32 @@ export class PythonInterpreterLocatorService implements IInterpreterLocatorServi .map(fixInterpreterDisplayName) .map(item => { item.path = path.normalize(item.path); return item; }) .reduce((accumulator, current) => { - if (accumulator.findIndex(item => arePathsSame(item.path, current.path)) === -1) { + const existingItem = accumulator.find(item => arePathsSame(item.path, current.path)); + if (!existingItem) { accumulator.push(current); + } else { + // Preserve type information. + if (existingItem.type === InterpreterType.Unknown && current.type !== InterpreterType.Unknown) { + existingItem.type = current.type; + } } return accumulator; }, []); } private getLocators(resource?: Uri) { const locators: IInterpreterLocatorService[] = []; - const versionService = new InterpreterVersionService(); // 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(condaLocator)); - } else { - const condaLocator = new CondaLocatorService(IS_WINDOWS); - locators.push(new CondaEnvService(condaLocator)); + if (this.isWindows) { + locators.push(this.serviceContainer.get(IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE)); } - // Supplements the above list of conda environments. - locators.push(new CondaEnvFileService(getCondaEnvFile(), versionService)); - locators.push(new VirtualEnvService(getKnownSearchPathsForVirtualEnvs(resource), this.virtualEnvMgr, versionService)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, CONDA_ENV_SERVICE)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, CONDA_ENV_FILE_SERVICE)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, VIRTUAL_ENV_SERVICE)); - if (!IS_WINDOWS) { - // This must be last, it is possible we have paths returned here that are already returned - // in one of the above lists. - locators.push(new KnownPathsService(getKnownSearchPathsForInterpreters(), versionService)); + if (!this.isWindows) { + locators.push(this.serviceContainer.get(IInterpreterLocatorService, KNOWN_PATH_SERVICE)); } - // This must be last, it is possible we have paths returned here that are already returned - // in one of the above lists. - locators.push(new CurrentPathService(this.virtualEnvMgr, versionService)); + locators.push(this.serviceContainer.get(IInterpreterLocatorService, CURRENT_PATH_SERVICE)); return locators; } diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts index bdb240ededad..6f767c5e847c 100644 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ b/src/client/interpreter/locators/services/KnownPathsService.ts @@ -1,17 +1,19 @@ -'use strict'; +import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; +import 'reflect-metadata'; import { Uri } from 'vscode'; import { fsExistsAsync, IS_WINDOWS } from '../../../common/utils'; -import { IInterpreterLocatorService } from '../../contracts'; -import { IInterpreterVersionService } from '../../interpreterVersion'; +import { IInterpreterLocatorService, IInterpreterVersionService, IKnownSearchPathsForInterpreters, InterpreterType } from '../../contracts'; import { lookForInterpretersInDirectory } from '../helpers'; + // tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); +@injectable() export class KnownPathsService implements IInterpreterLocatorService { - public constructor(private knownSearchPaths: string[], - private versionProvider: IInterpreterVersionService) { } + public constructor( @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: string[], + @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { } // tslint:disable-next-line:no-shadowed-variable public getInterpreters(_?: Uri) { return this.suggestionsFromKnownPaths(); @@ -31,7 +33,8 @@ export class KnownPathsService implements IInterpreterLocatorService { .then(displayName => { return { displayName, - path: interpreter + path: interpreter, + type: InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts index 938e09a55fa5..23d7d87f1efc 100644 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ b/src/client/interpreter/locators/services/condaEnvFileService.ts @@ -1,15 +1,22 @@ -'use strict'; import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; +import 'reflect-metadata'; import { Uri } from 'vscode'; import { IS_WINDOWS } from '../../../common/configSettings'; -import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { IInterpreterVersionService } from '../../interpreterVersion'; +import { + ICondaEnvironmentFile, + IInterpreterLocatorService, + IInterpreterVersionService, + InterpreterType, + PythonInterpreter +} from '../../contracts'; import { AnacondaCompanyName, AnacondaCompanyNames, AnacondaDisplayName, CONDA_RELATIVE_PY_PATH } from './conda'; +@injectable() export class CondaEnvFileService implements IInterpreterLocatorService { - constructor(private condaEnvironmentFile: string, - private versionService: IInterpreterVersionService) { + constructor( @inject(ICondaEnvironmentFile) private condaEnvironmentFile: string, + @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) { } public async getInterpreters(_?: Uri) { return this.getSuggestionsFromConda(); @@ -46,7 +53,8 @@ export class CondaEnvFileService implements IInterpreterLocatorService { displayName: `${AnacondaDisplayName} ${version} (${envName})`, path: interpreter, companyDisplayName: AnacondaCompanyName, - version: version + version: version, + type: InterpreterType.Conda }; return info; }); diff --git a/src/client/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts index b78c055bcd21..2feefcf002ae 100644 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ b/src/client/interpreter/locators/services/condaEnvService.ts @@ -1,16 +1,19 @@ -'use strict'; import * as child_process from 'child_process'; import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; import * as path from 'path'; +import 'reflect-metadata'; import { Uri } from 'vscode'; import { VersionUtils } from '../../../common/versionUtils'; -import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { AnacondaCompanyName, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; +import { ICondaLocatorService, IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../contracts'; +import { AnacondaCompanyName, AnacondaCompanyNames, CONDA_RELATIVE_PY_PATH, CondaInfo } from './conda'; import { CondaHelper } from './condaHelper'; +@injectable() export class CondaEnvService implements IInterpreterLocatorService { private readonly condaHelper = new CondaHelper(); - constructor(private condaLocator: ICondaLocatorService) { + constructor( @inject(ICondaLocatorService) private condaLocator: ICondaLocatorService, + @inject(IInterpreterVersionService) private versionService: IInterpreterVersionService) { } public async getInterpreters(resource?: Uri) { return this.getSuggestionsFromConda(); @@ -30,7 +33,7 @@ export class CondaEnvService implements IInterpreterLocatorService { } } public async parseCondaInfo(info: CondaInfo) { - const displayName = this.condaHelper.getDisplayName(info); + const condaDisplayName = 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. @@ -40,24 +43,49 @@ export class CondaEnvService implements IInterpreterLocatorService { } const promises = envs - .map(env => { + .map(async env => { + const envName = path.basename(env); + const pythonPath = path.join(env, ...CONDA_RELATIVE_PY_PATH); + + const existsPromise = fs.pathExists(pythonPath); + const versionPromise = this.versionService.getVersion(pythonPath, envName); + + const [exists, version] = await Promise.all([existsPromise, versionPromise]); + if (!exists) { + return; + } + + const versionWithoutCompanyName = this.stripCompanyName(version); + const displayName = `${condaDisplayName} ${versionWithoutCompanyName}`.trim(); // If it is an environment, hence suffix with env name. - const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${path.basename(env)})`; + const interpreterDisplayName = env === info.default_prefix ? displayName : `${displayName} (${envName})`; // tslint:disable-next-line:no-unnecessary-local-variable const interpreter: PythonInterpreter = { - path: path.join(env, ...CONDA_RELATIVE_PY_PATH), + path: pythonPath, displayName: interpreterDisplayName, - companyDisplayName: AnacondaCompanyName + companyDisplayName: AnacondaCompanyName, + type: InterpreterType.Conda, + envName }; return interpreter; - }) - .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 stripCompanyName(content: string) { + // Strip company name from version. + const startOfCompanyName = AnacondaCompanyNames.reduce((index, companyName) => { + if (index > 0) { + return index; + } + return content.indexOf(`:: ${companyName}`); + }, -1); + + return startOfCompanyName > 0 ? content.substring(0, startOfCompanyName).trim() : content; + } private async getSuggestionsFromConda(): Promise { return this.condaLocator.getCondaFile() .then(async condaFile => { diff --git a/src/client/interpreter/locators/services/condaHelper.ts b/src/client/interpreter/locators/services/condaHelper.ts index ea8276c6392f..52ea0cbecd15 100644 --- a/src/client/interpreter/locators/services/condaHelper.ts +++ b/src/client/interpreter/locators/services/condaHelper.ts @@ -2,29 +2,22 @@ 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; + return 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(); - } + // Take the second part of the sys.version. + const sysVersionParts = sysVersion.split('|', 2); + if (sysVersionParts.length === 2) { + const displayName = sysVersionParts[1].trim(); + return this.isIdentifiableAsAnaconda(displayName) ? displayName : `${displayName} : ${AnacondaDisplayName}`; + } else { + return AnacondaDisplayName; } - - const displayName = sysVersionParts.map(item => item.trim()).join(' : '); - return this.isIdentifiableAsAnaconda(displayName) ? displayName : `${displayName} : ${AnacondaDisplayName}`; } private isIdentifiableAsAnaconda(value: string) { const valueToSearch = value.toLowerCase(); diff --git a/src/client/interpreter/locators/services/condaLocator.ts b/src/client/interpreter/locators/services/condaLocator.ts index ff1db7269c50..759283d4909e 100644 --- a/src/client/interpreter/locators/services/condaLocator.ts +++ b/src/client/interpreter/locators/services/condaLocator.ts @@ -1,10 +1,13 @@ -'use strict'; import * as child_process from 'child_process'; import * as fs from 'fs-extra'; +import { inject, injectable, named, optional } from 'inversify'; import * as path from 'path'; -import { IS_WINDOWS } from '../../../common/utils'; +import 'reflect-metadata'; +import { IProcessService } from '../../../common/process/types'; +import { IsWindows } from '../../../common/types'; import { VersionUtils } from '../../../common/versionUtils'; -import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { ICondaLocatorService, IInterpreterLocatorService, PythonInterpreter, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; + // tslint:disable-next-line:no-require-imports no-var-requires const untildify: (value: string) => string = require('untildify'); @@ -12,12 +15,20 @@ const KNOWN_CONDA_LOCATIONS = ['~/anaconda/bin/conda', '~/miniconda/bin/conda', '~/anaconda2/bin/conda', '~/miniconda2/bin/conda', '~/anaconda3/bin/conda', '~/miniconda3/bin/conda']; +@injectable() export class CondaLocatorService implements ICondaLocatorService { - constructor(private isWindows: boolean, private registryLookupForConda?: IInterpreterLocatorService) { + private condaFile: string | undefined; + private isAvailable: boolean | undefined; + constructor( @inject(IsWindows) private isWindows: boolean, + @inject(IProcessService) private processService: IProcessService, + @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService) { } // tslint:disable-next-line:no-empty public dispose() { } public async getCondaFile(): Promise { + if (this.condaFile) { + return this.condaFile!; + } const isAvailable = await this.isCondaInCurrentPath(); if (isAvailable) { return 'conda'; @@ -33,7 +44,19 @@ export class CondaLocatorService implements ICondaLocatorService { return fs.pathExists(condaPath).then(exists => exists ? condaPath : 'conda'); }); } - return this.getCondaFileFromKnownLocations(); + this.condaFile = await this.getCondaFileFromKnownLocations(); + return this.condaFile!; + } + public async isCondaAvailable(): Promise { + return this.getCondaVersion() + .then(() => this.isAvailable = true) + .catch(() => this.isAvailable = false); + } + public async getCondaVersion(): Promise { + return this.getCondaFile() + .then(condaFile => this.processService.exec(condaFile, ['--version'], {})) + .then(result => result.stdout.trim()) + .catch(() => undefined); } public isCondaEnvironment(interpreter: PythonInterpreter) { return (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index c404d9be60f9..0f3ee31758c3 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -1,17 +1,18 @@ -'use strict'; import * as child_process from 'child_process'; +import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; +import 'reflect-metadata'; import { Uri } from 'vscode'; import { PythonSettings } from '../../../common/configSettings'; -import { IInterpreterLocatorService } from '../../contracts'; +import { IInterpreterLocatorService, IInterpreterVersionService, InterpreterType } from '../../contracts'; import { getFirstNonEmptyLineFromMultilineString } from '../../helpers'; -import { IInterpreterVersionService } from '../../interpreterVersion'; -import { VirtualEnvironmentManager } from '../../virtualEnvs'; +import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; +@injectable() export class CurrentPathService implements IInterpreterLocatorService { - public constructor(private virtualEnvMgr: VirtualEnvironmentManager, - private versionProvider: IInterpreterVersionService) { } + public constructor( @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, + @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { } public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownPaths(); } @@ -38,7 +39,8 @@ export class CurrentPathService implements IInterpreterLocatorService { displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; return { displayName, - path: interpreter + path: interpreter, + type: InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/locators/services/virtualEnvService.ts b/src/client/interpreter/locators/services/virtualEnvService.ts index 46085c55376b..070bf673d6cd 100644 --- a/src/client/interpreter/locators/services/virtualEnvService.ts +++ b/src/client/interpreter/locators/services/virtualEnvService.ts @@ -1,20 +1,22 @@ -'use strict'; +import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; +import 'reflect-metadata'; import { Uri, workspace } from 'vscode'; import { fsReaddirAsync, IS_WINDOWS } from '../../../common/utils'; -import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; -import { IInterpreterVersionService } from '../../interpreterVersion'; -import { VirtualEnvironmentManager } from '../../virtualEnvs'; +import { IInterpreterLocatorService, IInterpreterVersionService, IKnownSearchPathsForVirtualEnvironments, InterpreterType, PythonInterpreter } from '../../contracts'; +import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { lookForInterpretersInDirectory } from '../helpers'; import * as settings from './../../../common/configSettings'; + // tslint:disable-next-line:no-require-imports no-var-requires const untildify = require('untildify'); +@injectable() export class VirtualEnvService implements IInterpreterLocatorService { - public constructor(private knownSearchPaths: string[], - private virtualEnvMgr: VirtualEnvironmentManager, - private versionProvider: IInterpreterVersionService) { } + public constructor( @inject(IKnownSearchPathsForVirtualEnvironments) private knownSearchPaths: string[], + @inject(IVirtualEnvironmentManager) private virtualEnvMgr: IVirtualEnvironmentManager, + @inject(IInterpreterVersionService) private versionProvider: IInterpreterVersionService) { } public async getInterpreters(resource?: Uri) { return this.suggestionsFromKnownVenvs(); } @@ -65,7 +67,8 @@ export class VirtualEnvService implements IInterpreterLocatorService { const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); return { displayName: `${displayName} (${virtualEnvSuffix})`.trim(), - path: interpreter + path: interpreter, + type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts index aa74aefd2606..5daafad296b6 100644 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ b/src/client/interpreter/locators/services/windowsRegistryService.ts @@ -1,9 +1,11 @@ import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; import * as _ from 'lodash'; import * as path from 'path'; import { Uri } from 'vscode'; -import { Architecture, Hive, IRegistry } from '../../../common/platform/registry'; -import { IInterpreterLocatorService, PythonInterpreter } from '../../contracts'; +import { Architecture, IRegistry, RegistryHive } from '../../../common/platform/types'; +import { Is64Bit } from '../../../common/types'; +import { IInterpreterLocatorService, InterpreterType, PythonInterpreter } from '../../contracts'; // tslint:disable-next-line:variable-name const DefaultPythonExecutable = 'python.exe'; @@ -16,12 +18,13 @@ const PythonCoreComany = 'PYTHONCORE'; type CompanyInterpreter = { companyKey: string, - hive: Hive, + hive: RegistryHive, arch?: Architecture }; +@injectable() export class WindowsRegistryService implements IInterpreterLocatorService { - constructor(private registry: IRegistry, private is64Bit: boolean) { + constructor( @inject(IRegistry) private registry: IRegistry, @inject(Is64Bit) private is64Bit: boolean) { } // tslint:disable-next-line:variable-name @@ -34,12 +37,12 @@ export class WindowsRegistryService implements IInterpreterLocatorService { // https://github.com/python/peps/blob/master/pep-0514.txt#L357 const hkcuArch = this.is64Bit ? undefined : Architecture.x86; const promises: Promise[] = [ - this.getCompanies(Hive.HKCU, hkcuArch), - this.getCompanies(Hive.HKLM, Architecture.x86) + this.getCompanies(RegistryHive.HKCU, hkcuArch), + this.getCompanies(RegistryHive.HKLM, Architecture.x86) ]; // https://github.com/Microsoft/PTVS/blob/ebfc4ca8bab234d453f15ee426af3b208f3c143c/Python/Product/Cookiecutter/Shared/Interpreters/PythonRegistrySearch.cs#L44 if (this.is64Bit) { - promises.push(this.getCompanies(Hive.HKLM, Architecture.x64)); + promises.push(this.getCompanies(RegistryHive.HKLM, Architecture.x64)); } const companies = await Promise.all(promises); @@ -62,7 +65,7 @@ export class WindowsRegistryService implements IInterpreterLocatorService { return prev; }, []); } - private async getCompanies(hive: Hive, arch?: Architecture): Promise { + private async getCompanies(hive: RegistryHive, arch?: Architecture): Promise { return this.registry.getKeys('\\Software\\Python', hive, arch) .then(companyKeys => companyKeys .filter(companyKey => CompaniesToIgnore.indexOf(path.basename(companyKey).toUpperCase()) === -1) @@ -70,11 +73,11 @@ export class WindowsRegistryService implements IInterpreterLocatorService { return { companyKey, hive, arch }; })); } - private async getInterpretersForCompany(companyKey: string, hive: Hive, arch?: Architecture) { + private async getInterpretersForCompany(companyKey: string, hive: RegistryHive, arch?: Architecture) { const tagKeys = await this.registry.getKeys(companyKey, hive, arch); return Promise.all(tagKeys.map(tagKey => this.getInreterpreterDetailsForCompany(tagKey, companyKey, hive, arch))); } - private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: Hive, arch?: Architecture): Promise { + private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: RegistryHive, arch?: Architecture): Promise { const key = `${tagKey}\\InstallPath`; type InterpreterInformation = null | undefined | { installPath: string, @@ -119,7 +122,8 @@ export class WindowsRegistryService implements IInterpreterLocatorService { displayName, path: executablePath, version, - companyDisplayName: interpreterInfo.companyDisplayName + companyDisplayName: interpreterInfo.companyDisplayName, + type: InterpreterType.Unknown } as PythonInterpreter; }) .then(interpreter => interpreter ? fs.pathExists(interpreter.path).catch(() => false).then(exists => exists ? interpreter : null) : null) @@ -129,13 +133,13 @@ export class WindowsRegistryService implements IInterpreterLocatorService { return null; }); } - private async getInterpreterDisplayName(tagKey: string, companyKey: string, hive: Hive, arch?: Architecture) { + private async getInterpreterDisplayName(tagKey: string, companyKey: string, hive: RegistryHive, arch?: Architecture) { const displayName = await this.registry.getValue(tagKey, hive, arch, 'DisplayName'); if (displayName && displayName.length > 0) { return displayName; } } - private async getCompanyDisplayName(companyKey: string, hive: Hive, arch?: Architecture) { + private async getCompanyDisplayName(companyKey: string, hive: RegistryHive, arch?: Architecture) { const displayName = await this.registry.getValue(companyKey, hive, arch, 'DisplayName'); if (displayName && displayName.length > 0) { return displayName; diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts new file mode 100644 index 000000000000..56ade6625d40 --- /dev/null +++ b/src/client/interpreter/serviceRegistry.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import 'reflect-metadata'; +import { IsWindows } from '../common/types'; +import { IServiceManager } from '../ioc/types'; +import { + CONDA_ENV_FILE_SERVICE, + CONDA_ENV_SERVICE, + CURRENT_PATH_SERVICE, + ICondaEnvironmentFile, + ICondaLocatorService, + IInterpreterLocatorService, + IInterpreterVersionService, + IKnownSearchPathsForInterpreters, + IKnownSearchPathsForVirtualEnvironments, + INTERPRETER_LOCATOR_SERVICE, + KNOWN_PATH_SERVICE, + VIRTUAL_ENV_SERVICE, + WINDOWS_REGISTRY_SERVICE +} from './contracts'; +import { InterpreterVersionService } from './interpreterVersion'; +import { PythonInterpreterLocatorService } from './locators/index'; +import { CondaEnvFileService, getEnvironmentsFile } from './locators/services/condaEnvFileService'; +import { CondaEnvService } from './locators/services/condaEnvService'; +import { CondaLocatorService } from './locators/services/condaLocator'; +import { CurrentPathService } from './locators/services/currentPathService'; +import { getKnownSearchPathsForInterpreters, KnownPathsService } from './locators/services/KnownPathsService'; +import { getKnownSearchPathsForVirtualEnvs, VirtualEnvService } from './locators/services/virtualEnvService'; +import { WindowsRegistryService } from './locators/services/windowsRegistryService'; +import { VirtualEnvironmentManager } from './virtualEnvs/index'; +import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './virtualEnvs/types'; +import { VEnv } from './virtualEnvs/venv'; +import { VirtualEnv } from './virtualEnvs/virtualEnv'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingletonInstance(ICondaEnvironmentFile, getEnvironmentsFile()); + serviceManager.addSingletonInstance(IKnownSearchPathsForInterpreters, getKnownSearchPathsForInterpreters()); + serviceManager.addSingletonInstance(IKnownSearchPathsForVirtualEnvironments, getKnownSearchPathsForVirtualEnvs()); + + serviceManager.addSingleton(ICondaLocatorService, CondaLocatorService); + serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VirtualEnv); + serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VEnv); + + serviceManager.addSingleton(IVirtualEnvironmentManager, VirtualEnvironmentManager); + + serviceManager.addSingleton(IInterpreterVersionService, InterpreterVersionService); + serviceManager.addSingleton(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); + serviceManager.addSingleton(IInterpreterLocatorService, VirtualEnvService, VIRTUAL_ENV_SERVICE); + + const isWindows = serviceManager.get(IsWindows); + if (isWindows) { + serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); + } else { + serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); + } +} diff --git a/src/client/interpreter/virtualEnvs/contracts.ts b/src/client/interpreter/virtualEnvs/contracts.ts deleted file mode 100644 index 07298ffc1f1a..000000000000 --- a/src/client/interpreter/virtualEnvs/contracts.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IVirtualEnvironment { - detect(pythonPath: string): Promise; - readonly name: string; -} diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index 128881a4e043..d0a9bd1634f6 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -1,9 +1,11 @@ -import { IVirtualEnvironment } from './contracts'; +import { injectable, multiInject } from 'inversify'; +import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './types'; -export class VirtualEnvironmentManager { - constructor(private envs: IVirtualEnvironment[]) { +@injectable() +export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { + constructor( @multiInject(IVirtualEnvironmentIdentifier) private envs: IVirtualEnvironmentIdentifier[]) { } - public detect(pythonPath: string): Promise { + public detect(pythonPath: string): Promise { const promises = this.envs .map(item => item.detect(pythonPath) .then(result => { diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts new file mode 100644 index 000000000000..710507358999 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { InterpreterType } from '../contracts'; +export const IVirtualEnvironmentIdentifier = Symbol('IVirtualEnvironment'); + +export interface IVirtualEnvironmentIdentifier { + readonly name: string; + readonly type: InterpreterType.VEnv | InterpreterType.VirtualEnv; + detect(pythonPath: string): Promise; +} +export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); +export interface IVirtualEnvironmentManager { + detect(pythonPath: string): Promise; +} diff --git a/src/client/interpreter/virtualEnvs/venv.ts b/src/client/interpreter/virtualEnvs/venv.ts index da65ec561a87..3659abc3a9ee 100644 --- a/src/client/interpreter/virtualEnvs/venv.ts +++ b/src/client/interpreter/virtualEnvs/venv.ts @@ -1,15 +1,19 @@ -import { IVirtualEnvironment } from "./contracts"; +import { injectable } from 'inversify'; import * as path from 'path'; +import 'reflect-metadata'; import { fsExistsAsync } from '../../common/utils'; +import { InterpreterType } from '../contracts'; +import { IVirtualEnvironmentIdentifier } from './types'; const pyEnvCfgFileName = 'pyvenv.cfg'; -export class VEnv implements IVirtualEnvironment { +@injectable() +export class VEnv implements IVirtualEnvironmentIdentifier { public readonly name: string = 'venv'; - - detect(pythonPath: string): Promise { + public readonly type = InterpreterType.VEnv; + public detect(pythonPath: string): Promise { const dir = path.dirname(pythonPath); const pyEnvCfgPath = path.join(dir, '..', pyEnvCfgFileName); return fsExistsAsync(pyEnvCfgPath); } -} \ No newline at end of file +} diff --git a/src/client/interpreter/virtualEnvs/virtualEnv.ts b/src/client/interpreter/virtualEnvs/virtualEnv.ts index 9000d576ec5f..193f6072a2dc 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnv.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnv.ts @@ -1,15 +1,19 @@ -import { IVirtualEnvironment } from "./contracts"; +import { injectable } from 'inversify'; import * as path from 'path'; +import 'reflect-metadata'; import { fsExistsAsync } from '../../common/utils'; +import { InterpreterType } from '../contracts'; +import { IVirtualEnvironmentIdentifier } from './types'; const OrigPrefixFile = 'orig-prefix.txt'; -export class VirtualEnv implements IVirtualEnvironment { +@injectable() +export class VirtualEnv implements IVirtualEnvironmentIdentifier { public readonly name: string = 'virtualenv'; - - detect(pythonPath: string): Promise { + public readonly type = InterpreterType.VirtualEnv; + public detect(pythonPath: string): Promise { const dir = path.dirname(pythonPath); const origPrefixFile = path.join(dir, '..', 'lib', OrigPrefixFile); return fsExistsAsync(origPrefixFile); } -} \ No newline at end of file +} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index 04b26d4e6f5a..217bda29617e 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,19 +1,17 @@ import * as path from 'path'; -import * as vscode from 'vscode'; import { CancellationToken, OutputChannel, TextDocument, Uri } from 'vscode'; +import * as vscode from 'vscode'; import { IPythonSettings, PythonSettings } from '../common/configSettings'; import '../common/extensions'; -import { Product } from '../common/installer'; import { ExecutionResult, IProcessService, IPythonExecutionFactory } from '../common/process/types'; -import { ExecutionInfo, IInstaller, ILogger } from '../common/types'; +import { ExecutionInfo, IInstaller, ILogger, Product } from '../common/types'; import { IEnvironmentVariablesProvider } from '../common/variables/types'; import { IServiceContainer } from '../ioc/types'; -import { execPythonFile } from './../common/utils'; import { ErrorHandler } from './errorHandlers/main'; import { ILinterHelper, LinterId } from './types'; +// tslint:disable-next-line:no-require-imports no-var-requires +const namedRegexp = require('named-js-regexp'); -// tslint:disable-next-line:variable-name -let NamedRegexp = null; const REGEX = '(?\\d+),(?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; export interface IRegexGroup { @@ -40,19 +38,14 @@ export enum LintMessageSeverity { Information } -export function matchNamedRegEx(data, regex): IRegexGroup { - if (NamedRegexp === null) { - // tslint:disable-next-line:no-require-imports - NamedRegexp = require('named-js-regexp'); - } - - const compiledRegexp = NamedRegexp(regex, 'g'); +export function matchNamedRegEx(data, regex): IRegexGroup | undefined { + const compiledRegexp = namedRegexp(regex, 'g'); const rawMatch = compiledRegexp.exec(data); if (rawMatch !== null) { return rawMatch.groups(); } - return null; + return undefined; } export abstract class BaseLinter { public Id: LinterId; @@ -133,7 +126,7 @@ export abstract class BaseLinter { } else { const env = await this.serviceContainer.get(IEnvironmentVariablesProvider).getEnvironmentVariables(true, document.uri); const executionService = this.serviceContainer.get(IProcessService); - executionPromise = executionService.exec(executionInfo.execPath, args, { cwd, env, token: cancellation, mergeStdOutErr: true }); + executionPromise = executionService.exec(executionInfo.execPath!, args, { cwd, env, token: cancellation, mergeStdOutErr: true }); } try { const result = await executionPromise; @@ -154,7 +147,7 @@ export abstract class BaseLinter { } private parseLine(line: string, regEx: string): ILintMessage | undefined { - const match = matchNamedRegEx(line, regEx); + const match = matchNamedRegEx(line, regEx)!; if (!match) { return; } diff --git a/src/client/linters/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts index 134f41c6c542..eace0002477b 100644 --- a/src/client/linters/errorHandlers/baseErrorHandler.ts +++ b/src/client/linters/errorHandlers/baseErrorHandler.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { OutputChannel, Uri } from 'vscode'; -import { Product } from '../../common/installer'; -import { ExecutionInfo, IInstaller, ILogger } from '../../common/types'; +import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IErrorHandler, ILinterHelper } from '../types'; diff --git a/src/client/linters/errorHandlers/main.ts b/src/client/linters/errorHandlers/main.ts index 8e37acc2fc1a..abf41ba1e1cd 100644 --- a/src/client/linters/errorHandlers/main.ts +++ b/src/client/linters/errorHandlers/main.ts @@ -1,6 +1,5 @@ import { OutputChannel, Uri } from 'vscode'; -import { Product } from '../../common/installer'; -import { ExecutionInfo, IInstaller, ILogger } from '../../common/types'; +import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { IErrorHandler, ILinterHelper } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; @@ -13,9 +12,9 @@ export class ErrorHandler implements IErrorHandler { helper: ILinterHelper, logger: ILogger, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { // Create chain of handlers. - const moduleNotInstalledErrorHandler = new ModuleNotInstalledErrorHandler(product, installer, helper, logger, outputChannel, serviceContainer); - this.handler = new StandardErrorHandler(product, installer, helper, logger, outputChannel, serviceContainer); - this.handler.setNextHandler(moduleNotInstalledErrorHandler); + const standardErrorHandler = new StandardErrorHandler(product, installer, helper, logger, outputChannel, serviceContainer); + this.handler = new ModuleNotInstalledErrorHandler(product, installer, helper, logger, outputChannel, serviceContainer); + this.handler.setNextHandler(standardErrorHandler); } public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts index 1aa745cebc61..50f8686a4fc3 100644 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ b/src/client/linters/errorHandlers/notInstalled.ts @@ -1,8 +1,7 @@ import { OutputChannel, Uri } from 'vscode'; import { isNotInstalledError } from '../../common/helpers'; -import { Product } from '../../common/installer'; import { IPythonExecutionFactory } from '../../common/process/types'; -import { ExecutionInfo, IInstaller, ILogger } from '../../common/types'; +import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { ILinterHelper } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; @@ -20,7 +19,7 @@ export class ModuleNotInstalledErrorHandler extends BaseErrorHandler { const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create(resource); const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (!isModuleInstalled) { + if (isModuleInstalled) { return this.nextHandler ? await this.nextHandler.handleError(error, resource, execInfo) : false; } diff --git a/src/client/linters/errorHandlers/standard.ts b/src/client/linters/errorHandlers/standard.ts index 8250a5979512..15907311137c 100644 --- a/src/client/linters/errorHandlers/standard.ts +++ b/src/client/linters/errorHandlers/standard.ts @@ -1,6 +1,5 @@ import { OutputChannel, Uri, window } from 'vscode'; -import { Product } from '../../common/installer'; -import { ExecutionInfo, IInstaller, ILogger } from '../../common/types'; +import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { ILinterHelper, LinterId } from '../types'; import { BaseErrorHandler } from './baseErrorHandler'; diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts index b13c58ae4604..1edd736872d8 100644 --- a/src/client/linters/flake8.ts +++ b/src/client/linters/flake8.ts @@ -1,7 +1,6 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/helper.ts b/src/client/linters/helper.ts index 01a3fb207107..0c94cba4787d 100644 --- a/src/client/linters/helper.ts +++ b/src/client/linters/helper.ts @@ -1,10 +1,9 @@ import { injectable } from 'inversify'; import * as path from 'path'; import 'reflect-metadata'; -import { Uri, workspace } from 'vscode'; +import { Uri } from 'vscode'; import { ILintingSettings, PythonSettings } from '../common/configSettings'; -import { Product, ProductExecutableAndArgs } from '../common/installer'; -import { ExecutionInfo } from '../common/types'; +import { ExecutionInfo, Product } from '../common/types'; import { ILinterHelper, LinterId, LinterSettingsPropertyNames } from './types'; @injectable() @@ -22,7 +21,6 @@ export class LinterHelper implements ILinterHelper { this.linterIdMapping.set(Product.pylint, 'pylint'); } public getExecutionInfo(linter: Product, customArgs: string[], resource?: Uri): ExecutionInfo { - const id = this.translateToId(linter); const settings = PythonSettings.getInstance(resource); const names = this.getSettingsPropertyNames(linter); diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts index ecfc9dcf2127..53d32f25f3e9 100644 --- a/src/client/linters/mypy.ts +++ b/src/client/linters/mypy.ts @@ -1,7 +1,6 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/pep8Linter.ts b/src/client/linters/pep8Linter.ts index 034caead57b4..921452436711 100644 --- a/src/client/linters/pep8Linter.ts +++ b/src/client/linters/pep8Linter.ts @@ -1,7 +1,6 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts index c7db500e568a..bfa2267d78e3 100644 --- a/src/client/linters/prospector.ts +++ b/src/client/linters/prospector.ts @@ -1,9 +1,7 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product, ProductExecutableAndArgs } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { execPythonFile } from './../common/utils'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/pydocstyle.ts b/src/client/linters/pydocstyle.ts index 4d4d88b6bb44..09116b8aa2b5 100644 --- a/src/client/linters/pydocstyle.ts +++ b/src/client/linters/pydocstyle.ts @@ -1,8 +1,7 @@ import * as path from 'path'; import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { IS_WINDOWS } from './../common/utils'; import * as baseLinter from './baseLinter'; diff --git a/src/client/linters/pylama.ts b/src/client/linters/pylama.ts index b2bef08d7aa5..55524f3d161b 100644 --- a/src/client/linters/pylama.ts +++ b/src/client/linters/pylama.ts @@ -1,7 +1,6 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts index b4122c46b177..099315a5e460 100644 --- a/src/client/linters/pylint.ts +++ b/src/client/linters/pylint.ts @@ -1,7 +1,6 @@ import { OutputChannel } from 'vscode'; import { CancellationToken, TextDocument } from 'vscode'; -import { Product } from '../common/installer'; -import { IInstaller, ILogger } from '../common/types'; +import { IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import * as baseLinter from './baseLinter'; import { ILinterHelper } from './types'; diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts index b110d0d63f8b..2d99fefeb194 100644 --- a/src/client/linters/types.ts +++ b/src/client/linters/types.ts @@ -3,8 +3,7 @@ import { Uri } from 'vscode'; import { ILintingSettings } from '../common/configSettings'; -import { Product } from '../common/installer'; -import { ExecutionInfo } from '../common/types'; +import { ExecutionInfo, Product } from '../common/types'; export interface IErrorHandler { handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; diff --git a/src/client/providers/formatProvider.ts b/src/client/providers/formatProvider.ts index 528e3d8f69d7..f905f06f5c2f 100644 --- a/src/client/providers/formatProvider.ts +++ b/src/client/providers/formatProvider.ts @@ -1,6 +1,5 @@ -'use strict'; - import * as vscode from 'vscode'; +import { IServiceContainer } from '../ioc/types'; import { PythonSettings } from './../common/configSettings'; import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; import { BaseFormatter } from './../formatters/baseFormatter'; @@ -10,22 +9,22 @@ import { YapfFormatter } from './../formatters/yapfFormatter'; export class PythonFormattingEditProvider implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider { private formatters = new Map(); - public constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { - const yapfFormatter = new YapfFormatter(outputChannel); - const autoPep8 = new AutoPep8Formatter(outputChannel); - const dummy = new DummyFormatter(outputChannel); + public constructor(context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { + const yapfFormatter = new YapfFormatter(serviceContainer); + const autoPep8 = new AutoPep8Formatter(serviceContainer); + const dummy = new DummyFormatter(serviceContainer); this.formatters.set(yapfFormatter.Id, yapfFormatter); this.formatters.set(autoPep8.Id, autoPep8); this.formatters.set(dummy.Id, dummy); } public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { - return this.provideDocumentRangeFormattingEdits(document, null, options, token); + return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); } - public provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { + public provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range | undefined, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { const settings = PythonSettings.getInstance(document.uri); - const formatter = this.formatters.get(settings.formatting.provider); + const formatter = this.formatters.get(settings.formatting.provider)!; return formatter.formatDocument(document, options, token, range); } diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts index 1997a65f0e9d..f8e5a148fbe0 100644 --- a/src/client/providers/renameProvider.ts +++ b/src/client/providers/renameProvider.ts @@ -1,32 +1,33 @@ -'use strict'; - import * as path from 'path'; import * as vscode from 'vscode'; +import { OutputChannel, ProviderResult } from 'vscode'; import { PythonSettings } from '../common/configSettings'; +import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { getWorkspaceEditsFromPatch } from '../common/editor'; -import { Installer, Product } from '../common/installer'; +import { IInstaller, IOutputChannel, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; import { RefactorProxy } from '../refactor/proxy'; import { captureTelemetry } from '../telemetry'; import { REFACTOR_RENAME } from '../telemetry/constants'; const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); -interface RenameResponse { +type RenameResponse = { results: [{ diff: string }]; -} +}; export class PythonRenameProvider implements vscode.RenameProvider { - private installer: Installer; - constructor(private outputChannel: vscode.OutputChannel) { - this.installer = new Installer(outputChannel); + private readonly outputChannel: OutputChannel; + constructor(private serviceContainer: IServiceContainer) { + this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); } @captureTelemetry(REFACTOR_RENAME) - public provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { + public provideRenameEdits(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): ProviderResult { return vscode.workspace.saveAll(false).then(() => { return this.doRename(document, position, newName, token); }); } - private doRename(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Thenable { + private doRename(document: vscode.TextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): ProviderResult { if (document.lineAt(position.line).text.match(/^\s*\/\//)) { return; } @@ -56,7 +57,8 @@ export class PythonRenameProvider implements vscode.RenameProvider { return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); }).catch(reason => { if (reason === 'Not installed') { - this.installer.promptToInstall(Product.rope, document.uri) + const installer = this.serviceContainer.get(IInstaller); + installer.promptToInstall(Product.rope, document.uri) .catch(ex => console.error('Python Extension: promptToInstall', ex)); return Promise.reject(''); } else { diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts index 588b3cb06dea..d583ef5bf298 100644 --- a/src/client/providers/simpleRefactorProvider.ts +++ b/src/client/providers/simpleRefactorProvider.ts @@ -1,26 +1,26 @@ -'use strict'; - import * as vscode from 'vscode'; import { PythonSettings } from '../common/configSettings'; import { getTextEditsFromPatch } from '../common/editor'; -import { Installer, Product } from '../common/installer'; +import { IInstaller, Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; import { RefactorProxy } from '../refactor/proxy'; import { sendTelemetryWhenDone } from '../telemetry'; import { REFACTOR_EXTRACT_FUNCTION, REFACTOR_EXTRACT_VAR } from '../telemetry/constants'; import { StopWatch } from '../telemetry/stopWatch'; -interface RenameResponse { +type RenameResponse = { results: [{ diff: string }]; -} +}; -let installer: Installer; +let installer: IInstaller; -export function activateSimplePythonRefactorProvider(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) { +export function activateSimplePythonRefactorProvider(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer) { + installer = serviceContainer.get(IInstaller); let disposable = vscode.commands.registerCommand('python.refactorExtractVariable', () => { const stopWatch = new StopWatch(); const promise = extractVariable(context.extensionPath, - vscode.window.activeTextEditor, - vscode.window.activeTextEditor.selection, + vscode.window.activeTextEditor!, + vscode.window.activeTextEditor!.selection, // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); sendTelemetryWhenDone(REFACTOR_EXTRACT_VAR, promise, stopWatch); @@ -30,15 +30,13 @@ export function activateSimplePythonRefactorProvider(context: vscode.ExtensionCo disposable = vscode.commands.registerCommand('python.refactorExtractMethod', () => { const stopWatch = new StopWatch(); const promise = extractMethod(context.extensionPath, - vscode.window.activeTextEditor, - vscode.window.activeTextEditor.selection, + vscode.window.activeTextEditor!, + vscode.window.activeTextEditor!.selection, // tslint:disable-next-line:no-empty outputChannel).catch(() => { }); sendTelemetryWhenDone(REFACTOR_EXTRACT_FUNCTION, promise, stopWatch); }); context.subscriptions.push(disposable); - installer = new Installer(outputChannel); - context.subscriptions.push(installer); } // Exported for unit testing @@ -125,8 +123,8 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: }); }).then(done => { if (done && changeStartsAtLine >= 0) { - let newWordPosition: vscode.Position; - for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber++) { + let newWordPosition: vscode.Position | undefined; + for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber += 1) { const line = textEditor.document.lineAt(lineNumber); const indexOfWord = line.text.indexOf(newName); if (indexOfWord >= 0) { @@ -155,15 +153,15 @@ function extractName(extensionDir: string, textEditor: vscode.TextEditor, range: .catch(ex => console.error('Python Extension: simpleRefactorProvider.promptToInstall', ex)); return Promise.reject(''); } - let errorMessage = error + ''; + let errorMessage = `${error}`; if (typeof error === 'string') { errorMessage = error; } if (typeof error === 'object' && error.message) { errorMessage = error.message; } - outputChannel.appendLine('#'.repeat(10) + 'Refactor Output' + '#'.repeat(10)); - outputChannel.appendLine('Error in refactoring:\n' + errorMessage); + outputChannel.appendLine(`${'#'.repeat(10)}Refactor Output${'#'.repeat(10)}`); + outputChannel.appendLine(`Error in refactoring:\n${errorMessage}`); vscode.window.showErrorMessage(`Cannot perform refactoring using selected element(s). (${errorMessage})`); return Promise.reject(error); }); diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 2271ef930f8b..9f92173db7dc 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -15,6 +15,9 @@ export function sendTelemetryEvent(eventName: string, durationMs?: number, prope // tslint:disable-next-line:prefer-type-cast no-any const data = properties as any; Object.getOwnPropertyNames(data).forEach(prop => { + if (data[prop] === undefined || data[prop] === null) { + return; + } // tslint:disable-next-line:prefer-type-cast no-any no-unsafe-any (customProperties as any)[prop] = typeof data[prop] === 'string' ? data[prop] : data[prop].toString(); }); diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts index 3b5ad926e205..9e727561f9ac 100644 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ b/src/client/unittests/common/managers/baseTestManager.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; import { IPythonSettings, PythonSettings } from '../../../common/configSettings'; import { isNotInstalledError } from '../../../common/helpers'; -import { IDiposableRegistry, IInstaller, IOutputChannel, Product } from '../../../common/types'; +import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; import { sendTelemetryEvent } from '../../../telemetry/index'; @@ -19,6 +19,7 @@ enum CancellationTokenType { export abstract class BaseTestManager implements ITestManager { protected readonly settings: IPythonSettings; + public abstract get enabled(): boolean; protected get outputChannel() { return this._outputChannel; } @@ -45,7 +46,7 @@ export abstract class BaseTestManager implements ITestManager { protected serviceContainer: IServiceContainer) { this._status = TestStatus.Unknown; this.settings = PythonSettings.getInstance(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); - const disposables = serviceContainer.get(IDiposableRegistry); + const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); this._outputChannel = this.serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); this.testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 554eeaca3b97..9d7faf67bab7 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,7 +1,15 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; import { IPythonSettings, PythonSettings } from '../../common/configSettings'; -import { IProcessService, IPythonExecutionFactory, ObservableExecutionResult, SpawnOptions } from '../../common/process/types'; +import { ErrorUtils } from '../../common/errors/errorUtils'; +import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; +import { + IProcessService, + IPythonExecutionFactory, + IPythonExecutionService, + ObservableExecutionResult, + SpawnOptions +} from '../../common/process/types'; import { IEnvironmentVariablesProvider } from '../../common/variables/types'; import { IServiceContainer } from '../../ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; @@ -19,6 +27,7 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes const testExecutablePath = getExecutablePath(testProvider, PythonSettings.getInstance(options.workspaceFolder)); const moduleName = getTestModuleName(testProvider); const spawnOptions = options as SpawnOptions; + let pythonExecutionServicePromise: Promise; spawnOptions.mergeStdOutErr = typeof spawnOptions.mergeStdOutErr === 'boolean' ? spawnOptions.mergeStdOutErr : true; let promise: Promise>; @@ -26,8 +35,8 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes if (!testExecutablePath && testProvider === UNITTEST_PROVIDER) { // Unit tests have a special way of being executed const pythonServiceFactory = serviceContainer.get(IPythonExecutionFactory); - const pythonExecutionService = pythonServiceFactory.create(options.workspaceFolder); - promise = pythonExecutionService.then(executionService => { + pythonExecutionServicePromise = pythonServiceFactory.create(options.workspaceFolder); + promise = pythonExecutionServicePromise.then(executionService => { return executionService.execObservable(options.args, { ...spawnOptions }); }); } else if (testExecutablePath) { @@ -38,8 +47,8 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes }); } else { const pythonServiceFactory = serviceContainer.get(IPythonExecutionFactory); - const pythonExecutionService = pythonServiceFactory.create(options.workspaceFolder); - promise = pythonExecutionService.then(executionService => { + pythonExecutionServicePromise = pythonServiceFactory.create(options.workspaceFolder); + promise = pythonExecutionServicePromise.then(executionService => { return executionService.execModuleObservable(moduleName, options.args, { ...spawnOptions }); }); } @@ -47,12 +56,28 @@ export async function run(serviceContainer: IServiceContainer, testProvider: Tes return promise.then(result => { return new Promise((resolve, reject) => { let stdOut = ''; + let stdErr = ''; result.out.subscribe(output => { stdOut += output.out; + // If the test runner python module is not installed we'll have something in stderr. + // Hence track that separately and check at the end. + if (output.source === 'stderr') { + stdErr += output.out; + } if (options.outChannel) { options.outChannel.append(output.out); } - }, reject, () => resolve(stdOut)); + }, reject, async () => { + // If the test runner python module is not installed we'll have something in stderr. + if (moduleName && pythonExecutionServicePromise && ErrorUtils.outputHasModuleNotInstalledError(moduleName, stdErr)) { + const pythonExecutionService = await pythonExecutionServicePromise; + const isInstalled = await pythonExecutionService.isModuleInstalled(moduleName); + if (!isInstalled) { + return reject(new ModuleNotInstalledError(moduleName)); + } + } + resolve(stdOut); + }); }); }); } diff --git a/src/client/unittests/common/services/configSettingService.ts b/src/client/unittests/common/services/configSettingService.ts index 02a31597f673..d7dd923e9f10 100644 --- a/src/client/unittests/common/services/configSettingService.ts +++ b/src/client/unittests/common/services/configSettingService.ts @@ -1,5 +1,5 @@ -import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; -import { Product } from '../../../common/installer'; +import { Uri, workspace, WorkspaceConfiguration } from 'vscode'; +import { Product } from '../../../common/types'; import { ITestConfigSettingsService, UnitTestProduct } from './../types'; export class TestConfigSettingsService implements ITestConfigSettingsService { @@ -30,16 +30,12 @@ export class TestConfigSettingsService implements ITestConfigSettingsService { // tslint:disable-next-line:no-any private static async updateSetting(testDirectory: string | Uri, setting: string, value: any) { let pythonConfig: WorkspaceConfiguration; - let configTarget: ConfigurationTarget; const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - configTarget = ConfigurationTarget.Workspace; pythonConfig = workspace.getConfiguration('python'); } else if (workspace.workspaceFolders.length === 1) { - configTarget = ConfigurationTarget.Workspace; pythonConfig = workspace.getConfiguration('python', workspace.workspaceFolders[0].uri); } else { - configTarget = ConfigurationTarget.Workspace; const workspaceFolder = workspace.getWorkspaceFolder(resource); if (!workspaceFolder) { throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); diff --git a/src/client/unittests/common/services/storageService.ts b/src/client/unittests/common/services/storageService.ts index d62c4c1dedcd..399396a842db 100644 --- a/src/client/unittests/common/services/storageService.ts +++ b/src/client/unittests/common/services/storageService.ts @@ -1,13 +1,13 @@ import { inject, injectable } from 'inversify'; import 'reflect-metadata'; import { Disposable, Uri, workspace } from 'vscode'; -import { IDiposableRegistry } from '../../../common/types'; +import { IDisposableRegistry } from '../../../common/types'; import { ITestCollectionStorageService, Tests } from './../types'; @injectable() export class TestCollectionStorageService implements ITestCollectionStorageService { private testsIndexedByWorkspaceUri = new Map(); - constructor( @inject(IDiposableRegistry) disposables: Disposable[]) { + constructor( @inject(IDisposableRegistry) disposables: Disposable[]) { disposables.push(this); } public getTests(wkspace: Uri): Tests | undefined { diff --git a/src/client/unittests/common/services/testManagerService.ts b/src/client/unittests/common/services/testManagerService.ts index 76d422c0fb07..4d769787dd27 100644 --- a/src/client/unittests/common/services/testManagerService.ts +++ b/src/client/unittests/common/services/testManagerService.ts @@ -1,14 +1,13 @@ import { Disposable, Uri } from 'vscode'; import { PythonSettings } from '../../../common/configSettings'; -import { Product } from '../../../common/installer'; -import { IDiposableRegistry } from '../../../common/types'; +import { IDisposableRegistry, Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { ITestManager, ITestManagerFactory, ITestManagerService, ITestsHelper, UnitTestProduct } from './../types'; export class TestManagerService implements ITestManagerService { private cachedTestManagers = new Map(); constructor(private wkspace: Uri, private testsHelper: ITestsHelper, private serviceContainer: IServiceContainer) { - const disposables = serviceContainer.get(IDiposableRegistry); + const disposables = serviceContainer.get(IDisposableRegistry); disposables.push(this); } public dispose() { @@ -23,14 +22,14 @@ export class TestManagerService implements ITestManagerService { } // tslint:disable-next-line:no-non-null-assertion - const instance = this.cachedTestManagers.get(preferredTestManager); - if (!instance) { + if (!this.cachedTestManagers.has(preferredTestManager)) { const testDirectory = this.getTestWorkingDirectory(); const testProvider = this.testsHelper.parseProviderName(preferredTestManager); const factory = this.serviceContainer.get(ITestManagerFactory); this.cachedTestManagers.set(preferredTestManager, factory(testProvider, this.wkspace, testDirectory)); } - return this.cachedTestManagers.get(preferredTestManager)!; + const testManager = this.cachedTestManagers.get(preferredTestManager)!; + return testManager.enabled ? testManager : undefined; } public getTestWorkingDirectory() { const settings = PythonSettings.getInstance(this.wkspace); diff --git a/src/client/unittests/common/services/workspaceTestManagerService.ts b/src/client/unittests/common/services/workspaceTestManagerService.ts index 6307bfaa7d78..e217410accc5 100644 --- a/src/client/unittests/common/services/workspaceTestManagerService.ts +++ b/src/client/unittests/common/services/workspaceTestManagerService.ts @@ -1,7 +1,7 @@ import { inject, injectable, named } from 'inversify'; import 'reflect-metadata'; import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; -import { IDiposableRegistry, IOutputChannel } from '../../../common/types'; +import { IDisposableRegistry, IOutputChannel } from '../../../common/types'; import { TEST_OUTPUT_CHANNEL } from './../constants'; import { ITestManager, ITestManagerService, ITestManagerServiceFactory, IWorkspaceTestManagerService, UnitTestProduct } from './../types'; @@ -10,7 +10,7 @@ export class WorkspaceTestManagerService implements IWorkspaceTestManagerService private workspaceTestManagers = new Map(); constructor( @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private outChannel: OutputChannel, @inject(ITestManagerServiceFactory) private testManagerServiceFactory: ITestManagerServiceFactory, - @inject(IDiposableRegistry) disposables: Disposable[]) { + @inject(IDisposableRegistry) disposables: Disposable[]) { disposables.push(this); } public dispose() { diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index c064083e7f42..18c730283b52 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -1,14 +1,15 @@ import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import 'reflect-metadata'; +import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; import { window } from 'vscode'; -import * as vscode from 'vscode'; +import { IUnitTestSettings } from '../../common/configSettings'; import * as constants from '../../common/constants'; -import { Product } from '../../common/installer'; +import { Product } from '../../common/types'; import { CommandSource } from './constants'; import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; -import { ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestsToRun, UnitTestProduct } from './types'; +import { ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types'; import { ITestsHelper } from './types'; export async function selectTestWorkspace(): Promise { @@ -47,17 +48,39 @@ export class TestsHelper implements ITestsHelper { constructor( @inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor) { } public parseProviderName(product: UnitTestProduct): TestProvider { switch (product) { - case Product.nosetest: { - return 'nosetest'; + case Product.nosetest: return 'nosetest'; + case Product.pytest: return 'pytest'; + case Product.unittest: return 'unittest'; + default: { + throw new Error(`Unknown Test Product ${product}`); } - case Product.pytest: { - return 'pytest'; + } + } + public getSettingsPropertyNames(product: UnitTestProduct): TestSettingsPropertyNames { + const id = this.parseProviderName(product); + switch (id) { + case 'pytest': { + return { + argsName: 'pyTestArgs' as keyof IUnitTestSettings, + pathName: 'pyTestPath' as keyof IUnitTestSettings, + enabledName: 'pyTestEnabled' as keyof IUnitTestSettings + }; } - case Product.unittest: { - return 'unittest'; + case 'nosetest': { + return { + argsName: 'nosetestArgs' as keyof IUnitTestSettings, + pathName: 'nosetestPath' as keyof IUnitTestSettings, + enabledName: 'nosetestsEnabled' as keyof IUnitTestSettings + }; + } + case 'unittest': { + return { + argsName: 'unittestArgs' as keyof IUnitTestSettings, + enabledName: 'unittestEnabled' as keyof IUnitTestSettings + }; } default: { - throw new Error(`Unknown Test Product ${product}`); + throw new Error(`Unknown Test Provider '${product}'`); } } } diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index 2511d66d8695..53d7335a3255 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -1,5 +1,6 @@ import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; -import { Product } from '../../common/installer'; +import { IUnitTestSettings } from '../../common/configSettings'; +import { Product } from '../../common/types'; import { CommandSource } from './constants'; export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; @@ -145,10 +146,17 @@ export interface IWorkspaceTestManagerService extends Disposable { getPreferredTestManager(resource: Uri): UnitTestProduct | undefined; } +export type TestSettingsPropertyNames = { + enabledName: keyof IUnitTestSettings; + argsName: keyof IUnitTestSettings; + pathName?: keyof IUnitTestSettings; +}; + export const ITestsHelper = Symbol('ITestsHelper'); export interface ITestsHelper { parseProviderName(product: UnitTestProduct): TestProvider; + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; flattenTestFiles(testFiles: TestFile[]): Tests; placeTestFilesIntoFolders(tests: Tests): void; } @@ -207,6 +215,7 @@ export interface ITestManagerServiceFactory extends Function { export const ITestManager = Symbol('ITestManager'); export interface ITestManager extends Disposable { readonly status: TestStatus; + readonly enabled: boolean; readonly workingDirectory: string; readonly workspaceFolder: Uri; stop(): void; diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts index f816708cdc86..228e68efae65 100644 --- a/src/client/unittests/nosetest/main.ts +++ b/src/client/unittests/nosetest/main.ts @@ -1,7 +1,8 @@ import { inject, injectable } from 'inversify'; import 'reflect-metadata'; import { Uri } from 'vscode'; -import { Product } from '../../common/installer'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { BaseTestManager } from '../common/managers/baseTestManager'; import { TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; @@ -9,6 +10,9 @@ import { runTest } from './runner'; @injectable() export class TestManager extends BaseTestManager { + public get enabled() { + return PythonSettings.getInstance(this.workspaceFolder).unitTest.nosetestsEnabled; + } constructor(workspaceFolder: Uri, rootDirectory: string, @inject(IServiceContainer) serviceContainer: IServiceContainer) { super('nosetest', Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); diff --git a/src/client/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts index 605200b95d78..f513361e217f 100644 --- a/src/client/unittests/pytest/main.ts +++ b/src/client/unittests/pytest/main.ts @@ -1,12 +1,16 @@ 'use strict'; import { Uri } from 'vscode'; -import { Product } from '../../common/installer'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { BaseTestManager } from '../common/managers/baseTestManager'; import { TestDiscoveryOptions, TestRunOptions, Tests, TestsToRun } from '../common/types'; import { runTest } from './runner'; export class TestManager extends BaseTestManager { + public get enabled() { + return PythonSettings.getInstance(this.workspaceFolder).unitTest.pyTestEnabled; + } constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { super('pytest', Product.pytest, workspaceFolder, rootDirectory, serviceContainer); diff --git a/src/client/unittests/pytest/services/parserService.ts b/src/client/unittests/pytest/services/parserService.ts index f69b862f342f..3d5a97c36042 100644 --- a/src/client/unittests/pytest/services/parserService.ts +++ b/src/client/unittests/pytest/services/parserService.ts @@ -161,7 +161,7 @@ export class TestsParser implements ITestsParser { } } - /* Sample output from py.test --collect-only + /* Sample output from pytest --collect-only diff --git a/src/client/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts index 4fb8ef66063f..ef9a8f134879 100644 --- a/src/client/unittests/unittest/main.ts +++ b/src/client/unittests/unittest/main.ts @@ -1,11 +1,15 @@ -'use strict'; import { Uri } from 'vscode'; -import { Product } from '../../common/installer'; +import { PythonSettings } from '../../common/configSettings'; +import { Product } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { BaseTestManager } from '../common/managers/baseTestManager'; import { TestDiscoveryOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; import { runTest } from './runner'; + export class TestManager extends BaseTestManager { + public get enabled() { + return PythonSettings.getInstance(this.workspaceFolder).unitTest.unittestEnabled; + } constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { super('unittest', Product.unittest, workspaceFolder, rootDirectory, serviceContainer); } @@ -18,7 +22,7 @@ export class TestManager extends BaseTestManager { workspaceFolder: this.workspaceFolder, cwd: this.rootDirectory, args, token: this.testDiscoveryCancellationToken!, ignoreCache, - outChannel: this.outputChannel + outChannel: this.outputChannel }; } public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<{}> { diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts index 9486b7f7a5bc..b84b2c1682f5 100644 --- a/src/client/workspaceSymbols/main.ts +++ b/src/client/workspaceSymbols/main.ts @@ -1,10 +1,10 @@ import * as vscode from 'vscode'; -import { workspace } from 'vscode'; -import { Commands, PythonLanguage } from '../common/constants'; +import { OutputChannel, workspace } from 'vscode'; +import { Commands, PythonLanguage, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; import { isNotInstalledError } from '../common/helpers'; -import { Installer, Product } from '../common/installer'; -import { InstallerResponse } from '../common/types'; +import { IInstaller, InstallerResponse, IOutputChannel, Product } from '../common/types'; import { fsExistsAsync } from '../common/utils'; +import { IServiceContainer } from '../ioc/types'; import { Generator } from './generator'; import { WorkspaceSymbolProvider } from './provider'; @@ -13,14 +13,13 @@ const MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD = 2; export class WorkspaceSymbols implements vscode.Disposable { private disposables: vscode.Disposable[]; private generators: Generator[] = []; - private installer: Installer; + private readonly outputChannel: OutputChannel; // tslint:disable-next-line:no-any private timeout: any; - constructor(private outputChannel: vscode.OutputChannel) { + constructor(private serviceContainer: IServiceContainer) { + this.outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); this.disposables = []; this.disposables.push(this.outputChannel); - this.installer = new Installer(); - this.disposables.push(this.installer); this.registerCommands(); this.initializeGenerators(); vscode.languages.registerWorkspaceSymbolProvider(new WorkspaceSymbolProvider(this.generators, this.outputChannel)); @@ -103,7 +102,8 @@ export class WorkspaceSymbols implements vscode.Disposable { promptResponse = await promptPromise; continue; } else { - promptPromise = this.installer.promptToInstall(Product.ctags, workspace.workspaceFolders[0]!.uri); + const installer = this.serviceContainer.get(IInstaller); + promptPromise = installer.promptToInstall(Product.ctags, workspace.workspaceFolders![0]!.uri); promptResponse = await promptPromise; } if (promptResponse !== InstallerResponse.Installed || (!token || token.isCancellationRequested)) { diff --git a/src/test/common/common.test.ts b/src/test/common/common.test.ts index aeccb54166da..0b9070514fe7 100644 --- a/src/test/common/common.test.ts +++ b/src/test/common/common.test.ts @@ -29,22 +29,26 @@ suite('ChildProc', () => { test('Stream Stdout', done => { const output: string[] = []; function handleOutput(data: string) { - output.push(data); + if (data.trim().length > 0) { + output.push(data.trim()); + } } execPythonFile(undefined, 'python', ['-c', 'print(1)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], `1${EOL}`, 'Ouput value incorrect'); + assert.equal(output[0], '1', 'Ouput value incorrect'); }).then(done).catch(done); }); test('Stream Stdout (Unicode)', async () => { const output: string[] = []; function handleOutput(data: string) { - output.push(data); + if (data.trim().length > 0) { + output.push(data.trim()); + } } await execPythonFile(undefined, 'python', ['-c', 'print(\'öä\')'], __dirname, false, handleOutput); assert.equal(output.length, 1, 'Ouput length incorrect'); - assert.equal(output[0], `öä${EOL}`, 'Ouput value incorrect'); + assert.equal(output[0], 'öä', 'Ouput value incorrect'); }); test('Stream Stdout with Threads', function (done) { @@ -52,12 +56,14 @@ suite('ChildProc', () => { this.timeout(6000); const output: string[] = []; function handleOutput(data: string) { - output.push(data); + if (data.trim().length > 0) { + output.push(data.trim()); + } } execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput).then(() => { assert.equal(output.length, 2, 'Ouput length incorrect'); - assert.equal(output[0], `1${EOL}`, 'First Ouput value incorrect'); - assert.equal(output[1], `2${EOL}`, 'Second Ouput value incorrect'); + assert.equal(output[0], '1', 'First Ouput value incorrect'); + assert.equal(output[1], '2', 'Second Ouput value incorrect'); }).then(done).catch(done); }); @@ -66,7 +72,9 @@ suite('ChildProc', () => { const def = createDeferred(); const output: string[] = []; function handleOutput(data: string) { - output.push(data); + if (data.trim().length > 0) { + output.push(data.trim()); + } } const cancellation = new vscode.CancellationTokenSource(); execPythonFile(undefined, 'python', ['-c', 'import sys\nprint(1)\nsys.__stdout__.flush()\nimport time\ntime.sleep(5)\nprint(2)'], __dirname, false, handleOutput, cancellation.token).then(() => { diff --git a/src/test/common/installer.multiroot.test.ts b/src/test/common/installer.multiroot.test.ts index 16723f0c435f..9425800988e8 100644 --- a/src/test/common/installer.multiroot.test.ts +++ b/src/test/common/installer.multiroot.test.ts @@ -1,38 +1,46 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { Installer, Product } from '../../client/common/installer'; +import { IInstaller, Product } from '../../client/common/types'; import { rootWorkspaceUri } from '../common'; import { updateSetting } from '../common'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; // tslint:disable-next-line:no-suspicious-comment // TODO: Need to mock the command runner, to check what commands are being sent. // Instead of altering the environment. suite('Installer', () => { - let outputChannel: MockOutputChannel; - let installer: Installer; + let ioc: UnitTestIocContainer; const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { // tslint:disable-next-line:no-invalid-this this.skip(); } - outputChannel = new MockOutputChannel('Installer'); - installer = new Installer(outputChannel); await initializeTest(); }); setup(async () => { await initializeTest(); await resetSettings(); + initializeDI(); }); suiteTeardown(async () => { await closeActiveWindows(); await resetSettings(); }); - teardown(closeActiveWindows); + teardown(async () => { + ioc.dispose(); + closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + } async function resetSettings() { await updateSetting('linting.enabledWithoutWorkspace', true, undefined, ConfigurationTarget.Global); @@ -47,6 +55,7 @@ suite('Installer', () => { // tslint:disable-next-line:no-invalid-this this.skip(); } + const installer = ioc.serviceContainer.get(IInstaller); await installer.disableLinter(Product.pylint, workspaceUri); const pythonWConfig = workspace.getConfiguration('python', workspaceUri); const value = pythonWConfig.inspect('linting.pylintEnabled'); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index 3abf205d2bc9..c7fc8ab7160a 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -2,75 +2,111 @@ import * as assert from 'assert'; import * as path from 'path'; import { ConfigurationTarget, Uri, workspace } from 'vscode'; import { EnumEx } from '../../client/common/enumUtils'; -import { Installer, Product } from '../../client/common/installer'; -import { rootWorkspaceUri } from '../common'; +import { createDeferred } from '../../client/common/helpers'; +import { Installer } from '../../client/common/installer/installer'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { Logger } from '../../client/common/logger'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { IProcessService } from '../../client/common/process/types'; +import { ITerminalService } from '../../client/common/terminal/types'; +import { IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product } from '../../client/common/types'; import { updateSetting } from '../common'; -import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; - -// tslint:disable-next-line:no-suspicious-comment -// TODO: Need to mock the command runner, to check what commands are being sent. -// Instead of altering the environment. +import { rootWorkspaceUri } from '../common'; +import { MockModuleInstaller } from '../mocks/moduleInstaller'; +import { MockProcessService } from '../mocks/proc'; +import { MockTerminalService } from '../mocks/terminalService'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; +// tslint:disable-next-line:max-func-body-length suite('Installer', () => { - let outputChannel: MockOutputChannel; - let installer: Installer; + let ioc: UnitTestIocContainer; const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; - suiteSetup(async () => { - outputChannel = new MockOutputChannel('Installer'); - installer = new Installer(outputChannel); - await initializeTest(); - }); + suiteSetup(initializeTest); setup(async () => { await initializeTest(); await resetSettings(); + initializeDI(); }); suiteTeardown(async () => { await closeActiveWindows(); await resetSettings(); }); - teardown(closeActiveWindows); + teardown(async () => { + ioc.dispose(); + closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerLinterTypes(); + ioc.registerFormatterTypes(); + + ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + ioc.serviceManager.addSingleton(ILogger, Logger); + ioc.serviceManager.addSingleton(IInstaller, Installer); + ioc.serviceManager.addSingleton(IPathUtils, PathUtils); + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingleton(ITerminalService, MockTerminalService); + ioc.serviceManager.addSingletonInstance(IsWindows, false); + } async function resetSettings() { await updateSetting('linting.enabledWithoutWorkspace', true, undefined, ConfigurationTarget.Global); await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); } - async function testUninstallingProduct(product: Product) { - let isInstalled = await installer.isInstalled(product, resource); - if (isInstalled) { - await installer.uninstall(product, resource); - isInstalled = await installer.isInstalled(product, resource); - // Sometimes installation doesn't work on Travis - if (!IS_TRAVIS) { - assert.equal(isInstalled, false, 'Product uninstall failed'); + async function testCheckingIfProductIsInstalled(product: Product) { + const installer = ioc.serviceContainer.get(IInstaller); + const processService = ioc.serviceContainer.get(IProcessService); + const checkInstalledDef = createDeferred(); + processService.onExec((file, args, options, callback) => { + const moduleName = installer.translateProductToModuleName(product, ModuleNamePurpose.run); + if (args.length > 1 && args[0] === '-c' && args[1] === `import ${moduleName}`) { + checkInstalledDef.resolve(true); } - } + if (product === Product.prospector && args.length > 0 && args[0] === '--version') { + checkInstalledDef.resolve(true); + } + callback({ stdout: '' }); + }); + await installer.isInstalled(product, resource); + await checkInstalledDef.promise; } - EnumEx.getNamesAndValues(Product).forEach(prod => { - test(`${prod.name} : Uninstall`, async () => { - if (prod.value === Product.unittest || prod.value === Product.ctags) { + test(`Ensure isInstalled for Product: '${prod.name}' executes the right command`, async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); + if (prod.value === Product.ctags || prod.value === Product.unittest) { return; } - await testUninstallingProduct(prod.value); + await testCheckingIfProductIsInstalled(prod.value); }); }); async function testInstallingProduct(product: Product) { - const isInstalled = await installer.isInstalled(product, resource); - if (!isInstalled) { - await installer.install(product, resource); - } - const checkIsInstalledAgain = await installer.isInstalled(product, resource); - // Sometimes installation doesn't work on Travis - if (!IS_TRAVIS) { - assert.notEqual(checkIsInstalledAgain, false, 'Product installation failed'); - } + const installer = ioc.serviceContainer.get(IInstaller); + const checkInstalledDef = createDeferred(); + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const moduleInstallerOne = moduleInstallers.find(item => item.displayName === 'two')!; + + moduleInstallerOne.on('installModule', moduleName => { + const installName = installer.translateProductToModuleName(product, ModuleNamePurpose.install); + if (installName === moduleName) { + checkInstalledDef.resolve(); + } + }); + await installer.install(product); + await checkInstalledDef.promise; } EnumEx.getNamesAndValues(Product).forEach(prod => { - test(`${prod.name} : Install`, async () => { + test(`Ensure install for Product: '${prod.name}' executes the right command in IModuleInstaller`, async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('one', false)); + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('two', true)); if (prod.value === Product.unittest || prod.value === Product.ctags) { return; } @@ -79,6 +115,7 @@ suite('Installer', () => { }); test('Disable linting of files not contained in a workspace', async () => { + const installer = ioc.serviceContainer.get(IInstaller); await installer.disableLinter(Product.pylint, undefined); const pythonConfig = workspace.getConfiguration('python'); assert.equal(pythonConfig.get('linting.enabledWithoutWorkspace'), false, 'Incorrect setting'); @@ -89,6 +126,7 @@ suite('Installer', () => { // tslint:disable-next-line:no-invalid-this this.skip(); } + const installer = ioc.serviceContainer.get(IInstaller); await installer.disableLinter(Product.pylint, workspaceUri); const pythonConfig = workspace.getConfiguration('python', workspaceUri); assert.equal(pythonConfig.get('linting.pylintEnabled'), false, 'Incorrect setting'); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts new file mode 100644 index 000000000000..8b6556bfc024 --- /dev/null +++ b/src/test/common/moduleInstaller.test.ts @@ -0,0 +1,161 @@ +import { assert, expect } from 'chai'; +import * as path from 'path'; +import { ConfigurationTarget, Uri, workspace } from 'vscode'; +import { EnumEx } from '../../client/common/enumUtils'; +import { createDeferred } from '../../client/common/helpers'; +import { CondaInstaller } from '../../client/common/installer/condaInstaller'; +import { Installer } from '../../client/common/installer/installer'; +import { PipInstaller } from '../../client/common/installer/pipInstaller'; +import { IModuleInstaller } from '../../client/common/installer/types'; +import { Logger } from '../../client/common/logger'; +import { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { IProcessService } from '../../client/common/process/types'; +import { ITerminalService } from '../../client/common/terminal/types'; +import { IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product } from '../../client/common/types'; +import { ICondaLocatorService } from '../../client/interpreter/contracts'; +import { rootWorkspaceUri } from '../common'; +import { updateSetting } from '../common'; +import { MockCondaLocatorService } from '../interpreters/mocks'; +import { MockCondaLocator } from '../mocks/condaLocator'; +import { MockModuleInstaller } from '../mocks/moduleInstaller'; +import { MockProcessService } from '../mocks/proc'; +import { MockTerminalService } from '../mocks/terminalService'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST, IS_TRAVIS } from './../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Module Installer', () => { + let ioc: UnitTestIocContainer; + const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); + const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; + suiteSetup(initializeTest); + setup(async () => { + await initializeTest(); + await resetSettings(); + initializeDI(); + }); + suiteTeardown(async () => { + await closeActiveWindows(); + await resetSettings(); + }); + teardown(async () => { + ioc.dispose(); + closeActiveWindows(); + }); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerUnitTestTypes(); + ioc.registerVariableTypes(); + ioc.registerLinterTypes(); + ioc.registerFormatterTypes(); + + ioc.serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); + ioc.serviceManager.addSingleton(ILogger, Logger); + ioc.serviceManager.addSingleton(IInstaller, Installer); + + ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); + ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); + ioc.serviceManager.addSingleton(ICondaLocatorService, MockCondaLocator); + ioc.serviceManager.addSingleton(IPathUtils, PathUtils); + + ioc.registerMockProcessTypes(); + ioc.serviceManager.addSingleton(ITerminalService, MockTerminalService); + ioc.serviceManager.addSingletonInstance(IsWindows, false); + } + async function resetSettings() { + await updateSetting('linting.enabledWithoutWorkspace', true, undefined, ConfigurationTarget.Global); + await updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); + } + + test('Ensure pip is supported and conda is not', async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); + const installer = ioc.serviceContainer.get(IInstaller); + const processService = ioc.serviceContainer.get(IProcessService); + const checkInstalledDef = createDeferred(); + processService.onExec((file, args, options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '', stderr: 'not available' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + expect(moduleInstallers).length(3, 'Incorrect number of installers'); + + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + + const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; + expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); + expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); + + const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; + expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); + expect(mockInstaller.isSupported()).to.eventually.equal(false, 'mock is not supported'); + }); + + test('Ensure pip and conda are supported', async () => { + ioc.serviceManager.addSingletonInstance(IModuleInstaller, new MockModuleInstaller('mock', true)); + const installer = ioc.serviceContainer.get(IInstaller); + const processService = ioc.serviceContainer.get(IProcessService); + const checkInstalledDef = createDeferred(); + processService.onExec((file, args, options, callback) => { + if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { + callback({ stdout: '' }); + } + if (args.length > 0 && args[0] === '--version' && file === 'conda') { + callback({ stdout: '' }); + } + }); + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + expect(moduleInstallers).length(3, 'Incorrect number of installers'); + + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); + + const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; + expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); + expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); + }); + + test('Validate pip install arguments', async () => { + const moduleName = 'xyz'; + const installer = ioc.serviceContainer.get(IInstaller); + const terminalService = ioc.serviceContainer.get(ITerminalService); + const validateModuleInstallArgs = createDeferred(); + + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + + await pipInstaller.installModule(moduleName); + const commandSent = await terminalService.commandSent; + const commandParts = commandSent.split(' '); + commandParts.shift(); + expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); + }); + + test('Validate Conda install arguments', async () => { + const moduleName = 'xyz'; + const installer = ioc.serviceContainer.get(IInstaller); + const terminalService = ioc.serviceContainer.get(ITerminalService); + const validateModuleInstallArgs = createDeferred(); + + const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); + const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + + expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + + await pipInstaller.installModule(moduleName); + const commandSent = await terminalService.commandSent; + const commandParts = commandSent.split(' '); + commandParts.shift(); + expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); + }); +}); diff --git a/src/test/common/process/proc.exec.test.ts b/src/test/common/process/proc.exec.test.ts index 1a1ae6b0723c..32091f5a9ab7 100644 --- a/src/test/common/process/proc.exec.test.ts +++ b/src/test/common/process/proc.exec.test.ts @@ -121,7 +121,6 @@ suite('ProcessService', () => { const expectedOutput = ['1a2b3c']; expect(result).not.to.be.an('undefined', 'result is undefined'); - expect(result.stderr).to.equal(undefined, 'stderr not undefined'); const outputs = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); expect(outputs).to.deep.equal(expectedOutput, 'Output values are incorrect'); }); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts index ab8de5b5270e..abef8f28da6d 100644 --- a/src/test/common/process/proc.observable.test.ts +++ b/src/test/common/process/proc.observable.test.ts @@ -21,19 +21,6 @@ suite('ProcessService', () => { setup(initialize); teardown(initialize); - test('execObservable should output print statements', async () => { - const procService = new ProcessService(new BufferDecoder()); - const printOutput = '1234'; - const result = procService.execObservable(pythonPath, ['-c', `print("${printOutput}")`]); - - expect(result).not.to.be.an('undefined', 'result is undefined'); - const output = await result.out.toPromise(); - expect(output.source).to.be.equal('stdout', 'Source is incorrect'); - expect(output.out).to.have.length.greaterThan(0, 'Invalid output length'); - const stdOut = output.out.trim(); - expect(stdOut).to.equal(printOutput, 'Output is incorrect'); - }); - test('execObservable should stream output with new lines', function (done) { // tslint:disable-next-line:no-invalid-this this.timeout(10000); @@ -173,7 +160,7 @@ suite('ProcessService', () => { }, done, done); }); - test('execObservable should merge stdout and stderr streams', function (done) { + test('execObservable should send stdout and stderr streams separately', function (done) { // tslint:disable-next-line:no-invalid-this this.timeout(7000); const procService = new ProcessService(new BufferDecoder()); @@ -186,9 +173,9 @@ suite('ProcessService', () => { 'sys.stderr.write("c")', 'sys.stderr.flush()', 'time.sleep(1)']; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { mergeStdOutErr: true }); const outputs = [ - { out: '1', source: 'stdout' }, { out: 'a', source: 'stdout' }, - { out: '2', source: 'stdout' }, { out: 'b', source: 'stdout' }, - { out: '3', source: 'stdout' }, { out: 'c', source: 'stdout' }]; + { out: '1', source: 'stdout' }, { out: 'a', source: 'stderr' }, + { out: '2', source: 'stdout' }, { out: 'b', source: 'stderr' }, + { out: '3', source: 'stdout' }, { out: 'c', source: 'stderr' }]; expect(result).not.to.be.an('undefined', 'result is undefined'); result.out.subscribe(output => { diff --git a/src/test/common/process/pythonProc.simple.multiroot.test.ts b/src/test/common/process/pythonProc.simple.multiroot.test.ts index 23c73b0f60c5..7f5bec116e7c 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -12,7 +12,7 @@ import { PythonSettings } from '../../../client/common/configSettings'; import { PathUtils } from '../../../client/common/platform/pathUtils'; import { registerTypes as processRegisterTypes } from '../../../client/common/process/serviceRegistry'; import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; -import { IDiposableRegistry, IPathUtils, IsWindows } from '../../../client/common/types'; +import { IDisposableRegistry, IPathUtils, IsWindows } from '../../../client/common/types'; import { IS_WINDOWS } from '../../../client/common/utils'; import { registerTypes as variablesRegisterTypes } from '../../../client/common/variables/serviceRegistry'; import { ServiceManager } from '../../../client/ioc/serviceManager'; @@ -41,7 +41,7 @@ suite('PythonExecutableService', () => { setup(() => { cont = new Container(); serviceManager = new ServiceManager(cont); - serviceManager.addSingletonInstance(IDiposableRegistry, []); + serviceManager.addSingletonInstance(IDisposableRegistry, []); serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); serviceManager.addSingleton(IPathUtils, PathUtils); processRegisterTypes(serviceManager); diff --git a/src/test/common/variables/envVarsProvider.multiroot.test.ts b/src/test/common/variables/envVarsProvider.multiroot.test.ts index a64e38efe061..6267bf78625c 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -11,7 +11,7 @@ import { ConfigurationTarget, Disposable, Uri, workspace } from 'vscode'; import { IS_WINDOWS } from '../../../client/common/configSettings'; import { PathUtils } from '../../../client/common/platform/pathUtils'; import { registerTypes as processRegisterTypes } from '../../../client/common/process/serviceRegistry'; -import { IDiposableRegistry, IPathUtils } from '../../../client/common/types'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; import { IsWindows } from '../../../client/common/types'; import { registerTypes as variablesRegisterTypes } from '../../../client/common/variables/serviceRegistry'; import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; @@ -41,7 +41,7 @@ suite('Multiroot Environment Variables Provider', () => { setup(() => { cont = new Container(); serviceManager = new ServiceManager(cont); - serviceManager.addSingletonInstance(IDiposableRegistry, []); + serviceManager.addSingletonInstance(IDisposableRegistry, []); serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); serviceManager.addSingleton(IPathUtils, PathUtils); processRegisterTypes(serviceManager); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts index e58c9d900418..962e1111200b 100644 --- a/src/test/format/extension.format.test.ts +++ b/src/test/format/extension.format.test.ts @@ -93,7 +93,7 @@ suite('Formatting', () => { }); assert.equal(textEditor.document.getText(), formattedContents, 'Formatted text is not the same'); } - test('AutoPep8', () => testFormatting(new AutoPep8Formatter(ch), formattedAutoPep8, autoPep8FileToFormat, 'autopep8.output')); + test('AutoPep8', () => testFormatting(new AutoPep8Formatter(ioc.serviceContainer), formattedAutoPep8, autoPep8FileToFormat, 'autopep8.output')); - test('Yapf', () => testFormatting(new YapfFormatter(ch), formattedYapf, yapfFileToFormat, 'yapf.output')); + test('Yapf', () => testFormatting(new YapfFormatter(ioc.serviceContainer), formattedYapf, yapfFileToFormat, 'yapf.output')); }); diff --git a/src/test/interpreters/condaEnvService.test.ts b/src/test/interpreters/condaEnvService.test.ts index ae3dfba4c4ab..8f6142c77662 100644 --- a/src/test/interpreters/condaEnvService.test.ts +++ b/src/test/interpreters/condaEnvService.test.ts @@ -2,28 +2,41 @@ import * as assert from 'assert'; import * as path from 'path'; import { Uri } from 'vscode'; import { IS_WINDOWS, PythonSettings } from '../../client/common/configSettings'; -import { PythonInterpreter } from '../../client/interpreter/contracts'; +import { IProcessService } from '../../client/common/process/types'; +import { InterpreterType, 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 { MockCondaLocatorService, MockProvider } from './mocks'; +import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +import { MockCondaLocatorService, MockInterpreterVersionProvider, MockProvider } from './mocks'; const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); const fileInNonRootWorkspace = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); // tslint:disable-next-line:max-func-body-length suite('Interpreters from Conda Environments', () => { + let ioc: UnitTestIocContainer; + let processService: IProcessService; suiteSetup(initialize); setup(initializeTest); + + function initializeDI() { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerVariableTypes(); + ioc.registerProcessTypes(); + processService = ioc.serviceContainer.get(IProcessService); + } + test('Must return an empty list for empty json', async () => { - const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); // 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(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'conda', 'envs', 'scipy')], @@ -44,7 +57,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(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')], default_prefix: '', @@ -59,7 +72,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(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy')] }; @@ -72,7 +85,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(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); const info = { default_prefix: path.join(environmentsPath, 'conda', 'envs', 'numpy') }; @@ -85,7 +98,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(new CondaLocatorService(IS_WINDOWS)); + const condaProvider = new CondaEnvService(new CondaLocatorService(IS_WINDOWS, processService), new MockInterpreterVersionProvider('')); const info = { envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), path.join(environmentsPath, 'path0', 'one.exe'), @@ -108,16 +121,16 @@ suite('Interpreters from Conda Environments', () => { }); test('Must detect conda environments from a list', async () => { const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3' }, - { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, - { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3' }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.' }, - { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.' } + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.Unknown }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, + { displayName: 'xAnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', type: InterpreterType.Unknown }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'xContinuum Analytics, Inc.', type: InterpreterType.Unknown }, + { displayName: 'xnaconda', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[0]), false, '1. Identified environment incorrectly'); assert.equal(condaProvider.isCondaEnvironment(registryInterpreters[1]), false, '2. Identified environment incorrectly'); @@ -129,36 +142,36 @@ suite('Interpreters from Conda Environments', () => { }); test('Correctly identifies latest version when major version is different', async () => { const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3' }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '3.1.3', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, // tslint:disable-next-line:no-any - { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null, type: InterpreterType.Unknown }, // tslint:disable-next-line:no-any - { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, - { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } + { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined, type: InterpreterType.Unknown }, + { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); }); test('Correctly identifies latest version when major version is same', async () => { const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1' }, - { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3' }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1' }, + { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Two', path: PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath, companyDisplayName: 'Two 2', version: '2.11.3', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, // tslint:disable-next-line:no-any - { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null }, + { displayName: 'Four', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'Three 3', version: null, type: InterpreterType.Unknown }, // tslint:disable-next-line:no-any - { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined }, - { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2' }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.' } + { displayName: 'Five', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Three 3', version: undefined, type: InterpreterType.Unknown }, + { displayName: 'Six', path: path.join(environmentsPath, 'conda', 'envs', 'scipy'), companyDisplayName: 'xContinuum Analytics, Inc.', version: '2', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new CondaEnvService(new CondaLocatorService(true, mockRegistryProvider)); + const condaProvider = new CondaEnvService(new CondaLocatorService(true, processService, mockRegistryProvider), new MockInterpreterVersionProvider('')); // tslint:disable-next-line:no-non-null-assertion assert.equal(condaProvider.getLatestVersion(registryInterpreters)!.displayName, 'Two', 'Failed to identify latest version'); @@ -166,13 +179,13 @@ suite('Interpreters from Conda Environments', () => { test('Must use Conda env from Registry to locate conda.exe', async () => { const condaPythonExePath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments', 'conda', 'Scripts', 'python.exe'); const registryInterpreters: PythonInterpreter[] = [ - { 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: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: '1', type: InterpreterType.Unknown }, + { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: '1.11.0', type: InterpreterType.Unknown }, + { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: '2.10.1', type: InterpreterType.Unknown }, + { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } ]; const mockRegistryProvider = new MockProvider(registryInterpreters); - const condaProvider = new MockCondaLocatorService(true, mockRegistryProvider, false); + const condaProvider = new MockCondaLocatorService(true, processService, 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/condaHelper.test.ts b/src/test/interpreters/condaHelper.test.ts index 45d89c3410df..e289dd2f473f 100644 --- a/src/test/interpreters/condaHelper.test.ts +++ b/src/test/interpreters/condaHelper.test.ts @@ -17,7 +17,7 @@ suite('Interpreters display name from Conda Environments', () => { python_version: '3.6.1.final.10' }; const displayName = condaHelper.getDisplayName(info); - assert.equal(displayName, `Python 3.6.1 : ${AnacondaDisplayName}`, 'Incorrect display name'); + assert.equal(displayName, AnacondaDisplayName, 'Incorrect display name'); }); test('Must return info without first part if not a python version', () => { const info: CondaInfo = { @@ -26,13 +26,13 @@ suite('Interpreters display name from Conda Environments', () => { 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\'', () => { + test('Must return info without prefixing 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'); + assert.equal(displayName, 'Anaconda 4.4.0 (64-bit)', 'Incorrect display name'); }); test('Must include Ananconda name if Company name not found', () => { const info: CondaInfo = { @@ -40,6 +40,6 @@ suite('Interpreters display name from Conda Environments', () => { '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'); + assert.equal(displayName, `4.4.0 (64-bit) : ${AnacondaDisplayName}`, 'Incorrect display name'); }); }); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index 0cf3271124a3..e82bc5ebe66a 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -4,6 +4,7 @@ import { EOL } from 'os'; import * as path from 'path'; import { ConfigurationTarget, Uri, window, workspace } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; +import { InterpreterType } from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; import { getFirstNonEmptyLineFromMultilineString } from '../../client/interpreter/helpers'; import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; @@ -81,9 +82,9 @@ suite('Interpreters Display', () => { }).then(value => value.length === 0 ? PythonSettings.getInstance(Uri.file(fileInNonRootWorkspace)).pythonPath : value); const statusBar = new MockStatusBarItem(); const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', type: 'One 1' }, - { displayName: 'Two', path: pythonPath, type: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', type: 'Three 3' } + { displayName: 'One', path: 'c:/path1/one.exe', type: InterpreterType.VirtualEnv }, + { displayName: 'Two', path: pythonPath, type: InterpreterType.VirtualEnv }, + { displayName: 'Three', path: 'c:/path3/three.exe', type: InterpreterType.VirtualEnv } ]; const provider = new MockProvider(interpreters); const displayName = 'Mock Display Name'; @@ -102,9 +103,9 @@ suite('Interpreters Display', () => { const statusBar = new MockStatusBarItem(); const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, - { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.VirtualEnv }, + { displayName: 'Two', path: pythonPath, companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider(''); @@ -117,9 +118,9 @@ suite('Interpreters Display', () => { test('Will update status prompting user to select an interpreter', async () => { const statusBar = new MockStatusBarItem(); const interpreters = [ - { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1' }, - { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2' }, - { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3' } + { displayName: 'One', path: 'c:/path1/one.exe', companyDisplayName: 'One 1', type: InterpreterType.VirtualEnv }, + { displayName: 'Two', path: 'c:/asdf', companyDisplayName: 'Two 2', type: InterpreterType.VirtualEnv }, + { displayName: 'Three', path: 'c:/path3/three.exe', companyDisplayName: 'Three 3', type: InterpreterType.VirtualEnv } ]; const provider = new MockProvider(interpreters); const displayNameProvider = new MockInterpreterVersionProvider('', true); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index 5ae12af5c396..0f3e1bb92767 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,8 +1,8 @@ -import { Architecture, Hive, IRegistry } from '../../client/common/platform/registry'; -import { IInterpreterLocatorService, PythonInterpreter } from '../../client/interpreter/contracts'; -import { IInterpreterVersionService } from '../../client/interpreter/interpreterVersion'; +import { Architecture, IRegistry, RegistryHive } from '../../client/common/platform/types'; +import { IProcessService } from '../../client/common/process/types'; +import { IInterpreterLocatorService, IInterpreterVersionService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { CondaLocatorService } from '../../client/interpreter/locators/services/condaLocator'; -import { IVirtualEnvironment } from '../../client/interpreter/virtualEnvs/contracts'; +import { IVirtualEnvironmentIdentifier } from '../../client/interpreter/virtualEnvs/types'; export class MockProvider implements IInterpreterLocatorService { constructor(private suggestions: PythonInterpreter[]) { @@ -15,10 +15,10 @@ export class MockProvider implements IInterpreterLocatorService { } 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 }[]) { + constructor(private keys: { key: string, hive: RegistryHive, arch?: Architecture, values: string[] }[], + private values: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[]) { } - public async getKeys(key: string, hive: Hive, arch?: Architecture): Promise { + public async getKeys(key: string, hive: RegistryHive, arch?: Architecture): Promise { const items = this.keys.find(item => { if (typeof item.arch === 'number') { return item.key === key && item.hive === hive && item.arch === arch; @@ -28,7 +28,7 @@ export class MockRegistry implements IRegistry { return items ? Promise.resolve(items.values) : Promise.resolve([]); } - public async getValue(key: string, hive: Hive, arch?: Architecture, name?: string): Promise { + public async getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise { const items = this.values.find(item => { if (item.key !== key || item.hive !== hive) { return false; @@ -46,8 +46,8 @@ export class MockRegistry implements IRegistry { } } -export class MockVirtualEnv implements IVirtualEnvironment { - constructor(private isDetected: boolean, public name: string) { +export class MockVirtualEnv implements IVirtualEnvironmentIdentifier { + constructor(private isDetected: boolean, public name: string, public type: InterpreterType.VirtualEnv | InterpreterType.VEnv = InterpreterType.VirtualEnv) { } public async detect(pythonPath: string): Promise { return Promise.resolve(this.isDetected); @@ -71,8 +71,8 @@ export class MockInterpreterVersionProvider implements IInterpreterVersionServic // tslint:disable-next-line:max-classes-per-file export class MockCondaLocatorService extends CondaLocatorService { - constructor(isWindows: boolean, registryLookupForConda?: IInterpreterLocatorService, private isCondaInEnv?: boolean) { - super(isWindows, registryLookupForConda); + constructor(isWindows: boolean, procService: IProcessService, registryLookupForConda?: IInterpreterLocatorService, private isCondaInEnv?: boolean) { + super(isWindows, procService, registryLookupForConda); } public async isCondaInCurrentPath() { if (typeof this.isCondaInEnv === 'boolean') { diff --git a/src/test/interpreters/windowsRegistryService.test.ts b/src/test/interpreters/windowsRegistryService.test.ts index 203d69ac3c68..1ee6b44822a5 100644 --- a/src/test/interpreters/windowsRegistryService.test.ts +++ b/src/test/interpreters/windowsRegistryService.test.ts @@ -1,6 +1,6 @@ import * as assert from 'assert'; import * as path from 'path'; -import { Architecture, Hive } from '../../client/common/platform/registry'; +import { Architecture, RegistryHive } from '../../client/common/platform/types'; import { IS_WINDOWS } from '../../client/debugger/Common/Utils'; import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; import { initialize, initializeTest } from '../initialize'; @@ -29,15 +29,15 @@ suite('Interpreters from Windows Registry', () => { }); test('Must return a single entry', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } ]; const registryValues = [ - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'one.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, - { key: '\\Software\\Python\\Company One\\Tag1', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' } + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'one.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -53,11 +53,11 @@ suite('Interpreters from Windows Registry', () => { }); test('Must default names for PythonCore and exe', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore'] }, - { key: '\\Software\\Python\\PythonCore', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore\\Tag1'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore'] }, + { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PythonCore\\Tag1'] } ]; const registryValues = [ - { key: '\\Software\\Python\\PythonCore\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + { key: '\\Software\\Python\\PythonCore\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -73,11 +73,11 @@ suite('Interpreters from Windows Registry', () => { }); test('Must ignore company \'PyLauncher\'', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher'] }, - { key: '\\Software\\Python\\PythonCore', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher\\Tag1'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher'] }, + { key: '\\Software\\Python\\PythonCore', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\PyLauncher\\Tag1'] } ]; const registryValues = [ - { key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'c:/temp/Install Path Tag1' } + { key: '\\Software\\Python\\PyLauncher\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'c:/temp/Install Path Tag1' } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -88,11 +88,11 @@ suite('Interpreters from Windows Registry', () => { }); test('Must return a single entry and when registry contains only the InstallPath', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1'] } ]; const registryValues = [ - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -108,33 +108,33 @@ suite('Interpreters from Windows Registry', () => { }); test('Must return multiple entries', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three'] }, - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } ]; const registryValues = [ - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2'), name: 'Version' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2'), name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path3') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path3') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe') } + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe') } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -162,38 +162,38 @@ suite('Interpreters from Windows Registry', () => { }); test('Must return multiple entries excluding the invalid registry items and duplicate paths', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python\\Company Four', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, - { key: '\\Software\\Python\\Company Five', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, - { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } ]; - const registryValues: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[] = [ - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path1') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: undefined }, + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); @@ -221,38 +221,38 @@ suite('Interpreters from Windows Registry', () => { }); test('Must return multiple entries excluding the invalid registry items and nonexistent paths', async () => { const registryKeys = [ - { key: '\\Software\\Python', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, - { key: '\\Software\\Python\\Company Two', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, - { key: '\\Software\\Python\\Company Three', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, - { key: '\\Software\\Python\\Company Four', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, - { key: '\\Software\\Python\\Company Five', hive: Hive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, - { key: '\\Software\\Python', hive: Hive.HKLM, arch: Architecture.x86, values: ['A'] }, - { key: '\\Software\\Python\\Company A', hive: Hive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } + { key: '\\Software\\Python', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One', '\\Software\\Python\\Company Two', '\\Software\\Python\\Company Three', '\\Software\\Python\\Company Four', '\\Software\\Python\\Company Five', 'Missing Tag'] }, + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company One\\Tag1', '\\Software\\Python\\Company One\\Tag2'] }, + { key: '\\Software\\Python\\Company Two', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Two\\Tag A', '\\Software\\Python\\Company Two\\Tag B', '\\Software\\Python\\Company Two\\Tag C'] }, + { key: '\\Software\\Python\\Company Three', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Three\\Tag !'] }, + { key: '\\Software\\Python\\Company Four', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Four\\Four !'] }, + { key: '\\Software\\Python\\Company Five', hive: RegistryHive.HKCU, arch: Architecture.x86, values: ['\\Software\\Python\\Company Five\\Five !'] }, + { key: '\\Software\\Python', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['A'] }, + { key: '\\Software\\Python\\Company A', hive: RegistryHive.HKLM, arch: Architecture.x86, values: ['Another Tag'] } ]; - const registryValues: { key: string, hive: Hive, arch?: Architecture, value: string, name?: string }[] = [ - { key: '\\Software\\Python\\Company One', hive: Hive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, - { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, + const registryValues: { key: string, hive: RegistryHive, arch?: Architecture, value: string, name?: string }[] = [ + { key: '\\Software\\Python\\Company One', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Display Name for Company One', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag1', name: 'Version' }, + { key: '\\Software\\Python\\Company One\\Tag1\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag1', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy') }, - { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy') }, + { key: '\\Software\\Python\\Company One\\Tag2\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'scipy', 'python.exe'), name: 'ExecutablePath' }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path') }, - { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path') }, + { key: '\\Software\\Python\\Company Two\\Tag A\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'Version.Tag A', name: 'Version' }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, - { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, - { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'path2') }, + { key: '\\Software\\Python\\Company Two\\Tag B\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 'DisplayName.Tag B', name: 'DisplayName' }, + { key: '\\Software\\Python\\Company Two\\Tag C\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: undefined }, + { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: undefined }, - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: Hive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, + { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: Hive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } + { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } ]; const registry = new MockRegistry(registryKeys, registryValues); const winRegistry = new WindowsRegistryService(registry, false); diff --git a/src/test/linters/lint.test.ts b/src/test/linters/lint.test.ts index 9126e3c1bf10..ddd6ef04f628 100644 --- a/src/test/linters/lint.test.ts +++ b/src/test/linters/lint.test.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; import { createDeferred } from '../../client/common/helpers'; -import { SettingToDisableProduct } from '../../client/common/installer'; +import { SettingToDisableProduct } from '../../client/common/installer/installer'; import { IInstaller, ILogger, IOutputChannel, Product } from '../../client/common/types'; import { execPythonFile } from '../../client/common/utils'; import { IServiceContainer } from '../../client/ioc/types'; diff --git a/src/test/mocks/condaLocator.ts b/src/test/mocks/condaLocator.ts new file mode 100644 index 000000000000..8c7c74e7d5af --- /dev/null +++ b/src/test/mocks/condaLocator.ts @@ -0,0 +1,14 @@ +import { ICondaLocatorService } from '../../client/interpreter/contracts'; + +export class MockCondaLocator implements ICondaLocatorService { + constructor(private condaFile: string = 'conda', private available: boolean = true, private version: string = '1') { } + public async getCondaFile(): Promise { + return this.condaFile; + } + public async isCondaAvailable(): Promise { + return this.available; + } + public async getCondaVersion(): Promise { + return this.version; + } +} diff --git a/src/test/mocks/moduleInstaller.ts b/src/test/mocks/moduleInstaller.ts new file mode 100644 index 000000000000..998a719ed9af --- /dev/null +++ b/src/test/mocks/moduleInstaller.ts @@ -0,0 +1,16 @@ +import { EventEmitter } from 'events'; +import { Uri } from 'vscode'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { IModuleInstaller } from '../../client/common/installer/types'; + +export class MockModuleInstaller extends EventEmitter implements IModuleInstaller { + constructor(public readonly displayName: string, private supported: boolean) { + super(); + } + public async installModule(name: string): Promise { + this.emit('installModule', name); + } + public async isSupported(resource?: Uri): Promise { + return this.supported; + } +} diff --git a/src/test/mocks/terminalService.ts b/src/test/mocks/terminalService.ts new file mode 100644 index 000000000000..07e5d3e766a9 --- /dev/null +++ b/src/test/mocks/terminalService.ts @@ -0,0 +1,18 @@ +import { injectable } from 'inversify'; +import 'reflect-metadata'; +import { createDeferred, Deferred } from '../../client/common/helpers'; +import { ITerminalService } from '../../client/common/terminal/types'; + +@injectable() +export class MockTerminalService implements ITerminalService { + private deferred: Deferred; + constructor() { + this.deferred = createDeferred(this); + } + public get commandSent(): Promise { + return this.deferred.promise; + } + public sendCommand(command: string, args: string[]): Promise { + return this.deferred.resolve(`${command} ${args.join(' ')}`.trim()); + } +} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index e8ace91f771c..159a26b579e1 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -11,8 +11,9 @@ import { PythonExecutionFactory } from '../client/common/process/pythonExecution import { registerTypes as processRegisterTypes } from '../client/common/process/serviceRegistry'; import { IBufferDecoder, IProcessService, IPythonExecutionFactory } from '../client/common/process/types'; import { registerTypes as commonRegisterTypes } from '../client/common/serviceRegistry'; -import { GLOBAL_MEMENTO, IDiposableRegistry, IMemento, IOutputChannel, WORKSPACE_MEMENTO } from '../client/common/types'; +import { GLOBAL_MEMENTO, IDisposableRegistry, IMemento, IOutputChannel, WORKSPACE_MEMENTO } from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; +import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; @@ -35,7 +36,7 @@ export class IocContainer { this.serviceContainer = new ServiceContainer(cont); this.serviceManager.addSingletonInstance(IServiceContainer, this.serviceContainer); - this.serviceManager.addSingletonInstance(IDiposableRegistry, this.disposables); + this.serviceManager.addSingletonInstance(IDisposableRegistry, this.disposables); this.serviceManager.addSingleton(IMemento, MockMemento, GLOBAL_MEMENTO); this.serviceManager.addSingleton(IMemento, MockMemento, WORKSPACE_MEMENTO); @@ -66,6 +67,9 @@ export class IocContainer { public registerLinterTypes() { lintersRegisterTypes(this.serviceManager); } + public registerFormatterTypes() { + formattersRegisterTypes(this.serviceManager); + } public registerMockProcessTypes() { this.serviceManager.addSingleton(IBufferDecoder, BufferDecoder); diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts index f724f9174bd2..53bd496f471e 100644 --- a/src/test/unittests/mocks.ts +++ b/src/test/unittests/mocks.ts @@ -3,7 +3,7 @@ import { injectable } from 'inversify'; import 'reflect-metadata'; import { CancellationToken, Disposable, Uri } from 'vscode'; import { createDeferred, Deferred } from '../../client/common/helpers'; -import { Product } from '../../client/common/installer'; +import { Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; import { CANCELLATION_REASON } from '../../client/unittests/common/constants'; import { BaseTestManager } from '../../client/unittests/common/managers/baseTestManager'; @@ -55,6 +55,7 @@ export class MockDebugLauncher implements ITestDebugLauncher, Disposable { export class MockTestManagerWithRunningTests extends BaseTestManager { // tslint:disable-next-line:no-any public readonly runnerDeferred = createDeferred(); + public readonly enabled = true; // tslint:disable-next-line:no-any public readonly discoveryDeferred = createDeferred(); constructor(testProvider: TestProvider, product: Product, workspaceFolder: Uri, rootDirectory: string, diff --git a/src/test/unittests/stoppingDiscoverAndTest.test.ts b/src/test/unittests/stoppingDiscoverAndTest.test.ts index 790778bb41c2..3386ee2b6955 100644 --- a/src/test/unittests/stoppingDiscoverAndTest.test.ts +++ b/src/test/unittests/stoppingDiscoverAndTest.test.ts @@ -5,7 +5,7 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import { Uri } from 'vscode'; -import { Product } from '../../client/common/installer'; +import { Product } from '../../client/common/types'; import { CANCELLATION_REASON, CommandSource, UNITTEST_PROVIDER } from '../../client/unittests/common/constants'; import { ITestDiscoveryService } from '../../client/unittests/common/types'; import { initialize, initializeTest } from '../initialize';