Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pyenv locator #13996

Merged
merged 9 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/client/pythonEnvironments/common/environmentIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

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

if (await isPyenvEnvironment(interpreterPath)) {
return EnvironmentType.Pyenv;
}

if (await isVenvEnvironment(interpreterPath)) {
return EnvironmentType.Venv;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as path from 'path';
import {
getEnvironmentVariable, getOSType, getUserHomeDir, OSType,
} from '../../../../common/utils/platform';
import { pathExists } from '../../../common/externalDependencies';

/**
* Checks if the given interpreter belongs to a pyenv based environment.
* @param {string} interpreterPath: Absolute path to the python interpreter.
* @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment.
*/
export async function isPyenvEnvironment(interpreterPath:string): Promise<boolean> {
// Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix.
// They contain the path to pyenv's installation folder.
// If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix.
// If the interpreter path starts with the path to the pyenv folder, then it is a pyenv environment.
kimadeline marked this conversation as resolved.
Show resolved Hide resolved
const isWindows = getOSType() === OSType.Windows;
const envVariable = isWindows ? 'PYENV' : 'PYENV_ROOT';
let pyenvDir = getEnvironmentVariable(envVariable);

if (!pyenvDir) {
const homeDir = getUserHomeDir() || '';
pyenvDir = isWindows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv');
}

if (!await pathExists(pyenvDir)) {
return false;
}

if (!pyenvDir.endsWith(path.sep)) {
pyenvDir += path.sep;
}

return interpreterPath.startsWith(pyenvDir);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
import * as path from 'path';
import { pathExists } from '../../../common/externalDependencies';


kimadeline marked this conversation as resolved.
Show resolved Hide resolved
/**
* Checks if the given interpreter belongs to a venv based environment.
* @param {string} interpreterPath: Absolute path to the python interpreter.
* @returns {boolean} : Returns true if the interpreter belongs to a venv environment.
*/
export async function isVenvEnvironment(interpreterPath:string): Promise<boolean>{
export async function isVenvEnvironment(interpreterPath:string): Promise<boolean> {
const pyvenvConfigFile = 'pyvenv.cfg';

// Check if the pyvenv.cfg file is in the directory as the interpreter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,92 @@ suite('Environment Identifier', () => {
});
});

suite('Pyenv', () => {
let getEnvVarStub: sinon.SinonStub;
let getOsTypeStub: sinon.SinonStub;
let getUserHomeDirStub: sinon.SinonStub;

suiteSetup(() => {
getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable');
getOsTypeStub = sinon.stub(platformApis, 'getOSType');
getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir');
});

suiteTeardown(() => {
getEnvVarStub.restore();
getOsTypeStub.restore();
getUserHomeDirStub.restore();
});

test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async function () {
if (getOSTypeForTest() === OSType.Windows) {
// tslint:disable-next-line: no-invalid-this
return this.skip();
}
ericsnowcurrently marked this conversation as resolved.
Show resolved Hide resolved

const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv1', '.pyenv', 'versions', '3.6.9', 'bin', 'python');

getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv1'));
getEnvVarStub.withArgs('PYENV_ROOT').returns(undefined);

const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);

return undefined;
});

test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async function () {
if (getOSTypeForTest() !== OSType.Windows) {
// tslint:disable-next-line: no-invalid-this
return this.skip();
}

const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe');

getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv2'));
getEnvVarStub.withArgs('PYENV').returns(undefined);
getOsTypeStub.returns(platformApis.OSType.Windows);

const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);

return undefined;
});

test('PYENV_ROOT is set to a custom value on non-Windows', async function () {
if (getOSTypeForTest() === OSType.Windows) {
// tslint:disable-next-line: no-invalid-this
return this.skip();
}

const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python');

getEnvVarStub.withArgs('PYENV_ROOT').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3'));

const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);

return undefined;
});

test('PYENV is set to a custom value on Windows', async function () {
if (getOSTypeForTest() !== OSType.Windows) {
// tslint:disable-next-line: no-invalid-this
return this.skip();
}

const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python.exe');

getEnvVarStub.withArgs('PYENV').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3'));
getOsTypeStub.returns(platformApis.OSType.Windows);

const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
assert.deepStrictEqual(envType, EnvironmentType.Pyenv);

return undefined;
});
});

suite('Venv', () => {
test('Pyvenv.cfg is in the same directory as the interpreter', async () => {
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as assert from 'assert';
import * as path from 'path';
import * as sinon from 'sinon';
import * as platformUtils from '../../../../client/common/utils/platform';
import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies';
import { isPyenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/pyenvLocator';

suite('Pyenv Locator Tests', () => {
const home = platformUtils.getUserHomeDir() || '';
let getEnvVariableStub: sinon.SinonStub;
let pathExistsStub:sinon.SinonStub;
let getOsTypeStub: sinon.SinonStub;

setup(() => {
getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable');
getOsTypeStub = sinon.stub(platformUtils, 'getOSType');
pathExistsStub = sinon.stub(fileUtils, 'pathExists');
});

teardown(() => {
getEnvVariableStub.restore();
pathExistsStub.restore();
getOsTypeStub.restore();
});

type PyenvUnitTestData = {
testTitle: string,
interpreterPath: string,
pyenvEnvVar?: string,
osType: platformUtils.OSType,
};

const testData: PyenvUnitTestData[] = [
{
testTitle: 'undefined',
interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'),
osType: platformUtils.OSType.Linux,
},
{
testTitle: 'undefined',
interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
osType: platformUtils.OSType.Windows,
},
{
testTitle: 'its default value',
interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'),
pyenvEnvVar: path.join(home, '.pyenv'),
osType: platformUtils.OSType.Linux,
},
{
testTitle: 'its default value',
interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'),
osType: platformUtils.OSType.Windows,
},
{
testTitle: 'a custom value',
interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'),
pyenvEnvVar: path.join('path', 'to', 'mypyenv'),
osType: platformUtils.OSType.Linux,
},
{
testTitle: 'a custom value',
interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'),
pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'),
osType: platformUtils.OSType.Windows,
},
];

testData.forEach(({
testTitle, interpreterPath, pyenvEnvVar, osType,
}) => {
test(`The environment variable is set to ${testTitle} on ${osType}, and the interpreter path is in a subfolder of the pyenv folder`, async () => {
getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvEnvVar);
getEnvVariableStub.withArgs('PYENV').returns(pyenvEnvVar);
getOsTypeStub.returns(osType);
pathExistsStub.resolves(true);

const result = await isPyenvEnvironment(interpreterPath);

assert.strictEqual(result, true);
});
});

test('The pyenv directory does not exist', async () => {
const interpreterPath = path.join('path', 'to', 'python');

pathExistsStub.resolves(false);

const result = await isPyenvEnvironment(interpreterPath);

assert.strictEqual(result, false);
});

test('The interpreter path is not in a subfolder of the pyenv folder', async () => {
const interpreterPath = path.join('path', 'to', 'python');

pathExistsStub.resolves(true);

const result = await isPyenvEnvironment(interpreterPath);

assert.strictEqual(result, false);
});
});