Skip to content

Commit daee6d5

Browse files
author
Kartik Raj
committed
Some corrections
1 parent 892724a commit daee6d5

File tree

4 files changed

+172
-17
lines changed

4 files changed

+172
-17
lines changed

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import * as path from 'path';
5+
import { isPipenvEnvironment } from '../discovery/locators/services/pipEnvHelper';
56
import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator';
67
import { EnvironmentType } from '../info';
78
import { pathExists } from './externalDependencies';
@@ -80,7 +81,7 @@ async function isCondaEnvironment(interpreterPath: string): Promise<boolean> {
8081
*
8182
* Last category is globally installed python, or system python.
8283
*/
83-
export async function identifyEnvironment(interpreterPath: string, resource?: Resource): Promise<EnvironmentType> {
84+
export async function identifyEnvironment(interpreterPath: string): Promise<EnvironmentType> {
8485
if (await isCondaEnvironment(interpreterPath)) {
8586
return EnvironmentType.Conda;
8687
}
@@ -89,7 +90,7 @@ export async function identifyEnvironment(interpreterPath: string, resource?: Re
8990
return EnvironmentType.WindowsStore;
9091
}
9192

92-
if (await isPipenvEnvironment(interpreterPath, resource)) {
93+
if (await isPipenvEnvironment(interpreterPath)) {
9394
return EnvironmentType.Pipenv;
9495
}
9596

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { pathExists } from 'fs-extra';
5+
import * as path from 'path';
6+
import { getEnvironmentVariable } from '../../../../common/utils/platform';
7+
import { readFile } from '../../../common/externalDependencies';
8+
9+
/**
10+
* Returns the path to Pipfile associated with the provided directory.
11+
* @param searchDir the directory to look into
12+
* @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory
13+
*/
14+
async function getAssociatedPipfile(
15+
searchDir: string,
16+
lookIntoParentDirectories: boolean,
17+
): Promise<string | undefined> {
18+
const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile';
19+
let depthToSearch = 1;
20+
if (lookIntoParentDirectories) {
21+
// PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for
22+
// a Pipfile, defaults to 3: https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_MAX_DEPTH
23+
const maxDepth = getEnvironmentVariable('PIPENV_MAX_DEPTH');
24+
if (maxDepth) {
25+
depthToSearch = +maxDepth;
26+
} else {
27+
depthToSearch = 3;
28+
}
29+
}
30+
while (depthToSearch > 0 && searchDir !== path.dirname(searchDir)) {
31+
const pipFile = path.join(searchDir, pipFileName);
32+
// eslint-disable-next-line no-await-in-loop
33+
if (await pathExists(pipFile)) {
34+
return pipFile;
35+
}
36+
searchDir = path.dirname(searchDir);
37+
depthToSearch -= 1;
38+
}
39+
return undefined;
40+
}
41+
42+
/**
43+
* Returns true if interpreter path belongs to a global pipenv environment which is associated with a particular folder,
44+
* false otherwise.
45+
* @param interpreterPath Absolute path to any python interpreter.
46+
*/
47+
export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string, folder: string): Promise<boolean> {
48+
const pipFileCorrespondingToEnvironment = await getPipfileIfGlobalPipenvEnvironment(interpreterPath);
49+
if (!pipFileCorrespondingToEnvironment) {
50+
return false;
51+
}
52+
const projectCorrespondingToEnvironment = path.dirname(pipFileCorrespondingToEnvironment);
53+
54+
// PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories
55+
// https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT
56+
const pipFile = await getAssociatedPipfile(folder, !getEnvironmentVariable('PIPENV_NO_INHERIT'));
57+
if (!pipFile) {
58+
return false;
59+
}
60+
const projectCorrespondingToFolder = path.dirname(pipFile);
61+
62+
return projectCorrespondingToEnvironment === projectCorrespondingToFolder;
63+
}
64+
65+
/**
66+
* If interpreter path belongs to a pipenv environment which is located inside a project, return associated Pipfile,
67+
* otherwise return `undefined`.
68+
* @param interpreterPath Absolute path to any python interpreter.
69+
*/
70+
async function getPipfileIfLocalPipenvEnvironment(interpreterPath: string): Promise<string | undefined> {
71+
// Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment
72+
// folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT
73+
// This is the layout we wish to verify.
74+
// project
75+
// |__ Pipfile <--- check if Pipfile exists here
76+
// |__ .venv <--- check if name of the folder is '.venv'
77+
// |__ Scripts/bin
78+
// |__ python <--- interpreterPath
79+
const venvFolder = path.dirname(path.dirname(interpreterPath));
80+
if (path.basename(venvFolder) !== '.venv') {
81+
return undefined;
82+
}
83+
const directoryWhereVenvResides = path.dirname(venvFolder);
84+
return getAssociatedPipfile(directoryWhereVenvResides, false);
85+
}
86+
87+
/**
88+
* If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`.
89+
* @param interpreterPath Absolute path to any python interpreter.
90+
*/
91+
async function getPipfileIfGlobalPipenvEnvironment(interpreterPath: string): Promise<string | undefined> {
92+
// Global pipenv environments have a .project file with the absolute path to the project
93+
// See https://github.com/pypa/pipenv/blob/9299ae1f7353bdd523a1829f3c7cad0ee67c2e3b/CHANGELOG.rst#L754
94+
// Also, the name of the directory where Pipfile resides is used as a prefix in the environment folder.
95+
// This is the layout we wish to verify.
96+
// <Environment folder>
97+
// |__ .project <--- check if .project exists here
98+
// |__ Scripts/bin
99+
// |__ python <--- interpreterPath
100+
const dotProjectFile = path.join(path.dirname(path.dirname(interpreterPath)), '.project');
101+
if (!(await pathExists(dotProjectFile))) {
102+
return undefined;
103+
}
104+
105+
const project = await readFile(dotProjectFile);
106+
if (!(await pathExists(project))) {
107+
return undefined;
108+
}
109+
110+
// The name of the directory where Pipfile resides is used as a prefix in the environment folder.
111+
if (interpreterPath.indexOf(`${path.sep}${path.basename(project)}-`) === -1) {
112+
return undefined;
113+
}
114+
115+
return getAssociatedPipfile(project, false);
116+
}
117+
118+
/**
119+
* Checks if the given interpreter path belongs to a pipenv environment, by locating the Pipfile which was used to
120+
* create the environment.
121+
* @param interpreterPath: Absolute path to any python interpreter.
122+
*/
123+
export async function isPipenvEnvironment(interpreterPath: string): Promise<boolean> {
124+
if (await getPipfileIfLocalPipenvEnvironment(interpreterPath)) {
125+
return true;
126+
}
127+
if (await getPipfileIfGlobalPipenvEnvironment(interpreterPath)) {
128+
return true;
129+
}
130+
return false;
131+
}

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

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EnvironmentType } from '../../../client/pythonEnvironments/info';
1111
import { TEST_LAYOUT_ROOT } from './commonTestConstants';
1212

1313
suite('Environment Identifier', () => {
14+
let getEnvVar: sinon.SinonStub;
1415
suite('Conda', () => {
1516
test('Conda layout with conda-meta and python binary in the same directory', async () => {
1617
const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe');
@@ -36,28 +37,18 @@ suite('Environment Identifier', () => {
3637
getEnvVar.restore();
3738
});
3839

39-
test('Path to a general global pipenv environment', async () => {
40-
const expectedDotProjectFile = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', '.project');
41-
const expectedProjectFile = path.join(testLayoutsRoot, 'pipenv', 'project2');
40+
test('Path to a global pipenv environment', async () => {
41+
const expectedDotProjectFile = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', '.project');
42+
const expectedProjectFile = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2');
4243
readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile);
43-
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', 'bin', 'python');
44-
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
45-
assert.equal(envType, EnvironmentType.Pipenv);
46-
});
47-
48-
test('Path to a global pipenv environment whose Pipfile lies at 3 levels above the project the environment is associated with', async () => {
49-
getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5');
50-
const expectedDotProjectFile = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', '.project');
51-
const expectedProjectFile = path.join(testLayoutsRoot, 'pipenv', 'grandparent', 'parent', 'child', 'project3');
52-
readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile);
53-
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', 'Scripts', 'python.exe');
44+
const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'globalEnvironments', 'project2-vnNIWe9P', 'bin', 'python');
5445
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
5546
assert.equal(envType, EnvironmentType.Pipenv);
5647
});
5748

5849
test('Path to a local pipenv environment with a custom Pipfile name', async () => {
5950
getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfileName');
60-
const interpreterPath: string = path.join(testLayoutsRoot, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe');
51+
const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe');
6152
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
6253
assert.equal(envType, EnvironmentType.Pipenv);
6354
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as assert from 'assert';
2+
import * as path from 'path';
3+
import * as sinon from 'sinon';
4+
import * as platformApis from '../../../../client/common/utils/platform';
5+
import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies';
6+
import { isPipenvEnvironmentRelatedToFolder } from '../../../../client/pythonEnvironments/discovery/locators/services/pipEnvHelper';
7+
import { TEST_LAYOUT_ROOT } from '../../common/commonTestConstants';
8+
9+
suite('Pipenv utils', () => {
10+
let readFile: sinon.SinonStub;
11+
let getEnvVar: sinon.SinonStub;
12+
setup(() => {
13+
getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable');
14+
readFile = sinon.stub(externalDependencies, 'readFile');
15+
});
16+
17+
teardown(() => {
18+
readFile.restore();
19+
getEnvVar.restore();
20+
});
21+
22+
test('Global pipenv environment is associated with a project whose Pipfile lies at 3 levels above the project', async () => {
23+
getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5');
24+
const expectedDotProjectFile = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', '.project');
25+
const grandParentProject = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'grandparent');
26+
readFile.withArgs(expectedDotProjectFile).resolves(grandParentProject);
27+
const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'globalEnvironments', 'grandparent-2s1eXEJ2', 'Scripts', 'python.exe');
28+
const project = path.join(grandParentProject, 'parent', 'child', 'project3');
29+
const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, project);
30+
assert.equal(isRelated, true);
31+
});
32+
});

0 commit comments

Comments
 (0)