From b2b60028be4c3e4c073d8634f310cf2bca86140d Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Fri, 18 Sep 2020 12:19:19 -0700 Subject: [PATCH 1/9] Pyenv locator --- .../common/environmentIdentifier.ts | 5 + .../locators/services/pyenvLocator.ts | 33 ++++++ .../common/environmentIdentifier.unit.test.ts | 58 ++++++++++ .../pyenv1/.pyenv/versions/3.6.9/bin/python | 0 .../pyenv-win/versions/3.6.9/bin/python.exe | 0 .../pyenv3/versions/3.6.9/bin/python | 0 .../pyenv3/versions/3.6.9/bin/python.exe | 0 .../locators/pyenvLocator.unit.test.ts | 106 ++++++++++++++++++ 8 files changed, 202 insertions(+) create mode 100644 src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts create mode 100644 src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python create mode 100644 src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python create mode 100644 src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe create mode 100644 src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 2834b5642cfa..b56c84e2b73e 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -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'; @@ -45,6 +46,10 @@ export async function identifyEnvironment(interpreterPath: string): Promise { + 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') : path.join(homeDir, '.pyenv', 'pyenv-win'); + } + + if (!await pathExists(pyenvDir)) { + return false; + } + + if (!pyenvDir.endsWith(path.sep)) { + pyenvDir += path.sep; + } + + return interpreterPath.startsWith(pyenvDir); +} diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 6f6bd4cbc818..7bd995e7df4e 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -133,6 +133,64 @@ 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 () => { + 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); + }); + + test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async () => { + 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); + }); + + test('PYENV_ROOT is set to a custom value on non-Windows', async () => { + 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); + }); + + test('PYENV is set to a custom value on Windows', async () => { + 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); + }); + }); + suite('Venv', () => { test('Pyvenv.cfg is in the same directory as the interpreter', async () => { const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python'); diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts new file mode 100644 index 000000000000..7cf2614f14b2 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/pyenvLocator.unit.test.ts @@ -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); + }); +}); From 9bb444bda4cf8af472fdfbd9f3a9a28fa4ba5076 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Fri, 18 Sep 2020 12:43:44 -0700 Subject: [PATCH 2/9] Skip tests per platform --- .../common/environmentIdentifier.unit.test.ts | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts index 7bd995e7df4e..a2f00aa4d3e9 100644 --- a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -150,7 +150,12 @@ suite('Environment Identifier', () => { getUserHomeDirStub.restore(); }); - test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async () => { + 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')); @@ -158,9 +163,16 @@ suite('Environment Identifier', () => { 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 () => { + 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')); @@ -169,18 +181,32 @@ suite('Environment Identifier', () => { 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 () => { + 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 () => { + 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')); @@ -188,6 +214,8 @@ suite('Environment Identifier', () => { const envType: EnvironmentType = await identifyEnvironment(interpreterPath); assert.deepStrictEqual(envType, EnvironmentType.Pyenv); + + return undefined; }); }); From 24a95225154d02a9175e5b58fc41a2201c21283d Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Fri, 18 Sep 2020 12:46:12 -0700 Subject: [PATCH 3/9] Wrong pyenv path order in ternary --- .../discovery/locators/services/pyenvLocator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts index 8aa74bfed414..b28f95353c4b 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts @@ -18,7 +18,7 @@ export async function isPyenvEnvironment(interpreterPath:string): Promise Date: Mon, 21 Sep 2020 08:29:54 -0700 Subject: [PATCH 4/9] Add description --- .../discovery/locators/services/pyenvLocator.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts index b28f95353c4b..5065faeb5ec0 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts @@ -12,6 +12,10 @@ import { pathExists } from '../../../common/externalDependencies'; * @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment. */ export async function isPyenvEnvironment(interpreterPath:string): Promise { + // 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. const isWindows = getOSType() === OSType.Windows; const envVariable = isWindows ? 'PYENV' : 'PYENV_ROOT'; let pyenvDir = getEnvironmentVariable(envVariable); From 5c8c4ab66aea87cc72632e466cdf7412a3f07717 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 21 Sep 2020 08:30:10 -0700 Subject: [PATCH 5/9] Autoformat venv locator --- .../discovery/locators/services/venvLocator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts index ea3e6a859ba0..31dc064c6584 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts @@ -4,13 +4,12 @@ import * as path from 'path'; import { pathExists } from '../../../common/externalDependencies'; - /** * 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{ +export async function isVenvEnvironment(interpreterPath:string): Promise { const pyvenvConfigFile = 'pyvenv.cfg'; // Check if the pyvenv.cfg file is in the directory as the interpreter. From a6729897be620e454d20e2d00aba276497fd78f7 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 21 Sep 2020 10:09:53 -0700 Subject: [PATCH 6/9] Revert "Autoformat venv locator" This reverts commit 5c8c4ab66aea87cc72632e466cdf7412a3f07717. --- .../discovery/locators/services/venvLocator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts index 31dc064c6584..ea3e6a859ba0 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/venvLocator.ts @@ -4,12 +4,13 @@ import * as path from 'path'; import { pathExists } from '../../../common/externalDependencies'; + /** * 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 { +export async function isVenvEnvironment(interpreterPath:string): Promise{ const pyvenvConfigFile = 'pyvenv.cfg'; // Check if the pyvenv.cfg file is in the directory as the interpreter. From df4d5282ad346551055e172208a1454747a52779 Mon Sep 17 00:00:00 2001 From: Kim-Adeline Miguel Date: Mon, 21 Sep 2020 10:15:04 -0700 Subject: [PATCH 7/9] Add links --- .../discovery/locators/services/pyenvLocator.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts index 5065faeb5ec0..d343db89ec84 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts @@ -16,6 +16,8 @@ export async function isPyenvEnvironment(interpreterPath:string): Promise Date: Mon, 21 Sep 2020 12:41:24 -0700 Subject: [PATCH 8/9] Windows-specific fixes --- .../discovery/locators/services/pyenvLocator.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts index d343db89ec84..b21513c12211 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts @@ -20,7 +20,9 @@ export async function isPyenvEnvironment(interpreterPath:string): Promise Date: Mon, 21 Sep 2020 12:55:22 -0700 Subject: [PATCH 9/9] Typo --- .../discovery/locators/services/pyenvLocator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts index b21513c12211..0e79a29f4bfe 100644 --- a/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts +++ b/src/client/pythonEnvironments/discovery/locators/services/pyenvLocator.ts @@ -39,7 +39,7 @@ export async function isPyenvEnvironment(interpreterPath:string): Promise