Skip to content

Commit 2cc2c93

Browse files
authored
Pyenv locator (#13996)
* Pyenv locator * Skip tests per platform * Wrong pyenv path order in ternary * Add description * Autoformat venv locator * Revert "Autoformat venv locator" This reverts commit 5c8c4ab. * Add links * Windows-specific fixes * Typo
1 parent da38997 commit 2cc2c93

File tree

8 files changed

+243
-0
lines changed

8 files changed

+243
-0
lines changed

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { isCondaEnvironment } from '../discovery/locators/services/condaLocator';
55
import { isPipenvEnvironment } from '../discovery/locators/services/pipEnvHelper';
6+
import { isPyenvEnvironment } from '../discovery/locators/services/pyenvLocator';
67
import { isVenvEnvironment } from '../discovery/locators/services/venvLocator';
78
import { isVirtualenvEnvironment } from '../discovery/locators/services/virtualenvLocator';
89
import { isVirtualenvwrapperEnvironment } from '../discovery/locators/services/virtualenvwrapperLocator';
@@ -45,6 +46,10 @@ export async function identifyEnvironment(interpreterPath: string): Promise<Envi
4546
return EnvironmentType.Pipenv;
4647
}
4748

49+
if (await isPyenvEnvironment(interpreterPath)) {
50+
return EnvironmentType.Pyenv;
51+
}
52+
4853
if (await isVenvEnvironment(interpreterPath)) {
4954
return EnvironmentType.Venv;
5055
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import * as path from 'path';
4+
import {
5+
getEnvironmentVariable, getOSType, getUserHomeDir, OSType,
6+
} from '../../../../common/utils/platform';
7+
import { pathExists } from '../../../common/externalDependencies';
8+
9+
/**
10+
* Checks if the given interpreter belongs to a pyenv based environment.
11+
* @param {string} interpreterPath: Absolute path to the python interpreter.
12+
* @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment.
13+
*/
14+
export async function isPyenvEnvironment(interpreterPath:string): Promise<boolean> {
15+
// Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix.
16+
// They contain the path to pyenv's installation folder.
17+
// If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix.
18+
// If the interpreter path starts with the path to the pyenv folder, then it is a pyenv environment.
19+
// See https://github.com/pyenv/pyenv#locating-the-python-installation for general usage,
20+
// And https://github.com/pyenv-win/pyenv-win for Windows specifics.
21+
const isWindows = getOSType() === OSType.Windows;
22+
const envVariable = isWindows ? 'PYENV' : 'PYENV_ROOT';
23+
24+
let pyenvDir = getEnvironmentVariable(envVariable);
25+
let pathToCheck = interpreterPath;
26+
27+
if (!pyenvDir) {
28+
const homeDir = getUserHomeDir() || '';
29+
pyenvDir = isWindows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv');
30+
}
31+
32+
if (!await pathExists(pyenvDir)) {
33+
return false;
34+
}
35+
36+
if (!pyenvDir.endsWith(path.sep)) {
37+
pyenvDir += path.sep;
38+
}
39+
40+
if (getOSType() === OSType.Windows) {
41+
pyenvDir = pyenvDir.toUpperCase();
42+
pathToCheck = pathToCheck.toUpperCase();
43+
}
44+
45+
return pathToCheck.startsWith(pyenvDir);
46+
}

src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,92 @@ suite('Environment Identifier', () => {
133133
});
134134
});
135135

136+
suite('Pyenv', () => {
137+
let getEnvVarStub: sinon.SinonStub;
138+
let getOsTypeStub: sinon.SinonStub;
139+
let getUserHomeDirStub: sinon.SinonStub;
140+
141+
suiteSetup(() => {
142+
getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable');
143+
getOsTypeStub = sinon.stub(platformApis, 'getOSType');
144+
getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir');
145+
});
146+
147+
suiteTeardown(() => {
148+
getEnvVarStub.restore();
149+
getOsTypeStub.restore();
150+
getUserHomeDirStub.restore();
151+
});
152+
153+
test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async function () {
154+
if (getOSTypeForTest() === OSType.Windows) {
155+
// tslint:disable-next-line: no-invalid-this
156+
return this.skip();
157+
}
158+
159+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv1', '.pyenv', 'versions', '3.6.9', 'bin', 'python');
160+
161+
getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv1'));
162+
getEnvVarStub.withArgs('PYENV_ROOT').returns(undefined);
163+
164+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
165+
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);
166+
167+
return undefined;
168+
});
169+
170+
test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async function () {
171+
if (getOSTypeForTest() !== OSType.Windows) {
172+
// tslint:disable-next-line: no-invalid-this
173+
return this.skip();
174+
}
175+
176+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe');
177+
178+
getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv2'));
179+
getEnvVarStub.withArgs('PYENV').returns(undefined);
180+
getOsTypeStub.returns(platformApis.OSType.Windows);
181+
182+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
183+
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);
184+
185+
return undefined;
186+
});
187+
188+
test('PYENV_ROOT is set to a custom value on non-Windows', async function () {
189+
if (getOSTypeForTest() === OSType.Windows) {
190+
// tslint:disable-next-line: no-invalid-this
191+
return this.skip();
192+
}
193+
194+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python');
195+
196+
getEnvVarStub.withArgs('PYENV_ROOT').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3'));
197+
198+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
199+
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);
200+
201+
return undefined;
202+
});
203+
204+
test('PYENV is set to a custom value on Windows', async function () {
205+
if (getOSTypeForTest() !== OSType.Windows) {
206+
// tslint:disable-next-line: no-invalid-this
207+
return this.skip();
208+
}
209+
210+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python.exe');
211+
212+
getEnvVarStub.withArgs('PYENV').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3'));
213+
getOsTypeStub.returns(platformApis.OSType.Windows);
214+
215+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
216+
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);
217+
218+
return undefined;
219+
});
220+
});
221+
136222
suite('Venv', () => {
137223
test('Pyvenv.cfg is in the same directory as the interpreter', async () => {
138224
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python');

src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python

Whitespace-only changes.

src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe

Whitespace-only changes.

src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python

Whitespace-only changes.

src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe

Whitespace-only changes.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import * as assert from 'assert';
4+
import * as path from 'path';
5+
import * as sinon from 'sinon';
6+
import * as platformUtils from '../../../../client/common/utils/platform';
7+
import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies';
8+
import { isPyenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/pyenvLocator';
9+
10+
suite('Pyenv Locator Tests', () => {
11+
const home = platformUtils.getUserHomeDir() || '';
12+
let getEnvVariableStub: sinon.SinonStub;
13+
let pathExistsStub:sinon.SinonStub;
14+
let getOsTypeStub: sinon.SinonStub;
15+
16+
setup(() => {
17+
getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable');
18+
getOsTypeStub = sinon.stub(platformUtils, 'getOSType');
19+
pathExistsStub = sinon.stub(fileUtils, 'pathExists');
20+
});
21+
22+
teardown(() => {
23+
getEnvVariableStub.restore();
24+
pathExistsStub.restore();
25+
getOsTypeStub.restore();
26+
});
27+
28+
type PyenvUnitTestData = {
29+
testTitle: string,
30+
interpreterPath: string,
31+
pyenvEnvVar?: string,
32+
osType: platformUtils.OSType,
33+
};
34+
35+
const testData: PyenvUnitTestData[] = [
36+
{
37+
testTitle: 'undefined',
38+
interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'),
39+
osType: platformUtils.OSType.Linux,
40+
},
41+
{
42+
testTitle: 'undefined',
43+
interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
44+
osType: platformUtils.OSType.Windows,
45+
},
46+
{
47+
testTitle: 'its default value',
48+
interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'),
49+
pyenvEnvVar: path.join(home, '.pyenv'),
50+
osType: platformUtils.OSType.Linux,
51+
},
52+
{
53+
testTitle: 'its default value',
54+
interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
55+
pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'),
56+
osType: platformUtils.OSType.Windows,
57+
},
58+
{
59+
testTitle: 'a custom value',
60+
interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'),
61+
pyenvEnvVar: path.join('path', 'to', 'mypyenv'),
62+
osType: platformUtils.OSType.Linux,
63+
},
64+
{
65+
testTitle: 'a custom value',
66+
interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
67+
pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'),
68+
osType: platformUtils.OSType.Windows,
69+
},
70+
];
71+
72+
testData.forEach(({
73+
testTitle, interpreterPath, pyenvEnvVar, osType,
74+
}) => {
75+
test(`The environment variable is set to ${testTitle} on ${osType}, and the interpreter path is in a subfolder of the pyenv folder`, async () => {
76+
getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvEnvVar);
77+
getEnvVariableStub.withArgs('PYENV').returns(pyenvEnvVar);
78+
getOsTypeStub.returns(osType);
79+
pathExistsStub.resolves(true);
80+
81+
const result = await isPyenvEnvironment(interpreterPath);
82+
83+
assert.strictEqual(result, true);
84+
});
85+
});
86+
87+
test('The pyenv directory does not exist', async () => {
88+
const interpreterPath = path.join('path', 'to', 'python');
89+
90+
pathExistsStub.resolves(false);
91+
92+
const result = await isPyenvEnvironment(interpreterPath);
93+
94+
assert.strictEqual(result, false);
95+
});
96+
97+
test('The interpreter path is not in a subfolder of the pyenv folder', async () => {
98+
const interpreterPath = path.join('path', 'to', 'python');
99+
100+
pathExistsStub.resolves(true);
101+
102+
const result = await isPyenvEnvironment(interpreterPath);
103+
104+
assert.strictEqual(result, false);
105+
});
106+
});

0 commit comments

Comments
 (0)