Skip to content

Commit

Permalink
Pyenv locator (#13996)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kimadeline committed Sep 21, 2020
1 parent da38997 commit 2cc2c93
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 0 deletions.
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,46 @@
// 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.
// See https://github.com/pyenv/pyenv#locating-the-python-installation for general usage,
// And https://github.com/pyenv-win/pyenv-win for Windows specifics.
const isWindows = getOSType() === OSType.Windows;
const envVariable = isWindows ? 'PYENV' : 'PYENV_ROOT';

let pyenvDir = getEnvironmentVariable(envVariable);
let pathToCheck = interpreterPath;

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;
}

if (getOSType() === OSType.Windows) {
pyenvDir = pyenvDir.toUpperCase();
pathToCheck = pathToCheck.toUpperCase();
}

return pathToCheck.startsWith(pyenvDir);
}
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();
}

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
Empty file.
Empty file.
Empty file.
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);
});
});

0 comments on commit 2cc2c93

Please sign in to comment.