Skip to content

Commit b3b1799

Browse files
author
Kartik Raj
authored
Added Custom virtual env locator (#14882)
* Added custom virtual env locator * Connect locator to api * Remove conda type check from workspace and custom virtual env locators * Oops * Add tests for onChanged scenarios
1 parent 8ddde41 commit b3b1799

File tree

15 files changed

+625
-14
lines changed

15 files changed

+625
-14
lines changed

src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
import {
1111
getFileInfo, isParentPath, pathExists,
1212
} from '../../../common/externalDependencies';
13-
import { isCondaEnvironment } from '../../../discovery/locators/services/condaLocator';
1413
import { isPipenvEnvironment } from '../../../discovery/locators/services/pipEnvHelper';
1514
import { isVenvEnvironment, isVirtualenvEnvironment } from '../../../discovery/locators/services/virtualEnvironmentIdentifier';
1615
import { PythonEnvInfo, PythonEnvKind } from '../../info';
@@ -37,10 +36,6 @@ function getWorkspaceVirtualEnvDirs(root: string): string[] {
3736
* @param interpreterPath: Absolute path to the interpreter paths.
3837
*/
3938
async function getVirtualEnvKind(interpreterPath: string): Promise<PythonEnvKind> {
40-
if (await isCondaEnvironment(interpreterPath)) {
41-
return PythonEnvKind.Conda;
42-
}
43-
4439
if (await isPipenvEnvironment(interpreterPath)) {
4540
return PythonEnvKind.Pipenv;
4641
}
@@ -53,7 +48,7 @@ async function getVirtualEnvKind(interpreterPath: string): Promise<PythonEnvKind
5348
return PythonEnvKind.VirtualEnv;
5449
}
5550

56-
return PythonEnvKind.Custom;
51+
return PythonEnvKind.Unknown;
5752
}
5853

5954
async function buildSimpleVirtualEnvInfo(executablePath: string, kind: PythonEnvKind): Promise<PythonEnvInfo> {
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { uniq } from 'lodash';
5+
import * as path from 'path';
6+
import { traceVerbose } from '../../../../common/logger';
7+
import { chain, iterable } from '../../../../common/utils/async';
8+
import { getUserHomeDir } from '../../../../common/utils/platform';
9+
import { PythonEnvInfo, PythonEnvKind } from '../../../base/info';
10+
import { buildEnvInfo } from '../../../base/info/env';
11+
import { IPythonEnvsIterator } from '../../../base/locator';
12+
import { FSWatchingLocator } from '../../../base/locators/lowLevel/fsWatchingLocator';
13+
import {
14+
findInterpretersInDir, getEnvironmentDirFromPath, getPythonVersionFromPath, isStandardPythonBinary,
15+
} from '../../../common/commonUtils';
16+
import {
17+
getFileInfo, getPythonSetting, onDidChangePythonSetting, pathExists,
18+
} from '../../../common/externalDependencies';
19+
import { isPipenvEnvironment } from './pipEnvHelper';
20+
import {
21+
isVenvEnvironment,
22+
isVirtualenvEnvironment,
23+
isVirtualenvwrapperEnvironment,
24+
} from './virtualEnvironmentIdentifier';
25+
26+
/**
27+
* Default number of levels of sub-directories to recurse when looking for interpreters.
28+
*/
29+
const DEFAULT_SEARCH_DEPTH = 2;
30+
31+
export const VENVPATH_SETTING_KEY = 'venvPath';
32+
export const VENVFOLDERS_SETTING_KEY = 'venvFolders';
33+
34+
/**
35+
* Gets all custom virtual environment locations to look for environments.
36+
*/
37+
async function getCustomVirtualEnvDirs(): Promise<string[]> {
38+
const venvDirs: string[] = [];
39+
const venvPath = getPythonSetting<string>(VENVPATH_SETTING_KEY);
40+
if (venvPath) {
41+
venvDirs.push(venvPath);
42+
}
43+
const venvFolders = getPythonSetting<string[]>(VENVFOLDERS_SETTING_KEY) ?? [];
44+
const homeDir = getUserHomeDir();
45+
if (homeDir && (await pathExists(homeDir))) {
46+
venvFolders
47+
.map((item) => path.join(homeDir, item))
48+
.forEach((d) => venvDirs.push(d));
49+
}
50+
return uniq(venvDirs).filter(pathExists);
51+
}
52+
53+
/**
54+
* Gets the virtual environment kind for a given interpreter path.
55+
* This only checks for environments created using venv, virtualenv,
56+
* and virtualenvwrapper based environments.
57+
* @param interpreterPath: Absolute path to the interpreter paths.
58+
*/
59+
async function getVirtualEnvKind(interpreterPath: string): Promise<PythonEnvKind> {
60+
if (await isPipenvEnvironment(interpreterPath)) {
61+
return PythonEnvKind.Pipenv;
62+
}
63+
64+
if (await isVirtualenvwrapperEnvironment(interpreterPath)) {
65+
return PythonEnvKind.VirtualEnvWrapper;
66+
}
67+
68+
if (await isVenvEnvironment(interpreterPath)) {
69+
return PythonEnvKind.Venv;
70+
}
71+
72+
if (await isVirtualenvEnvironment(interpreterPath)) {
73+
return PythonEnvKind.VirtualEnv;
74+
}
75+
76+
return PythonEnvKind.Unknown;
77+
}
78+
79+
async function buildSimpleVirtualEnvInfo(executablePath: string, kind: PythonEnvKind): Promise<PythonEnvInfo> {
80+
const envInfo = buildEnvInfo({
81+
kind,
82+
version: await getPythonVersionFromPath(executablePath),
83+
executable: executablePath,
84+
});
85+
const location = getEnvironmentDirFromPath(executablePath);
86+
envInfo.location = location;
87+
envInfo.name = path.basename(location);
88+
// tslint:disable-next-line:no-suspicious-comment
89+
// TODO: Call a general display name provider here to build display name.
90+
const fileData = await getFileInfo(executablePath);
91+
envInfo.executable.ctime = fileData.ctime;
92+
envInfo.executable.mtime = fileData.mtime;
93+
return envInfo;
94+
}
95+
96+
/**
97+
* Finds and resolves custom virtual environments that users have provided.
98+
*/
99+
export class CustomVirtualEnvironmentLocator extends FSWatchingLocator {
100+
constructor() {
101+
super(getCustomVirtualEnvDirs, getVirtualEnvKind, {
102+
// Note detecting kind of virtual env depends on the file structure around the
103+
// executable, so we need to wait before attempting to detect it. However even
104+
// if the type detected is incorrect, it doesn't do any practical harm as kinds
105+
// in this locator are used in the same way (same activation commands etc.)
106+
delayOnCreated: 1000,
107+
});
108+
}
109+
110+
protected async initResources(): Promise<void> {
111+
this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.emitter.fire({})));
112+
this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.emitter.fire({})));
113+
}
114+
115+
// eslint-disable-next-line class-methods-use-this
116+
protected doIterEnvs(): IPythonEnvsIterator {
117+
async function* iterator() {
118+
const envRootDirs = await getCustomVirtualEnvDirs();
119+
const envGenerators = envRootDirs.map((envRootDir) => {
120+
async function* generator() {
121+
traceVerbose(`Searching for custom virtual envs in: ${envRootDir}`);
122+
123+
const envGenerator = findInterpretersInDir(envRootDir, DEFAULT_SEARCH_DEPTH);
124+
125+
for await (const env of envGenerator) {
126+
// We only care about python.exe (on windows) and python (on linux/mac)
127+
// Other version like python3.exe or python3.8 are often symlinks to
128+
// python.exe or python in the same directory in the case of virtual
129+
// environments.
130+
if (isStandardPythonBinary(env)) {
131+
// We should extract the kind here to avoid doing is*Environment()
132+
// check multiple times. Those checks are file system heavy and
133+
// we can use the kind to determine this anyway.
134+
const kind = await getVirtualEnvKind(env);
135+
yield buildSimpleVirtualEnvInfo(env, kind);
136+
traceVerbose(`Custom Virtual Environment: [added] ${env}`);
137+
} else {
138+
traceVerbose(`Custom Virtual Environment: [skipped] ${env}`);
139+
}
140+
}
141+
}
142+
return generator();
143+
});
144+
145+
yield* iterable(chain(envGenerators));
146+
}
147+
148+
return iterator();
149+
}
150+
151+
// eslint-disable-next-line class-methods-use-this
152+
protected async doResolveEnv(env: string | PythonEnvInfo): Promise<PythonEnvInfo | undefined> {
153+
const executablePath = typeof env === 'string' ? env : env.executable.filename;
154+
if (await pathExists(executablePath)) {
155+
// We should extract the kind here to avoid doing is*Environment()
156+
// check multiple times. Those checks are file system heavy and
157+
// we can use the kind to determine this anyway.
158+
const kind = await getVirtualEnvKind(executablePath);
159+
return buildSimpleVirtualEnvInfo(executablePath, kind);
160+
}
161+
return undefined;
162+
}
163+
}

src/client/pythonEnvironments/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { WorkspaceVirtualEnvironmentLocator } from './base/locators/lowLevel/wor
2020
import { getEnvs } from './base/locatorUtils';
2121
import { initializeExternalDependencies as initializeLegacyExternalDependencies } from './common/externalDependencies';
2222
import { ExtensionLocators, WatchRootsArgs, WorkspaceLocators } from './discovery/locators';
23+
import { CustomVirtualEnvironmentLocator } from './discovery/locators/services/customVirtualEnvLocator';
2324
import { GlobalVirtualEnvironmentLocator } from './discovery/locators/services/globalVirtualEnvronmentLocator';
2425
import { PosixKnownPathsLocator } from './discovery/locators/services/posixKnownPathsLocator';
2526
import { PyenvLocator } from './discovery/locators/services/pyenvLocator';
@@ -105,19 +106,20 @@ function createNonWorkspaceLocators(
105106
if (getOSType() === OSType.Windows) {
106107
// Windows specific locators go here
107108
locators = [
108-
new GlobalVirtualEnvironmentLocator(),
109-
new PyenvLocator(),
110109
new WindowsRegistryLocator(),
111110
new WindowsStoreLocator(),
112111
];
113112
} else {
114113
// Linux/Mac locators go here
115114
locators = [
116-
new GlobalVirtualEnvironmentLocator(),
117-
new PyenvLocator(),
118115
new PosixKnownPathsLocator(),
119116
];
120117
}
118+
locators.push(
119+
new GlobalVirtualEnvironmentLocator(),
120+
new PyenvLocator(),
121+
new CustomVirtualEnvironmentLocator(),
122+
);
121123
const disposables = (locators.filter((d) => d.dispose !== undefined)) as IDisposable[];
122124
ext.disposables.push(...disposables);
123125
return locators;

src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ suite('WorkspaceVirtualEnvironment Locator', () => {
117117
const expectedEnvs = [
118118
createExpectedEnvInfo(
119119
path.join(testWorkspaceFolder, 'posix2conda', 'python'),
120-
PythonEnvKind.Conda,
120+
PythonEnvKind.Unknown,
121121
{ major: 3, minor: 8, micro: 5 },
122122
'posix2conda',
123123
),
@@ -130,7 +130,7 @@ suite('WorkspaceVirtualEnvironment Locator', () => {
130130
),
131131
createExpectedEnvInfo(
132132
path.join(testWorkspaceFolder, 'posix3custom', 'bin', 'python'),
133-
PythonEnvKind.Custom,
133+
PythonEnvKind.Unknown,
134134
undefined,
135135
'posix3custom',
136136
),
@@ -149,7 +149,7 @@ suite('WorkspaceVirtualEnvironment Locator', () => {
149149
const interpreterPath = path.join(testWorkspaceFolder, 'posix2conda', 'python');
150150
const expected = createExpectedEnvInfo(
151151
path.join(testWorkspaceFolder, 'posix2conda', 'python'),
152-
PythonEnvKind.Conda,
152+
PythonEnvKind.Unknown,
153153
{ major: 3, minor: 8, micro: 5 },
154154
'posix2conda',
155155
);
@@ -163,7 +163,7 @@ suite('WorkspaceVirtualEnvironment Locator', () => {
163163
const interpreterPath = path.join(testWorkspaceFolder, 'posix2conda', 'python');
164164
const expected = createExpectedEnvInfo(
165165
path.join(testWorkspaceFolder, 'posix2conda', 'python'),
166-
PythonEnvKind.Conda,
166+
PythonEnvKind.Unknown,
167167
{ major: 3, minor: 8, micro: 5 },
168168
'posix2conda',
169169
);

src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/activate

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not a real binary
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not a real binary
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Not a real binary

src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/activate.sh

Whitespace-only changes.

src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/python

Whitespace-only changes.

0 commit comments

Comments
 (0)