diff --git a/package.json b/package.json index 5bcea42a1452..d69de7260ef3 100644 --- a/package.json +++ b/package.json @@ -1297,7 +1297,7 @@ }, "python.linting.flake8CategorySeverity.F": { "type": "string", - "default": "Error", + "default": "Warning", "description": "Severity of Flake8 message type 'F'.", "enum": [ "Hint", @@ -1859,4 +1859,4 @@ "publisherDisplayName": "Microsoft", "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" } -} +} \ No newline at end of file diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 42c60d57175e..ef806ec2ee46 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -168,9 +168,12 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { W: vscode.DiagnosticSeverity.Warning }, flake8CategorySeverity: { - F: vscode.DiagnosticSeverity.Error, E: vscode.DiagnosticSeverity.Error, - W: vscode.DiagnosticSeverity.Warning + W: vscode.DiagnosticSeverity.Warning, + // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code + // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as + // unused imports, variables, etc. + F: vscode.DiagnosticSeverity.Warning }, mypyCategorySeverity: { error: vscode.DiagnosticSeverity.Error, diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 794d9efc04d6..89ec9aac3e9e 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -86,4 +86,11 @@ export class FileSystem implements IFileSystem { return fs.appendFileSync(filename, data, optionsOrEncoding); } + public getRealPathAsync(filePath: string): Promise { + return new Promise(resolve => { + fs.realpath(filePath, (err, realPath) => { + resolve(err ? filePath : realPath); + }); + }); + } } diff --git a/src/client/common/platform/types.ts b/src/client/common/platform/types.ts index 15f1bae1bdfa..b78c8886f91c 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -41,4 +41,5 @@ export interface IFileSystem { appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string; }): void; // tslint:disable-next-line:unified-signatures appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string; }): void; + getRealPathAsync(path: string): Promise; } diff --git a/src/client/interpreter/configuration/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector.ts index 6dc9dc0c7033..607be98c174c 100644 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ b/src/client/interpreter/configuration/interpreterSelector.ts @@ -4,36 +4,47 @@ import { ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri } import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import * as settings from '../../common/configSettings'; import { Commands } from '../../common/constants'; +import { IFileSystem } from '../../common/platform/types'; import { IServiceContainer } from '../../ioc/types'; import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter, WorkspacePythonPath } from '../contracts'; import { IInterpreterSelector, IPythonPathUpdaterServiceManager } from './types'; -interface IInterpreterQuickPickItem extends QuickPickItem { +export interface IInterpreterQuickPickItem extends QuickPickItem { path: string; } @injectable() export class InterpreterSelector implements IInterpreterSelector { private disposables: Disposable[] = []; - private pythonPathUpdaterService: IPythonPathUpdaterServiceManager; private readonly interpreterManager: IInterpreterService; private readonly workspaceService: IWorkspaceService; private readonly applicationShell: IApplicationShell; private readonly documentManager: IDocumentManager; + private readonly fileSystem: IFileSystem; + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.interpreterManager = serviceContainer.get(IInterpreterService); this.workspaceService = this.serviceContainer.get(IWorkspaceService); this.applicationShell = this.serviceContainer.get(IApplicationShell); this.documentManager = this.serviceContainer.get(IDocumentManager); + this.fileSystem = this.serviceContainer.get(IFileSystem); const commandManager = serviceContainer.get(ICommandManager); this.disposables.push(commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this))); this.disposables.push(commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this))); - this.pythonPathUpdaterService = serviceContainer.get(IPythonPathUpdaterServiceManager); } public dispose() { this.disposables.forEach(disposable => disposable.dispose()); } + + public async getSuggestions(resourceUri?: Uri) { + let interpreters = await this.interpreterManager.getInterpreters(resourceUri); + interpreters = await this.removeDuplicates(interpreters); + // tslint:disable-next-line:no-non-null-assertion + interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); + return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); + } + private async getWorkspaceToSetPythonPath(): Promise { if (!Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0) { return undefined; @@ -47,6 +58,7 @@ export class InterpreterSelector implements IInterpreterSelector { const workspaceFolder = await applicationShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; } + private async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise { let detail = suggestion.path; if (workspaceUri && suggestion.path.startsWith(workspaceUri.fsPath)) { @@ -62,11 +74,19 @@ export class InterpreterSelector implements IInterpreterSelector { }; } - private async getSuggestions(resourceUri?: Uri) { - const interpreters = await this.interpreterManager.getInterpreters(resourceUri); - // tslint:disable-next-line:no-non-null-assertion - interpreters.sort((a, b) => a.displayName! > b.displayName! ? 1 : -1); - return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); + private async removeDuplicates(interpreters: PythonInterpreter[]): Promise { + const result: PythonInterpreter[] = []; + await Promise.all(interpreters.filter(async x => { + x.realPath = await this.fileSystem.getRealPathAsync(x.path); + return true; + })); + interpreters.forEach(x => { + if (result.findIndex(a => a.displayName === x.displayName + && a.type === x.type && this.fileSystem.arePathsSame(a.realPath!, x.realPath!)) < 0) { + result.push(x); + } + }); + return result; } private async setInterpreter() { @@ -95,7 +115,8 @@ export class InterpreterSelector implements IInterpreterSelector { const selection = await this.applicationShell.showQuickPick(suggestions, quickPickOptions); if (selection !== undefined) { - await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); + const pythonPathUpdaterService = this.serviceContainer.get(IPythonPathUpdaterServiceManager); + await pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); } } @@ -110,16 +131,17 @@ export class InterpreterSelector implements IInterpreterSelector { const workspaceFolder = this.workspaceService.getWorkspaceFolder(this.documentManager.activeTextEditor!.document.uri); const isWorkspaceChange = Array.isArray(this.workspaceService.workspaceFolders) && this.workspaceService.workspaceFolders.length === 1; + const pythonPathUpdaterService = this.serviceContainer.get(IPythonPathUpdaterServiceManager); if (isGlobalChange) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); + await pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Global, 'shebang'); return; } if (isWorkspaceChange || !workspaceFolder) { - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', this.workspaceService.workspaceFolders![0].uri); + await pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.Workspace, 'shebang', this.workspaceService.workspaceFolders![0].uri); return; } - await this.pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, 'shebang', workspaceFolder.uri); + await pythonPathUpdaterService.updatePythonPath(shebang, ConfigurationTarget.WorkspaceFolder, 'shebang', workspaceFolder.uri); } } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 727415e00b0e..040c910c7373 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -53,8 +53,7 @@ export interface ICondaService { export enum InterpreterType { Unknown = 1, Conda = 2, - VirtualEnv = 4, - VEnv = 8 + VirtualEnv = 4 } export type PythonInterpreter = { @@ -67,6 +66,7 @@ export type PythonInterpreter = { envName?: string; envPath?: string; cachedEntry?: boolean; + realPath?: string; }; export type WorkspacePythonPath = { diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 6ddaf6edc13c..ff5abaddd331 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -81,9 +81,7 @@ export class InterpreterDisplay implements IInterpreterDisplay { } this.statusBar.show(); } - private async getVirtualEnvironmentName(pythonPath: string) { - return this.virtualEnvMgr - .detect(pythonPath) - .then(env => env ? env.name : ''); + private async getVirtualEnvironmentName(pythonPath: string): Promise { + return this.virtualEnvMgr.getEnvironmentName(pythonPath); } } diff --git a/src/client/interpreter/index.ts b/src/client/interpreter/index.ts index 3c23ef5d807e..485766343d3d 100644 --- a/src/client/interpreter/index.ts +++ b/src/client/interpreter/index.ts @@ -102,14 +102,13 @@ export class InterpreterManager implements Disposable, IInterpreterService { const pythonExecutableName = path.basename(fullyQualifiedPath); const versionInfo = await this.serviceContainer.get(IInterpreterVersionService).getVersion(fullyQualifiedPath, pythonExecutableName); const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); - const virtualEnv = await virtualEnvManager.detect(fullyQualifiedPath); - const virtualEnvName = virtualEnv ? virtualEnv.name : ''; + const virtualEnvName = await virtualEnvManager.getEnvironmentName(fullyQualifiedPath); const dislayNameSuffix = virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; const displayName = `${versionInfo}${dislayNameSuffix}`; return { displayName, path: fullyQualifiedPath, - type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown, + type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown, version: versionInfo }; } diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index d6000631acde..fe19dd221f8d 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -67,14 +67,14 @@ export class BaseVirtualEnvService extends CacheableLocatorService { private async getVirtualEnvDetails(interpreter: string): Promise { return Promise.all([ this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.detect(interpreter) + this.virtualEnvMgr.getEnvironmentName(interpreter) ]) - .then(([displayName, virtualEnv]) => { - const virtualEnvSuffix = virtualEnv ? virtualEnv.name : this.getVirtualEnvironmentRootDirectory(interpreter); + .then(([displayName, virtualEnvName]) => { + const virtualEnvSuffix = virtualEnvName.length ? virtualEnvName : this.getVirtualEnvironmentRootDirectory(interpreter); return { displayName: `${displayName} (${virtualEnvSuffix})`.trim(), path: interpreter, - type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown + type: virtualEnvName.length > 0 ? InterpreterType.VirtualEnv : InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts index 62374b78859c..8b31e1968474 100644 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ b/src/client/interpreter/locators/services/currentPathService.ts @@ -37,14 +37,14 @@ export class CurrentPathService extends CacheableLocatorService { private async getInterpreterDetails(interpreter: string): Promise { return Promise.all([ this.versionProvider.getVersion(interpreter, path.basename(interpreter)), - this.virtualEnvMgr.detect(interpreter) + this.virtualEnvMgr.getEnvironmentName(interpreter) ]). - then(([displayName, virtualEnv]) => { - displayName += virtualEnv ? ` (${virtualEnv.name})` : ''; + then(([displayName, virtualEnvName]) => { + displayName += virtualEnvName.length > 0 ? ` (${virtualEnvName})` : ''; return { displayName, path: interpreter, - type: virtualEnv ? virtualEnv.type : InterpreterType.Unknown + type: virtualEnvName ? InterpreterType.VirtualEnv : InterpreterType.Unknown }; }); } diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 7c4c0cbb55c6..56e60c0b6b35 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -41,9 +41,7 @@ import { getKnownSearchPathsForInterpreters, KnownPathsService } from './locator import { WindowsRegistryService } from './locators/services/windowsRegistryService'; import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService'; import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './virtualEnvs/types'; -import { VEnv } from './virtualEnvs/venv'; -import { VirtualEnv } from './virtualEnvs/virtualEnv'; +import { IVirtualEnvironmentManager } from './virtualEnvs/types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingletonInstance(IKnownSearchPathsForInterpreters, getKnownSearchPathsForInterpreters()); @@ -51,9 +49,6 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); serviceManager.addSingleton(ICondaService, CondaService); - serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VirtualEnv); - serviceManager.addSingleton(IVirtualEnvironmentIdentifier, VEnv); - serviceManager.addSingleton(IVirtualEnvironmentManager, VirtualEnvironmentManager); serviceManager.addSingleton(IInterpreterVersionService, InterpreterVersionService); diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index 01a538fbb012..af5545398696 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -1,21 +1,26 @@ -import { injectable, multiInject } from 'inversify'; -import { IVirtualEnvironmentIdentifier, IVirtualEnvironmentManager } from './types'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IProcessService } from '../../common/process/types'; +import { IServiceContainer } from '../../ioc/types'; +import { IVirtualEnvironmentManager } from './types'; @injectable() export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { - constructor(@multiInject(IVirtualEnvironmentIdentifier) private envs: IVirtualEnvironmentIdentifier[]) { + private processService: IProcessService; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.processService = serviceContainer.get(IProcessService); } - public detect(pythonPath: string): Promise { - const promises = this.envs - .map(item => item.detect(pythonPath) - .then(result => { - return { env: item, result }; - })); - - return Promise.all(promises) - .then(results => { - const env = results.find(items => items.result === true); - return env ? env.env : undefined; - }); + public async getEnvironmentName(pythonPath: string): Promise { + // https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv + const output = await this.processService.exec(pythonPath, ['-c', 'import sys;print(hasattr(sys, "real_prefix"))']); + if (output.stdout.length > 0) { + const result = output.stdout.trim(); + if (result === 'True') { + return 'virtualenv'; + } + } + return ''; } } diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index 10c44f9e8e96..971772fd009d 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -1,15 +1,7 @@ // 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; + getEnvironmentName(pythonPath: string): Promise; } diff --git a/src/client/interpreter/virtualEnvs/venv.ts b/src/client/interpreter/virtualEnvs/venv.ts deleted file mode 100644 index 3fca7bb4f2c0..000000000000 --- a/src/client/interpreter/virtualEnvs/venv.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { IFileSystem } from '../../common/platform/types'; -import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType } from '../contracts'; -import { IVirtualEnvironmentIdentifier } from './types'; - -const pyEnvCfgFileName = 'pyvenv.cfg'; - -@injectable() -export class VEnv implements IVirtualEnvironmentIdentifier { - public readonly name: string = 'venv'; - public readonly type = InterpreterType.VEnv; - private fs: IFileSystem; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.fs = serviceContainer.get(IFileSystem); - } - public detect(pythonPath: string): Promise { - const dir = path.dirname(pythonPath); - const pyEnvCfgPath = path.join(dir, '..', pyEnvCfgFileName); - return this.fs.fileExistsAsync(pyEnvCfgPath); - } -} diff --git a/src/client/interpreter/virtualEnvs/virtualEnv.ts b/src/client/interpreter/virtualEnvs/virtualEnv.ts deleted file mode 100644 index e1dada31a344..000000000000 --- a/src/client/interpreter/virtualEnvs/virtualEnv.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { IFileSystem } from '../../common/platform/types'; -import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType } from '../contracts'; -import { IVirtualEnvironmentIdentifier } from './types'; - -@injectable() -export class VirtualEnv implements IVirtualEnvironmentIdentifier { - public readonly name: string = 'virtualenv'; - public readonly type = InterpreterType.VirtualEnv; - private fs: IFileSystem; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.fs = serviceContainer.get(IFileSystem); - } - public async detect(pythonPath: string): Promise { - const dir = path.dirname(pythonPath); - const libExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'lib')); - const binExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'bin')); - const includeExists = await this.fs.directoryExistsAsync(path.join(dir, '..', 'include')); - return libExists && binExists && includeExists; - } -} diff --git a/src/test/configuration/interpreterSelector.test.ts b/src/test/configuration/interpreterSelector.test.ts new file mode 100644 index 000000000000..b53dd61c79d2 --- /dev/null +++ b/src/test/configuration/interpreterSelector.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { IFileSystem } from '../../client/common/platform/types'; +import { IInterpreterQuickPickItem, InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; +import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { ServiceContainer } from '../../client/ioc/container'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceContainer } from '../../client/ioc/types'; + +class InterpreterQuickPickItem implements IInterpreterQuickPickItem { + public path: string; + public label: string; + public description: string; + public detail?: string; + constructor(l: string, p: string) { + this.path = p; + this.label = l; + } +} + +// tslint:disable-next-line:max-func-body-length +suite('Intepreters - selector', () => { + let serviceContainer: IServiceContainer; + let workspace: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; + + setup(() => { + const cont = new Container(); + const serviceManager = new ServiceManager(cont); + serviceContainer = new ServiceContainer(cont); + + workspace = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); + + appShell = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IApplicationShell, appShell.object); + + interpreterService = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); + + documentManager = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + + fileSystem = TypeMoq.Mock.ofType(); + fileSystem + .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) + .returns((a: string, b: string) => a === b); + fileSystem + .setup(x => x.getRealPathAsync(TypeMoq.It.isAnyString())) + .returns((a: string) => new Promise(resolve => resolve(a))); + + serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + + const commandManager = TypeMoq.Mock.ofType(); + serviceManager.addSingletonInstance(ICommandManager, commandManager.object); + }); + + test('Suggestions', async () => { + const initial: PythonInterpreter[] = [ + { displayName: '1', path: 'path1', type: InterpreterType.Unknown }, + { displayName: '2', path: 'path1', type: InterpreterType.Unknown }, + { displayName: '1', path: 'path1', type: InterpreterType.Unknown }, + { displayName: '2', path: 'path2', type: InterpreterType.Unknown }, + { displayName: '2', path: 'path2', type: InterpreterType.Unknown }, + { displayName: '2 (virtualenv)', path: 'path2', type: InterpreterType.VirtualEnv }, + { displayName: '3', path: 'path2', type: InterpreterType.Unknown }, + { displayName: '4', path: 'path4', type: InterpreterType.Conda } + ]; + interpreterService + .setup(x => x.getInterpreters(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve) => resolve(initial))); + + const selector = new InterpreterSelector(serviceContainer); + const actual = await selector.getSuggestions(); + + const expected: InterpreterQuickPickItem[] = [ + new InterpreterQuickPickItem('1', 'path1'), + new InterpreterQuickPickItem('2', 'path1'), + new InterpreterQuickPickItem('2', 'path2'), + new InterpreterQuickPickItem('2 (virtualenv)', 'path2'), + new InterpreterQuickPickItem('3', 'path2'), + new InterpreterQuickPickItem('4', 'path4') + ]; + + assert.equal(actual.length, expected.length, 'Suggestion lengths are different.'); + for (let i = 0; i < expected.length; i += 1) { + assert.equal(actual[i].label, expected[i].label, + `Suggestion label is different at ${i}: exected '${expected[i].label}', found '${actual[i].label}'.`); + assert.equal(actual[i].path, expected[i].path, + `Suggestion path is different at ${i}: exected '${expected[i].path}', found '${actual[i].path}'.`); + } + }); +}); diff --git a/src/test/debugger/configProvider/provider.test.ts b/src/test/debugger/configProvider/provider.test.ts index 6f894cca86a2..7940fb268886 100644 --- a/src/test/debugger/configProvider/provider.test.ts +++ b/src/test/debugger/configProvider/provider.test.ts @@ -72,8 +72,10 @@ import { IServiceContainer } from '../../../client/ioc/types'; expect(debugConfig).to.have.property('type', provider.debugType); expect(debugConfig).to.have.property('request', 'launch'); expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd', __dirname); - expect(debugConfig).to.have.property('envFile', path.join(__dirname, '.env')); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env').toLowerCase()); expect(debugConfig).to.have.property('env'); // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); @@ -92,8 +94,10 @@ import { IServiceContainer } from '../../../client/ioc/types'; expect(debugConfig).to.have.property('type', provider.debugType); expect(debugConfig).to.have.property('request', 'launch'); expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd', __dirname); - expect(debugConfig).to.have.property('envFile', path.join(__dirname, '.env')); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env').toLowerCase()); expect(debugConfig).to.have.property('env'); // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); @@ -106,14 +110,17 @@ import { IServiceContainer } from '../../../client/ioc/types'; setupWorkspaces([]); const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const filePath = Uri.file(path.dirname('')).fsPath; expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', provider.debugType); expect(debugConfig).to.have.property('request', 'launch'); expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd', Uri.file(path.dirname('')).fsPath); - expect(debugConfig).to.have.property('envFile', path.join(Uri.file(path.dirname('')).fsPath, '.env')); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env').toLowerCase()); expect(debugConfig).to.have.property('env'); // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); @@ -166,14 +173,17 @@ import { IServiceContainer } from '../../../client/ioc/types'; setupWorkspaces([defaultWorkspace]); const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); + const filePath = Uri.file(defaultWorkspace).fsPath; expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); expect(debugConfig).to.have.property('pythonPath', pythonPath); expect(debugConfig).to.have.property('type', provider.debugType); expect(debugConfig).to.have.property('request', 'launch'); expect(debugConfig).to.have.property('program', activeFile); - expect(debugConfig).to.have.property('cwd', Uri.file(defaultWorkspace).fsPath); - expect(debugConfig).to.have.property('envFile', path.join(Uri.file(defaultWorkspace).fsPath, '.env')); + expect(debugConfig).to.have.property('cwd'); + expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); + expect(debugConfig).to.have.property('envFile'); + expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env').toLowerCase()); expect(debugConfig).to.have.property('env'); // tslint:disable-next-line:no-any expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); diff --git a/src/test/interpreters/display.test.ts b/src/test/interpreters/display.test.ts index e83ad98ee6cd..fad85341c2a4 100644 --- a/src/test/interpreters/display.test.ts +++ b/src/test/interpreters/display.test.ts @@ -104,7 +104,7 @@ suite('Interpreters Display', () => { statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(expectedTooltip)!, TypeMoq.Times.once()); }); - test('If interpreter is not idenfied then tooltip should point to python Path and text containing the folder name', async () => { + test('If interpreter is not identified then tooltip should point to python Path and text containing the folder name', async () => { const resource = Uri.file('x'); const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); const workspaceFolder = Uri.file('workspace'); @@ -113,7 +113,7 @@ suite('Interpreters Display', () => { interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(undefined)); configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); await interpreterDisplay.refresh(resource); @@ -121,7 +121,7 @@ suite('Interpreters Display', () => { statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(pythonPath), TypeMoq.Times.once()); statusBar.verify(s => s.text = TypeMoq.It.isValue(`${path.basename(pythonPath)} [Environment]`), TypeMoq.Times.once()); }); - test('If virtual environment interpreter is not idenfied then text should contain the type of virtual environment', async () => { + test('If virtual environment interpreter is not identified then text should contain the type of virtual environment', async () => { const resource = Uri.file('x'); const pythonPath = path.join('user', 'development', 'env', 'bin', 'python'); const workspaceFolder = Uri.file('workspace'); @@ -131,7 +131,7 @@ suite('Interpreters Display', () => { configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ name: 'Mock Name' } as any)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Name')); versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns((_path, defaultDisplayName) => Promise.resolve(defaultDisplayName)); await interpreterDisplay.refresh(resource); @@ -152,7 +152,7 @@ suite('Interpreters Display', () => { fileSystem.setup(f => f.fileExistsAsync(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); - virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); @@ -173,7 +173,7 @@ suite('Interpreters Display', () => { const defaultDisplayName = `${path.basename(pythonPath)} [Environment]`; versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(defaultDisplayName)); // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve({ name: 'Mock Env Name' } as any)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('Mock Env Name')); const expectedText = `${defaultDisplayName} (Mock Env Name)`; await interpreterDisplay.refresh(resource); @@ -194,7 +194,7 @@ suite('Interpreters Display', () => { const displayName = 'Version from Interperter'; versionProvider.setup(v => v.getVersion(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())).returns(() => Promise.resolve(displayName)); // tslint:disable-next-line:no-any - virtualEnvMgr.setup(v => v.detect(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(undefined)); + virtualEnvMgr.setup(v => v.getEnvironmentName(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve('')); await interpreterDisplay.refresh(resource); diff --git a/src/test/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index 3919ebfb407f..2c79853a15ce 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -1,8 +1,7 @@ import { injectable } from 'inversify'; import { Architecture, IRegistry, RegistryHive } from '../../client/common/platform/types'; import { IPersistentState } from '../../client/common/types'; -import { IInterpreterVersionService, InterpreterType } from '../../client/interpreter/contracts'; -import { IVirtualEnvironmentIdentifier } from '../../client/interpreter/virtualEnvs/types'; +import { IInterpreterVersionService } from '../../client/interpreter/contracts'; @injectable() export class MockRegistry implements IRegistry { @@ -37,15 +36,6 @@ export class MockRegistry implements IRegistry { } } -@injectable() -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); - } -} - // tslint:disable-next-line:max-classes-per-file @injectable() export class MockInterpreterVersionProvider implements IInterpreterVersionService {