Skip to content

Commit 2a0fa40

Browse files
authored
Venv identifier (microsoft#13844)
* Venv identifier * Add unit tests * Test tweaks * More tests * Fix linting errors * Address comments..
1 parent 0773296 commit 2a0fa40

10 files changed

Lines changed: 173 additions & 63 deletions

File tree

‎src/client/pythonEnvironments/common/environmentIdentifier.ts‎

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import * as fsapi from 'fs-extra';
5-
import * as path from 'path';
6-
import { createDeferred } from '../../common/utils/async';
4+
import { isCondaEnvironment } from '../discovery/locators/services/condaLocator';
5+
import { isVenvEnvironment } from '../discovery/locators/services/venvLocator';
76
import { isWindowsStoreEnvironment } from '../discovery/locators/services/windowsStoreLocator';
87
import { EnvironmentType } from '../info';
98

10-
function pathExists(absPath: string): Promise<boolean> {
11-
const deferred = createDeferred<boolean>();
12-
fsapi.exists(absPath, (result) => {
13-
deferred.resolve(result);
14-
});
15-
return deferred.promise;
16-
}
17-
18-
/**
19-
* Checks if the given interpreter path belongs to a conda environment. Using
20-
* known folder layout, and presence of 'conda-meta' directory.
21-
* @param {string} interpreterPath: Absolute path to any python interpreter.
22-
*
23-
* Remarks: This is what we will use to begin with. Another approach we can take
24-
* here is to parse ~/.conda/environments.txt. This file will have list of conda
25-
* environments. We can compare the interpreter path against the paths in that file.
26-
* We don't want to rely on this file because it is an implementation detail of
27-
* conda. If it turns out that the layout based identification is not sufficient
28-
* that is the next alternative that is cheap.
29-
*
30-
* sample content of the ~/.conda/environments.txt:
31-
* C:\envs\\myenv
32-
* C:\ProgramData\Miniconda3
33-
*
34-
* Yet another approach is to use `conda env list --json` and compare the returned env
35-
* list to see if the given interpreter path belongs to any of the returned environments.
36-
* This approach is heavy, and involves running a binary. For now we decided not to
37-
* take this approach, since it does not look like we need it.
38-
*
39-
* sample output from `conda env list --json`:
40-
* conda env list --json
41-
* {
42-
* "envs": [
43-
* "C:\\envs\\myenv",
44-
* "C:\\ProgramData\\Miniconda3"
45-
* ]
46-
* }
47-
*/
48-
async function isCondaEnvironment(interpreterPath: string): Promise<boolean> {
49-
const condaMetaDir = 'conda-meta';
50-
51-
// Check if the conda-meta directory is in the same directory as the interpreter.
52-
// This layout is common in Windows.
53-
// env
54-
// |__ conda-meta <--- check if this directory exists
55-
// |__ python.exe <--- interpreterPath
56-
const condaEnvDir1 = path.join(path.dirname(interpreterPath), condaMetaDir);
57-
58-
// Check if the conda-meta directory is in the parent directory relative to the interpreter.
59-
// This layout is common on linux/Mac.
60-
// env
61-
// |__ conda-meta <--- check if this directory exists
62-
// |__ bin
63-
// |__ python <--- interpreterPath
64-
const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir);
65-
66-
return [await pathExists(condaEnvDir1), await pathExists(condaEnvDir2)].includes(true);
67-
}
68-
699
/**
7010
* Returns environment type.
7111
* @param {string} interpreterPath : Absolute path to the python interpreter binary.
@@ -98,6 +38,10 @@ export async function identifyEnvironment(interpreterPath: string): Promise<Envi
9838
return EnvironmentType.WindowsStore;
9939
}
10040

41+
if (await isVenvEnvironment(interpreterPath)) {
42+
return EnvironmentType.Venv;
43+
}
44+
10145
// additional identifiers go here
10246

10347
return EnvironmentType.Unknown;

‎src/client/pythonEnvironments/common/externalDependencies.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
import * as fsapi from 'fs-extra';
45
import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types';
6+
import { createDeferred } from '../../common/utils/async';
57
import { IServiceContainer } from '../../ioc/types';
68

79
let internalServiceContainer: IServiceContainer;
@@ -17,3 +19,11 @@ export async function shellExecute(command: string, timeout: number): Promise<Ex
1719
const proc = await getProcessFactory().create();
1820
return proc.shellExec(command, { timeout });
1921
}
22+
23+
export function pathExists(absPath: string): Promise<boolean> {
24+
const deferred = createDeferred<boolean>();
25+
fsapi.exists(absPath, (result) => {
26+
deferred.resolve(result);
27+
});
28+
return deferred.promise;
29+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
import * as path from 'path';
4+
import { pathExists } from '../../../common/externalDependencies';
5+
6+
/**
7+
* Checks if the given interpreter path belongs to a conda environment. Using
8+
* known folder layout, and presence of 'conda-meta' directory.
9+
* @param {string} interpreterPath: Absolute path to any python interpreter.
10+
*
11+
* Remarks: This is what we will use to begin with. Another approach we can take
12+
* here is to parse ~/.conda/environments.txt. This file will have list of conda
13+
* environments. We can compare the interpreter path against the paths in that file.
14+
* We don't want to rely on this file because it is an implementation detail of
15+
* conda. If it turns out that the layout based identification is not sufficient
16+
* that is the next alternative that is cheap.
17+
*
18+
* sample content of the ~/.conda/environments.txt:
19+
* C:\envs\myenv
20+
* C:\ProgramData\Miniconda3
21+
*
22+
* Yet another approach is to use `conda env list --json` and compare the returned env
23+
* list to see if the given interpreter path belongs to any of the returned environments.
24+
* This approach is heavy, and involves running a binary. For now we decided not to
25+
* take this approach, since it does not look like we need it.
26+
*
27+
* sample output from `conda env list --json`:
28+
* conda env list --json
29+
* {
30+
* "envs": [
31+
* "C:\\envs\\myenv",
32+
* "C:\\ProgramData\\Miniconda3"
33+
* ]
34+
* }
35+
*/
36+
export async function isCondaEnvironment(interpreterPath: string): Promise<boolean> {
37+
const condaMetaDir = 'conda-meta';
38+
39+
// Check if the conda-meta directory is in the same directory as the interpreter.
40+
// This layout is common in Windows.
41+
// env
42+
// |__ conda-meta <--- check if this directory exists
43+
// |__ python.exe <--- interpreterPath
44+
const condaEnvDir1 = path.join(path.dirname(interpreterPath), condaMetaDir);
45+
46+
// Check if the conda-meta directory is in the parent directory relative to the interpreter.
47+
// This layout is common on linux/Mac.
48+
// env
49+
// |__ conda-meta <--- check if this directory exists
50+
// |__ bin
51+
// |__ python <--- interpreterPath
52+
const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPath)), condaMetaDir);
53+
54+
return [await pathExists(condaEnvDir1), await pathExists(condaEnvDir2)].includes(true);
55+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import { pathExists } from '../../../common/externalDependencies';
6+
7+
8+
/**
9+
* Checks if the given interpreter belongs to a venv based environment.
10+
* @param {string} interpreterPath: Absolute path to the python interpreter.
11+
* @returns {boolean} : Returns true if the interpreter belongs to a venv environment.
12+
*/
13+
export async function isVenvEnvironment(interpreterPath:string): Promise<boolean>{
14+
const pyvenvConfigFile = 'pyvenv.cfg';
15+
16+
// Check if the pyvenv.cfg file is in the directory as the interpreter.
17+
// env
18+
// |__ pyvenv.cfg <--- check if this file exists
19+
// |__ python <--- interpreterPath
20+
const venvPath1 = path.join(path.dirname(interpreterPath), pyvenvConfigFile);
21+
22+
// Check if the pyvenv.cfg file is in the parent directory relative to the interpreter.
23+
// env
24+
// |__ pyvenv.cfg <--- check if this file exists
25+
// |__ bin or Scripts
26+
// |__ python <--- interpreterPath
27+
const venvPath2 = path.join(path.dirname(path.dirname(interpreterPath)), pyvenvConfigFile);
28+
29+
return [await pathExists(venvPath1), await pathExists(venvPath2)].includes(true);
30+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ suite('Environment Identifier', () => {
2525

2626
suite('Windows Store', () => {
2727
let getEnvVar: sinon.SinonStub;
28-
const fakeLocalAppDataPath = 'X:\\users\\user\\AppData\\Local';
28+
const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps');
2929
const fakeProgramFilesPath = 'X:\\Program Files';
3030
const executable = ['python.exe', 'python3.exe', 'python3.8.exe'];
3131
suiteSetup(() => {
@@ -96,4 +96,17 @@ suite('Environment Identifier', () => {
9696
});
9797
});
9898
});
99+
100+
suite('Venv', () => {
101+
test('Pyvenv.cfg is in the same directory as the interpreter', async () => {
102+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python');
103+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
104+
assert.deepEqual(envType, EnvironmentType.Venv);
105+
});
106+
test('Pyvenv.cfg is in the same directory as the interpreter', async () => {
107+
const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv2', 'bin', 'python');
108+
const envType: EnvironmentType = await identifyEnvironment(interpreterPath);
109+
assert.deepEqual(envType, EnvironmentType.Venv);
110+
});
111+
});
99112
});

‎src/test/pythonEnvironments/common/envlayouts/venv1/python‎

Whitespace-only changes.

‎src/test/pythonEnvironments/common/envlayouts/venv1/pyvenv.cfg‎

Whitespace-only changes.

‎src/test/pythonEnvironments/common/envlayouts/venv2/bin/python‎

Whitespace-only changes.

‎src/test/pythonEnvironments/common/envlayouts/venv2/pyvenv.cfg‎

Whitespace-only changes.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import * as path from 'path';
6+
import * as sinon from 'sinon';
7+
import { ImportMock } from 'ts-mock-imports';
8+
import * as fileapis from '../../../../client/pythonEnvironments/common/externalDependencies';
9+
import { isVenvEnvironment } from '../../../../client/pythonEnvironments/discovery/locators/services/venvLocator';
10+
11+
suite('Venv Locator Tests', () => {
12+
suite('Venv identifier Tests', () => {
13+
const pyvenvCfg = 'pyvenv.cfg';
14+
const envRoot = path.join('path', 'to', 'env');
15+
const configPath = path.join('env', pyvenvCfg);
16+
let fileExistsStub:sinon.SinonStub;
17+
18+
setup(() => {
19+
fileExistsStub = ImportMock.mockFunction(fileapis, 'pathExists');
20+
});
21+
22+
teardown(() => {
23+
fileExistsStub.restore();
24+
});
25+
26+
test('pyvenv.cfg does not exist', async () => {
27+
const interpreter = path.join(envRoot, 'python');
28+
fileExistsStub.callsFake(() => Promise.resolve(false));
29+
assert.ok(!(await isVenvEnvironment(interpreter)));
30+
});
31+
32+
test('pyvenv.cfg exists in the current folder', async () => {
33+
const interpreter = path.join(envRoot, 'python');
34+
35+
fileExistsStub.callsFake((p:string) => {
36+
if (p.endsWith(configPath)) {
37+
return Promise.resolve(true);
38+
}
39+
return Promise.resolve(false);
40+
});
41+
42+
assert.ok(await isVenvEnvironment(interpreter));
43+
});
44+
45+
test('pyvenv.cfg exists in the parent folder', async () => {
46+
const interpreter = path.join(envRoot, 'bin', 'python');
47+
48+
fileExistsStub.callsFake((p:string) => {
49+
if (p.endsWith(configPath)) {
50+
return Promise.resolve(true);
51+
}
52+
return Promise.resolve(false);
53+
});
54+
55+
assert.ok(await isVenvEnvironment(interpreter));
56+
});
57+
});
58+
});

0 commit comments

Comments
 (0)