diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index b7787c26bce4..b71d2416f412 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -22,7 +22,7 @@ export interface IKnownSearchPathsForInterpreters { } export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider'); export interface IVirtualEnvironmentsSearchPathProvider { - getSearchPaths(resource?: Uri): string[]; + getSearchPaths(resource?: Uri): Promise; } export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts index dca6823df3c1..c53270b5b009 100644 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/baseVirtualEnvService.ts @@ -29,7 +29,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService { return this.suggestionsFromKnownVenvs(resource); } private async suggestionsFromKnownVenvs(resource?: Uri) { - const searchPaths = this.searchPathsProvider.getSearchPaths(resource); + const searchPaths = await this.searchPathsProvider.getSearchPaths(resource); return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir, resource))) .then(listOfInterpreters => _.flatten(listOfInterpreters)); } diff --git a/src/client/interpreter/locators/services/globalVirtualEnvService.ts b/src/client/interpreter/locators/services/globalVirtualEnvService.ts index 9812cc72bed1..9dd0b827f85e 100644 --- a/src/client/interpreter/locators/services/globalVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/globalVirtualEnvService.ts @@ -7,9 +7,10 @@ import { inject, injectable, named } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService, ICurrentProcess } from '../../../common/types'; +import { IConfigurationService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; +import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { BaseVirtualEnvService } from './baseVirtualEnvService'; @injectable() @@ -23,31 +24,24 @@ export class GlobalVirtualEnvService extends BaseVirtualEnvService { @injectable() export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { - private readonly process: ICurrentProcess; private readonly config: IConfigurationService; + private readonly virtualEnvMgr: IVirtualEnvironmentManager; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.process = serviceContainer.get(ICurrentProcess); this.config = serviceContainer.get(IConfigurationService); + this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); } - public getSearchPaths(resource?: Uri): string[] { + public async getSearchPaths(resource?: Uri): Promise { const homedir = os.homedir(); const venvFolders = this.config.getSettings(resource).venvFolders; const folders = venvFolders.map(item => path.join(homedir, item)); // tslint:disable-next-line:no-string-literal - const pyenvRoot = this.process.env['PYENV_ROOT']; + const pyenvRoot = await this.virtualEnvMgr.getPyEnvRoot(resource); if (pyenvRoot) { folders.push(pyenvRoot); folders.push(path.join(pyenvRoot, 'versions')); - } else { - // Check if .pyenv/versions is in the list - const pyenvVersions = path.join('.pyenv', 'versions'); - if (venvFolders.indexOf('.pyenv') >= 0 && venvFolders.indexOf(pyenvVersions) < 0) { - // if .pyenv is in the list, but .pyenv/versions is not, add it. - folders.push(path.join(homedir, pyenvVersions)); - } } return folders; } diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts index 97eb6813dd45..f122225b8ee1 100644 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts @@ -28,7 +28,7 @@ export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualE public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public getSearchPaths(resource?: Uri): string[] { + public async getSearchPaths(resource?: Uri): Promise { const configService = this.serviceContainer.get(IConfigurationService); const paths: string[] = []; const venvPath = configService.getSettings(resource).venvPath; diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts index 5ff0efeda8a1..d2160a584042 100644 --- a/src/client/interpreter/virtualEnvs/index.ts +++ b/src/client/interpreter/virtualEnvs/index.ts @@ -4,9 +4,11 @@ import { inject, injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; +import { noop } from '../../../utils/misc'; import { IWorkspaceService } from '../../common/application/types'; import { IFileSystem } from '../../common/platform/types'; import { IProcessServiceFactory } from '../../common/process/types'; +import { ICurrentProcess, IPathUtils } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { InterpreterType, IPipEnvService } from '../contracts'; import { IVirtualEnvironmentManager } from './types'; @@ -20,7 +22,7 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { private fs: IFileSystem; private pyEnvRoot?: string; private workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); this.fs = serviceContainer.get(IFileSystem); this.pipEnvService = serviceContainer.get(IPipEnvService); @@ -66,16 +68,27 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { // Lets not try to determine whether this is a conda environment or not. return InterpreterType.Unknown; } - private async getPyEnvRoot(resource?: Uri): Promise { + public async getPyEnvRoot(resource?: Uri): Promise { if (this.pyEnvRoot) { return this.pyEnvRoot; } + + const currentProccess = this.serviceContainer.get(ICurrentProcess); + const pyenvRoot = currentProccess.env.PYENV_ROOT; + if (pyenvRoot) { + return this.pyEnvRoot = pyenvRoot; + } + try { const processService = await this.processServiceFactory.create(resource); const output = await processService.exec('pyenv', ['root']); - return this.pyEnvRoot = output.stdout.trim(); + if (output.stdout.trim().length > 0) { + return this.pyEnvRoot = output.stdout.trim(); + } } catch { - return; + noop(); } + const pathUtils = this.serviceContainer.get(IPathUtils); + return this.pyEnvRoot = path.join(pathUtils.home, '.pyenv'); } } diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts index e13336760bf9..98865f0edc85 100644 --- a/src/client/interpreter/virtualEnvs/types.ts +++ b/src/client/interpreter/virtualEnvs/types.ts @@ -8,4 +8,5 @@ export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); export interface IVirtualEnvironmentManager { getEnvironmentName(pythonPath: string, resource?: Uri): Promise; getEnvironmentType(pythonPath: string, resource?: Uri): Promise; + getPyEnvRoot(resource?: Uri): Promise; } diff --git a/src/test/interpreters/venv.unit.test.ts b/src/test/interpreters/venv.unit.test.ts index 75ac86b8a615..fa4e64c2ffb6 100644 --- a/src/test/interpreters/venv.unit.test.ts +++ b/src/test/interpreters/venv.unit.test.ts @@ -10,9 +10,9 @@ import { Uri, WorkspaceFolder } from 'vscode'; import { IWorkspaceService } from '../../client/common/application/types'; import { PlatformService } from '../../client/common/platform/platformService'; import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../client/common/types'; -import { EnvironmentVariables } from '../../client/common/variables/types'; import { GlobalVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/globalVirtualEnvService'; import { WorkspaceVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/workspaceVirtualEnvService'; +import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; @@ -23,6 +23,7 @@ suite('Virtual environments', () => { let config: TypeMoq.IMock; let workspace: TypeMoq.IMock; let process: TypeMoq.IMock; + let virtualEnvMgr: TypeMoq.IMock; setup(() => { const cont = new Container(); @@ -33,39 +34,36 @@ suite('Virtual environments', () => { config = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); process = TypeMoq.Mock.ofType(); + virtualEnvMgr = TypeMoq.Mock.ofType(); config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); serviceManager.addSingletonInstance(IConfigurationService, config.object); serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); serviceManager.addSingletonInstance(ICurrentProcess, process.object); + serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); }); test('Global search paths', async () => { const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); const homedir = os.homedir(); - const folders = ['Envs', '.virtualenvs', '.pyenv']; + const folders = ['Envs', '.virtualenvs']; settings.setup(x => x.venvFolders).returns(() => folders); - - let paths = pathProvider.getSearchPaths(); + virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + let paths = await pathProvider.getSearchPaths(); let expected = folders.map(item => path.join(homedir, item)); - expected.push(path.join(homedir, '.pyenv', 'versions')); + virtualEnvMgr.verifyAll(); expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.'); - const envMap: EnvironmentVariables = {}; - process.setup(x => x.env).returns(() => envMap); - - const customFolder = path.join(homedir, 'some_folder'); - // tslint:disable-next-line:no-string-literal - envMap['PYENV_ROOT'] = customFolder; - paths = pathProvider.getSearchPaths(); + virtualEnvMgr.reset(); + virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve('pyenv_path')); + paths = await pathProvider.getSearchPaths(); - expected = folders.map(item => path.join(homedir, item)); - expected.push(customFolder); - expected.push(path.join(customFolder, 'versions')); - expect(paths).to.deep.equal(expected, 'PYENV_ROOT not resolved correctly.'); + virtualEnvMgr.verifyAll(); + expected = expected.concat(['pyenv_path', path.join('pyenv_path', 'versions')]); + expect(paths).to.deep.equal(expected, 'pyenv path not resolved correctly.'); }); test('Workspace search paths', async () => { @@ -81,7 +79,7 @@ suite('Virtual environments', () => { workspace.setup(x => x.workspaceFolders).returns(() => [wsRoot.object, folder1.object]); const pathProvider = new WorkspaceVirtualEnvironmentsSearchPathProvider(serviceContainer); - const paths = pathProvider.getSearchPaths(Uri.file('')); + const paths = await pathProvider.getSearchPaths(Uri.file('')); const homedir = os.homedir(); const isWindows = new PlatformService(); diff --git a/src/test/interpreters/virtualEnvs/index.unit.test.ts b/src/test/interpreters/virtualEnvs/index.unit.test.ts new file mode 100644 index 000000000000..988ef3ba288e --- /dev/null +++ b/src/test/interpreters/virtualEnvs/index.unit.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { ICurrentProcess, IPathUtils } from '../../../client/common/types'; +import { VirtualEnvironmentManager } from '../../../client/interpreter/virtualEnvs'; +import { IVirtualEnvironmentManager } from '../../../client/interpreter/virtualEnvs/types'; +import { IServiceContainer } from '../../../client/ioc/types'; + +suite('Virtual Environment Manager', () => { + let process: TypeMoq.IMock; + let processService: TypeMoq.IMock; + let pathUtils: TypeMoq.IMock; + let virtualEnvMgr: IVirtualEnvironmentManager; + + setup(() => { + const serviceContainer = TypeMoq.Mock.ofType(); + process = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + const processFactory = TypeMoq.Mock.ofType(); + pathUtils = TypeMoq.Mock.ofType(); + + processService.setup(p => (p as any).then).returns(() => undefined); + processFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => processFactory.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => process.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + + virtualEnvMgr = new VirtualEnvironmentManager(serviceContainer.object); + }); + + test('Get PyEnv Root from PYENV_ROOT', async () => { + process + .setup(p => p.env) + .returns(() => { return { PYENV_ROOT: 'yes' }; }) + .verifiable(TypeMoq.Times.once()); + + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + expect(pyenvRoot).to.equal('yes'); + }); + + test('Get PyEnv Root from current PYENV_ROOT', async () => { + process + .setup(p => p.env) + .returns(() => { return {}; }) + .verifiable(TypeMoq.Times.once()); + processService + .setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) + .returns(() => Promise.resolve({ stdout: 'PROC' })) + .verifiable(TypeMoq.Times.once()); + + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + processService.verifyAll(); + expect(pyenvRoot).to.equal('PROC'); + }); + + test('Get default PyEnv Root path', async () => { + process + .setup(p => p.env) + .returns(() => { return {}; }) + .verifiable(TypeMoq.Times.once()); + processService + .setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) + .returns(() => Promise.resolve({ stdout: '', stderr: 'err' })) + .verifiable(TypeMoq.Times.once()); + pathUtils + .setup(p => p.home) + .returns(() => 'HOME') + .verifiable(TypeMoq.Times.once()); + const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); + + process.verifyAll(); + processService.verifyAll(); + expect(pyenvRoot).to.equal(path.join('HOME', '.pyenv')); + }); +});