Skip to content

Commit 8c1661f

Browse files
authored
Get pyenv root directory from central location (microsoft#2619)
For microsoft#1351
1 parent 082d69b commit 8c1661f

File tree

8 files changed

+129
-36
lines changed

8 files changed

+129
-36
lines changed

src/client/interpreter/contracts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface IKnownSearchPathsForInterpreters {
2222
}
2323
export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider');
2424
export interface IVirtualEnvironmentsSearchPathProvider {
25-
getSearchPaths(resource?: Uri): string[];
25+
getSearchPaths(resource?: Uri): Promise<string[]>;
2626
}
2727
export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService');
2828

src/client/interpreter/locators/services/baseVirtualEnvService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class BaseVirtualEnvService extends CacheableLocatorService {
2929
return this.suggestionsFromKnownVenvs(resource);
3030
}
3131
private async suggestionsFromKnownVenvs(resource?: Uri) {
32-
const searchPaths = this.searchPathsProvider.getSearchPaths(resource);
32+
const searchPaths = await this.searchPathsProvider.getSearchPaths(resource);
3333
return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir, resource)))
3434
.then(listOfInterpreters => _.flatten(listOfInterpreters));
3535
}

src/client/interpreter/locators/services/globalVirtualEnvService.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { inject, injectable, named } from 'inversify';
77
import * as os from 'os';
88
import * as path from 'path';
99
import { Uri } from 'vscode';
10-
import { IConfigurationService, ICurrentProcess } from '../../../common/types';
10+
import { IConfigurationService } from '../../../common/types';
1111
import { IServiceContainer } from '../../../ioc/types';
1212
import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts';
13+
import { IVirtualEnvironmentManager } from '../../virtualEnvs/types';
1314
import { BaseVirtualEnvService } from './baseVirtualEnvService';
1415

1516
@injectable()
@@ -23,31 +24,24 @@ export class GlobalVirtualEnvService extends BaseVirtualEnvService {
2324

2425
@injectable()
2526
export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider {
26-
private readonly process: ICurrentProcess;
2727
private readonly config: IConfigurationService;
28+
private readonly virtualEnvMgr: IVirtualEnvironmentManager;
2829

2930
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
30-
this.process = serviceContainer.get<ICurrentProcess>(ICurrentProcess);
3131
this.config = serviceContainer.get<IConfigurationService>(IConfigurationService);
32+
this.virtualEnvMgr = serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager);
3233
}
3334

34-
public getSearchPaths(resource?: Uri): string[] {
35+
public async getSearchPaths(resource?: Uri): Promise<string[]> {
3536
const homedir = os.homedir();
3637
const venvFolders = this.config.getSettings(resource).venvFolders;
3738
const folders = venvFolders.map(item => path.join(homedir, item));
3839

3940
// tslint:disable-next-line:no-string-literal
40-
const pyenvRoot = this.process.env['PYENV_ROOT'];
41+
const pyenvRoot = await this.virtualEnvMgr.getPyEnvRoot(resource);
4142
if (pyenvRoot) {
4243
folders.push(pyenvRoot);
4344
folders.push(path.join(pyenvRoot, 'versions'));
44-
} else {
45-
// Check if .pyenv/versions is in the list
46-
const pyenvVersions = path.join('.pyenv', 'versions');
47-
if (venvFolders.indexOf('.pyenv') >= 0 && venvFolders.indexOf(pyenvVersions) < 0) {
48-
// if .pyenv is in the list, but .pyenv/versions is not, add it.
49-
folders.push(path.join(homedir, pyenvVersions));
50-
}
5145
}
5246
return folders;
5347
}

src/client/interpreter/locators/services/workspaceVirtualEnvService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualE
2828
public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
2929

3030
}
31-
public getSearchPaths(resource?: Uri): string[] {
31+
public async getSearchPaths(resource?: Uri): Promise<string[]> {
3232
const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService);
3333
const paths: string[] = [];
3434
const venvPath = configService.getSettings(resource).venvPath;

src/client/interpreter/virtualEnvs/index.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import { inject, injectable } from 'inversify';
55
import * as path from 'path';
66
import { Uri } from 'vscode';
7+
import { noop } from '../../../utils/misc';
78
import { IWorkspaceService } from '../../common/application/types';
89
import { IFileSystem } from '../../common/platform/types';
910
import { IProcessServiceFactory } from '../../common/process/types';
11+
import { ICurrentProcess, IPathUtils } from '../../common/types';
1012
import { IServiceContainer } from '../../ioc/types';
1113
import { InterpreterType, IPipEnvService } from '../contracts';
1214
import { IVirtualEnvironmentManager } from './types';
@@ -20,7 +22,7 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager {
2022
private fs: IFileSystem;
2123
private pyEnvRoot?: string;
2224
private workspaceService: IWorkspaceService;
23-
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
25+
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {
2426
this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory);
2527
this.fs = serviceContainer.get<IFileSystem>(IFileSystem);
2628
this.pipEnvService = serviceContainer.get<IPipEnvService>(IPipEnvService);
@@ -66,16 +68,27 @@ export class VirtualEnvironmentManager implements IVirtualEnvironmentManager {
6668
// Lets not try to determine whether this is a conda environment or not.
6769
return InterpreterType.Unknown;
6870
}
69-
private async getPyEnvRoot(resource?: Uri): Promise<string | undefined> {
71+
public async getPyEnvRoot(resource?: Uri): Promise<string | undefined> {
7072
if (this.pyEnvRoot) {
7173
return this.pyEnvRoot;
7274
}
75+
76+
const currentProccess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess);
77+
const pyenvRoot = currentProccess.env.PYENV_ROOT;
78+
if (pyenvRoot) {
79+
return this.pyEnvRoot = pyenvRoot;
80+
}
81+
7382
try {
7483
const processService = await this.processServiceFactory.create(resource);
7584
const output = await processService.exec('pyenv', ['root']);
76-
return this.pyEnvRoot = output.stdout.trim();
85+
if (output.stdout.trim().length > 0) {
86+
return this.pyEnvRoot = output.stdout.trim();
87+
}
7788
} catch {
78-
return;
89+
noop();
7990
}
91+
const pathUtils = this.serviceContainer.get<IPathUtils>(IPathUtils);
92+
return this.pyEnvRoot = path.join(pathUtils.home, '.pyenv');
8093
}
8194
}

src/client/interpreter/virtualEnvs/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager');
88
export interface IVirtualEnvironmentManager {
99
getEnvironmentName(pythonPath: string, resource?: Uri): Promise<string>;
1010
getEnvironmentType(pythonPath: string, resource?: Uri): Promise<InterpreterType>;
11+
getPyEnvRoot(resource?: Uri): Promise<string | undefined>;
1112
}

src/test/interpreters/venv.unit.test.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { Uri, WorkspaceFolder } from 'vscode';
1010
import { IWorkspaceService } from '../../client/common/application/types';
1111
import { PlatformService } from '../../client/common/platform/platformService';
1212
import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../client/common/types';
13-
import { EnvironmentVariables } from '../../client/common/variables/types';
1413
import { GlobalVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/globalVirtualEnvService';
1514
import { WorkspaceVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/workspaceVirtualEnvService';
15+
import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types';
1616
import { ServiceContainer } from '../../client/ioc/container';
1717
import { ServiceManager } from '../../client/ioc/serviceManager';
1818

@@ -23,6 +23,7 @@ suite('Virtual environments', () => {
2323
let config: TypeMoq.IMock<IConfigurationService>;
2424
let workspace: TypeMoq.IMock<IWorkspaceService>;
2525
let process: TypeMoq.IMock<ICurrentProcess>;
26+
let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>;
2627

2728
setup(() => {
2829
const cont = new Container();
@@ -33,39 +34,36 @@ suite('Virtual environments', () => {
3334
config = TypeMoq.Mock.ofType<IConfigurationService>();
3435
workspace = TypeMoq.Mock.ofType<IWorkspaceService>();
3536
process = TypeMoq.Mock.ofType<ICurrentProcess>();
37+
virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>();
3638

3739
config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object);
3840

3941
serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object);
4042
serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object);
4143
serviceManager.addSingletonInstance<ICurrentProcess>(ICurrentProcess, process.object);
44+
serviceManager.addSingletonInstance<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, virtualEnvMgr.object);
4245
});
4346

4447
test('Global search paths', async () => {
4548
const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer);
4649

4750
const homedir = os.homedir();
48-
const folders = ['Envs', '.virtualenvs', '.pyenv'];
51+
const folders = ['Envs', '.virtualenvs'];
4952
settings.setup(x => x.venvFolders).returns(() => folders);
50-
51-
let paths = pathProvider.getSearchPaths();
53+
virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined));
54+
let paths = await pathProvider.getSearchPaths();
5255
let expected = folders.map(item => path.join(homedir, item));
53-
expected.push(path.join(homedir, '.pyenv', 'versions'));
5456

57+
virtualEnvMgr.verifyAll();
5558
expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.');
5659

57-
const envMap: EnvironmentVariables = {};
58-
process.setup(x => x.env).returns(() => envMap);
59-
60-
const customFolder = path.join(homedir, 'some_folder');
61-
// tslint:disable-next-line:no-string-literal
62-
envMap['PYENV_ROOT'] = customFolder;
63-
paths = pathProvider.getSearchPaths();
60+
virtualEnvMgr.reset();
61+
virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve('pyenv_path'));
62+
paths = await pathProvider.getSearchPaths();
6463

65-
expected = folders.map(item => path.join(homedir, item));
66-
expected.push(customFolder);
67-
expected.push(path.join(customFolder, 'versions'));
68-
expect(paths).to.deep.equal(expected, 'PYENV_ROOT not resolved correctly.');
64+
virtualEnvMgr.verifyAll();
65+
expected = expected.concat(['pyenv_path', path.join('pyenv_path', 'versions')]);
66+
expect(paths).to.deep.equal(expected, 'pyenv path not resolved correctly.');
6967
});
7068

7169
test('Workspace search paths', async () => {
@@ -81,7 +79,7 @@ suite('Virtual environments', () => {
8179
workspace.setup(x => x.workspaceFolders).returns(() => [wsRoot.object, folder1.object]);
8280

8381
const pathProvider = new WorkspaceVirtualEnvironmentsSearchPathProvider(serviceContainer);
84-
const paths = pathProvider.getSearchPaths(Uri.file(''));
82+
const paths = await pathProvider.getSearchPaths(Uri.file(''));
8583

8684
const homedir = os.homedir();
8785
const isWindows = new PlatformService();
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
// tslint:disable:no-any
7+
8+
import { expect } from 'chai';
9+
import * as path from 'path';
10+
import * as TypeMoq from 'typemoq';
11+
import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types';
12+
import { ICurrentProcess, IPathUtils } from '../../../client/common/types';
13+
import { VirtualEnvironmentManager } from '../../../client/interpreter/virtualEnvs';
14+
import { IVirtualEnvironmentManager } from '../../../client/interpreter/virtualEnvs/types';
15+
import { IServiceContainer } from '../../../client/ioc/types';
16+
17+
suite('Virtual Environment Manager', () => {
18+
let process: TypeMoq.IMock<ICurrentProcess>;
19+
let processService: TypeMoq.IMock<IProcessService>;
20+
let pathUtils: TypeMoq.IMock<IPathUtils>;
21+
let virtualEnvMgr: IVirtualEnvironmentManager;
22+
23+
setup(() => {
24+
const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
25+
process = TypeMoq.Mock.ofType<ICurrentProcess>();
26+
processService = TypeMoq.Mock.ofType<IProcessService>();
27+
const processFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>();
28+
pathUtils = TypeMoq.Mock.ofType<IPathUtils>();
29+
30+
processService.setup(p => (p as any).then).returns(() => undefined);
31+
processFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object));
32+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => processFactory.object);
33+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => process.object);
34+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object);
35+
36+
virtualEnvMgr = new VirtualEnvironmentManager(serviceContainer.object);
37+
});
38+
39+
test('Get PyEnv Root from PYENV_ROOT', async () => {
40+
process
41+
.setup(p => p.env)
42+
.returns(() => { return { PYENV_ROOT: 'yes' }; })
43+
.verifiable(TypeMoq.Times.once());
44+
45+
const pyenvRoot = await virtualEnvMgr.getPyEnvRoot();
46+
47+
process.verifyAll();
48+
expect(pyenvRoot).to.equal('yes');
49+
});
50+
51+
test('Get PyEnv Root from current PYENV_ROOT', async () => {
52+
process
53+
.setup(p => p.env)
54+
.returns(() => { return {}; })
55+
.verifiable(TypeMoq.Times.once());
56+
processService
57+
.setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root'])))
58+
.returns(() => Promise.resolve({ stdout: 'PROC' }))
59+
.verifiable(TypeMoq.Times.once());
60+
61+
const pyenvRoot = await virtualEnvMgr.getPyEnvRoot();
62+
63+
process.verifyAll();
64+
processService.verifyAll();
65+
expect(pyenvRoot).to.equal('PROC');
66+
});
67+
68+
test('Get default PyEnv Root path', async () => {
69+
process
70+
.setup(p => p.env)
71+
.returns(() => { return {}; })
72+
.verifiable(TypeMoq.Times.once());
73+
processService
74+
.setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root'])))
75+
.returns(() => Promise.resolve({ stdout: '', stderr: 'err' }))
76+
.verifiable(TypeMoq.Times.once());
77+
pathUtils
78+
.setup(p => p.home)
79+
.returns(() => 'HOME')
80+
.verifiable(TypeMoq.Times.once());
81+
const pyenvRoot = await virtualEnvMgr.getPyEnvRoot();
82+
83+
process.verifyAll();
84+
processService.verifyAll();
85+
expect(pyenvRoot).to.equal(path.join('HOME', '.pyenv'));
86+
});
87+
});

0 commit comments

Comments
 (0)