diff --git a/news/2 Fixes/4642.md b/news/2 Fixes/4642.md new file mode 100644 index 000000000000..6a541d917370 --- /dev/null +++ b/news/2 Fixes/4642.md @@ -0,0 +1 @@ +Add virtualenvwrapper default virtual environment location to the `python.venvFolders` config setting. diff --git a/package.json b/package.json index 3fec8a14ed6a..8928d43da68b 100644 --- a/package.json +++ b/package.json @@ -2010,12 +2010,8 @@ }, "python.venvFolders": { "type": "array", - "default": [ - "envs", - ".pyenv", - ".direnv" - ], - "description": "Folders in your home directory to look into for virtual environments.", + "default": [], + "description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", "scope": "resource", "items": { "type": "string" diff --git a/src/client/interpreter/locators/services/globalVirtualEnvService.ts b/src/client/interpreter/locators/services/globalVirtualEnvService.ts index 9dd0b827f85e..bcf836350ee9 100644 --- a/src/client/interpreter/locators/services/globalVirtualEnvService.ts +++ b/src/client/interpreter/locators/services/globalVirtualEnvService.ts @@ -7,12 +7,15 @@ import { inject, injectable, named } from 'inversify'; import * as os from 'os'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, ICurrentProcess } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; import { BaseVirtualEnvService } from './baseVirtualEnvService'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + @injectable() export class GlobalVirtualEnvService extends BaseVirtualEnvService { public constructor( @@ -25,17 +28,30 @@ export class GlobalVirtualEnvService extends BaseVirtualEnvService { @injectable() export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { private readonly config: IConfigurationService; + private readonly currentProcess: ICurrentProcess; private readonly virtualEnvMgr: IVirtualEnvironmentManager; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.config = serviceContainer.get(IConfigurationService); this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); + this.currentProcess = serviceContainer.get(ICurrentProcess); } 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)); + const venvFolders = [ + 'envs', + '.pyenv', + '.direnv', + '.virtualenvs', + ...this.config.getSettings(resource).venvFolders]; + const folders = [...new Set(venvFolders.map(item => path.join(homedir, item)))]; + + // Add support for the WORKON_HOME environment variable used by pipenv and virtualenvwrapper. + const workonHomePath = this.currentProcess.env.WORKON_HOME; + if (workonHomePath) { + folders.push(untildify(workonHomePath)); + } // tslint:disable-next-line:no-string-literal const pyenvRoot = await this.virtualEnvMgr.getPyEnvRoot(resource); diff --git a/src/test/interpreters/venv.unit.test.ts b/src/test/interpreters/venv.unit.test.ts index 8ab27cbb71ca..4c0836fb274d 100644 --- a/src/test/interpreters/venv.unit.test.ts +++ b/src/test/interpreters/venv.unit.test.ts @@ -18,6 +18,10 @@ import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { MockAutoSelectionService } from '../mocks/autoSelector'; +// tslint:disable-next-line:no-require-imports no-var-requires +const untildify: (value: string) => string = require('untildify'); + +// tslint:disable-next-line: max-func-body-length suite('Virtual environments', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; @@ -52,11 +56,16 @@ suite('Virtual environments', () => { const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); const homedir = os.homedir(); - const folders = ['Envs', '.virtualenvs']; + const folders = ['Envs', 'testpath']; settings.setup(x => x.venvFolders).returns(() => folders); 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)); + let expected = [ + 'envs', + '.pyenv', + '.direnv', + '.virtualenvs', + ...folders].map(item => path.join(homedir, item)); virtualEnvMgr.verifyAll(); expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.'); @@ -70,6 +79,60 @@ suite('Virtual environments', () => { expect(paths).to.deep.equal(expected, 'pyenv path not resolved correctly.'); }); + test('Global search paths with duplicates', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const folders = ['.virtualenvs', '.direnv']; + settings.setup(x => x.venvFolders).returns(() => folders); + const paths = await pathProvider.getSearchPaths(); + + expect([...new Set(paths)]).to.deep.equal(paths, 'Duplicates are not removed from the list of global search paths'); + }); + + test('Global search paths with tilde path in the WORKON_HOME environment variable', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const homedir = os.homedir(); + const workonFolder = path.join('~', '.workonFolder'); + process.setup(p => p.env).returns(() => { + return { WORKON_HOME: workonFolder }; + }); + settings.setup(x => x.venvFolders).returns(() => []); + + const paths = await pathProvider.getSearchPaths(); + const expected = [ + 'envs', + '.pyenv', + '.direnv', + '.virtualenvs' + ].map(item => path.join(homedir, item)); + expected.push(untildify(workonFolder)); + + expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); + }); + + test('Global search paths with absolute path in the WORKON_HOME environment variable', async () => { + const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); + + const homedir = os.homedir(); + const workonFolder = path.join('path', 'to', '.workonFolder'); + process.setup(p => p.env).returns(() => { + return { WORKON_HOME: workonFolder }; + }); + settings.setup(x => x.venvFolders).returns(() => []); + + const paths = await pathProvider.getSearchPaths(); + const expected = [ + 'envs', + '.pyenv', + '.direnv', + '.virtualenvs' + ].map(item => path.join(homedir, item)); + expected.push(workonFolder); + + expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); + }); + test('Workspace search paths', async () => { settings.setup(x => x.venvPath).returns(() => path.join('~', 'foo'));