Skip to content

Commit 89ac7ed

Browse files
author
Kartik Raj
authored
Auto select pipenv and poetry environments created for a workspace (microsoft/vscode-python#23102)
For microsoft/vscode-python#19153 Filter out any pipenv or poetry envs which do not belong to the current workspace.
1 parent a5f56fe commit 89ac7ed

File tree

15 files changed

+136
-35
lines changed

15 files changed

+136
-35
lines changed

extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import { Resource } from '../../common/types';
1111
import { Architecture } from '../../common/utils/platform';
1212
import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate';
1313
import { isParentPath } from '../../pythonEnvironments/common/externalDependencies';
14-
import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info';
14+
import {
15+
EnvironmentType,
16+
PythonEnvironment,
17+
virtualEnvTypes,
18+
workspaceVirtualEnvTypes,
19+
} from '../../pythonEnvironments/info';
1520
import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion';
1621
import { IInterpreterHelper } from '../contracts';
1722
import { IInterpreterComparer } from './types';
@@ -147,8 +152,8 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
147152
if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) {
148153
return true;
149154
}
150-
if (virtualEnvTypes.includes(i.envType)) {
151-
// We're not sure if these envs were created for the workspace, so do not recommend them.
155+
if (!workspaceVirtualEnvTypes.includes(i.envType) && virtualEnvTypes.includes(i.envType)) {
156+
// These are global virtual envs so we're not sure if these envs were created for the workspace, skip them.
152157
return false;
153158
}
154159
if (i.version?.major === 2) {

extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { inject, injectable } from 'inversify';
77
import { Disposable, Uri } from 'vscode';
8-
import { arePathsSame } from '../../../common/platform/fs-paths';
8+
import { arePathsSame, isParentPath } from '../../../common/platform/fs-paths';
99
import { IPathUtils, Resource } from '../../../common/types';
1010
import { getEnvPath } from '../../../pythonEnvironments/base/info/env';
1111
import { PythonEnvironment } from '../../../pythonEnvironments/info';
@@ -45,6 +45,13 @@ export class InterpreterSelector implements IInterpreterSelector {
4545
workspaceUri?: Uri,
4646
useDetailedName = false,
4747
): IInterpreterQuickPickItem {
48+
if (!useDetailedName) {
49+
const workspacePath = workspaceUri?.fsPath;
50+
if (workspacePath && isParentPath(interpreter.path, workspacePath)) {
51+
// If interpreter is in the workspace, then display the full path.
52+
useDetailedName = true;
53+
}
54+
}
4855
const path =
4956
interpreter.envPath && getEnvPath(interpreter.path, interpreter.envPath).pathType === 'envFolderPath'
5057
? interpreter.envPath

extensions/positron-python/src/client/pythonEnvironments/base/info/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export function setEnvDisplayString(env: PythonEnvInfo): void {
160160

161161
function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): string {
162162
// main parts
163-
const shouldDisplayKind = getAllDetails || env.searchLocation || globallyInstalledEnvKinds.includes(env.kind);
163+
const shouldDisplayKind = getAllDetails || globallyInstalledEnvKinds.includes(env.kind);
164164
const shouldDisplayArch = !virtualEnvKinds.includes(env.kind);
165165
const displayNameParts: string[] = ['Python'];
166166
if (env.version && !isVersionEmpty(env.version)) {

extensions/positron-python/src/client/pythonEnvironments/base/info/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ type _PythonEnvInfo = PythonEnvBaseInfo & PythonBuildInfo;
196196
* @prop distro - the installed Python distro that this env is using or belongs to
197197
* @prop display - the text to use when showing the env to users
198198
* @prop detailedDisplayName - display name containing all details
199-
* @prop searchLocation - the root under which a locator found this env, if any
199+
* @prop searchLocation - the project to which this env is related to, if any
200200
*/
201201
export type PythonEnvInfo = _PythonEnvInfo & {
202202
distro: PythonDistroInfo;

extensions/positron-python/src/client/pythonEnvironments/base/locator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export type BasicEnvInfo = {
144144
executablePath: string;
145145
source?: PythonEnvSource[];
146146
envPath?: string;
147+
searchLocation?: Uri;
147148
};
148149

149150
/**

extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
// Licensed under the MIT License.
33

44
import { cloneDeep, isEqual, uniq } from 'lodash';
5-
import { Event, EventEmitter } from 'vscode';
5+
import { Event, EventEmitter, Uri } from 'vscode';
66
import { traceVerbose } from '../../../../logging';
7+
import { isParentPath } from '../../../common/externalDependencies';
78
import { PythonEnvKind } from '../../info';
89
import { areSameEnv } from '../../info/env';
910
import { getPrioritizedEnvKinds } from '../../info/envKind';
@@ -136,9 +137,24 @@ function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicE
136137
const [env] = sortEnvInfoByPriority(oldEnv, newEnv);
137138
const merged = cloneDeep(env);
138139
merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? []));
140+
merged.searchLocation = getMergedSearchLocation(oldEnv, newEnv);
139141
return merged;
140142
}
141143

144+
function getMergedSearchLocation(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): Uri | undefined {
145+
if (oldEnv.searchLocation && newEnv.searchLocation) {
146+
// Choose the deeper project path of the two, as that can be used to signify
147+
// that the environment is related to both the projects.
148+
if (isParentPath(oldEnv.searchLocation.fsPath, newEnv.searchLocation.fsPath)) {
149+
return oldEnv.searchLocation;
150+
}
151+
if (isParentPath(newEnv.searchLocation.fsPath, oldEnv.searchLocation.fsPath)) {
152+
return newEnv.searchLocation;
153+
}
154+
}
155+
return oldEnv.searchLocation ?? newEnv.searchLocation;
156+
}
157+
142158
/**
143159
* Selects an environment based on the environment selection priority. This should
144160
* match the priority in the environment identifier.

extensions/positron-python/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<Pytho
5353
* returned could still be invalid.
5454
*/
5555
export async function resolveBasicEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
56-
const { kind, source } = env;
56+
const { kind, source, searchLocation } = env;
5757
const resolvers = getResolvers();
5858
const resolverForKind = resolvers.get(kind)!;
5959
const resolvedEnv = await resolverForKind(env);
60-
resolvedEnv.searchLocation = getSearchLocation(resolvedEnv);
60+
resolvedEnv.searchLocation = getSearchLocation(resolvedEnv, searchLocation);
6161
resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? []));
6262
if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) {
6363
// We can update env further using information we can get from the Windows registry.
@@ -87,7 +87,11 @@ async function getEnvType(env: PythonEnvInfo) {
8787
return undefined;
8888
}
8989

90-
function getSearchLocation(env: PythonEnvInfo): Uri | undefined {
90+
function getSearchLocation(env: PythonEnvInfo, searchLocation: Uri | undefined): Uri | undefined {
91+
if (searchLocation) {
92+
// A search location has already been established by the downstream locators, simply use that.
93+
return searchLocation;
94+
}
9195
const folders = getWorkspaceFolderPaths();
9296
const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f));
9397
if (isRootedEnv) {

extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
import { toLower, uniq, uniqBy } from 'lodash';
55
import * as path from 'path';
6+
import { Uri } from 'vscode';
67
import { chain, iterable } from '../../../../common/utils/async';
78
import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform';
89
import { PythonEnvKind } from '../../info';
910
import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator';
1011
import { FSWatchingLocator } from './fsWatchingLocator';
1112
import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils';
1213
import { pathExists, untildify } from '../../../common/externalDependencies';
13-
import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv';
14+
import { getProjectDir, isPipenvEnvironment } from '../../../common/environmentManagers/pipenv';
1415
import {
1516
isVenvEnvironment,
1617
isVirtualenvEnvironment,
@@ -57,6 +58,18 @@ async function getGlobalVirtualEnvDirs(): Promise<string[]> {
5758
return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs);
5859
}
5960

61+
async function getSearchLocation(env: BasicEnvInfo): Promise<Uri | undefined> {
62+
if (env.kind === PythonEnvKind.Pipenv) {
63+
// Pipenv environments are created only for a specific project, so they must only
64+
// appear if that particular project is being queried.
65+
const project = await getProjectDir(path.dirname(path.dirname(env.executablePath)));
66+
if (project) {
67+
return Uri.file(project);
68+
}
69+
}
70+
return undefined;
71+
}
72+
6073
/**
6174
* Gets the virtual environment kind for a given interpreter path.
6275
* This only checks for environments created using venv, virtualenv,
@@ -123,8 +136,9 @@ export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator {
123136
// check multiple times. Those checks are file system heavy and
124137
// we can use the kind to determine this anyway.
125138
const kind = await getVirtualEnvKind(filename);
139+
const searchLocation = await getSearchLocation({ kind, executablePath: filename });
126140
try {
127-
yield { kind, executablePath: filename };
141+
yield { kind, executablePath: filename, searchLocation };
128142
traceVerbose(`Global Virtual Environment: [added] ${filename}`);
129143
} catch (ex) {
130144
traceError(`Failed to process environment: ${filename}`, ex);

extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
'use strict';
55

66
import * as path from 'path';
7+
import { Uri } from 'vscode';
78
import { chain, iterable } from '../../../../common/utils/async';
89
import { PythonEnvKind } from '../../info';
910
import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator';
@@ -59,7 +60,7 @@ export class PoetryLocator extends LazyResourceBasedLocator {
5960
// We should extract the kind here to avoid doing is*Environment()
6061
// check multiple times. Those checks are file system heavy and
6162
// we can use the kind to determine this anyway.
62-
yield { executablePath: filename, kind };
63+
yield { executablePath: filename, kind, searchLocation: Uri.file(root) };
6364
traceVerbose(`Poetry Virtual Environment: [added] ${filename}`);
6465
} catch (ex) {
6566
traceError(`Failed to process environment: ${filename}`, ex);

extensions/positron-python/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async function getPipfileIfLocal(interpreterPath: string): Promise<string | unde
7070
* Returns the project directory for pipenv environments given the environment folder
7171
* @param envFolder Path to the environment folder
7272
*/
73-
async function getProjectDir(envFolder: string): Promise<string | undefined> {
73+
export async function getProjectDir(envFolder: string): Promise<string | undefined> {
7474
// Global pipenv environments have a .project file with the absolute path to the project
7575
// See https://github.com/pypa/pipenv/blob/v2018.6.25/CHANGELOG.rst#features--improvements
7676
// This is the layout we expect

extensions/positron-python/src/client/pythonEnvironments/info/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ export enum EnvironmentType {
2525
Global = 'Global',
2626
System = 'System',
2727
}
28+
/**
29+
* These envs are only created for a specific workspace, which we're able to detect.
30+
*/
31+
export const workspaceVirtualEnvTypes = [EnvironmentType.Poetry, EnvironmentType.Pipenv];
2832

2933
export const virtualEnvTypes = [
30-
EnvironmentType.Poetry,
31-
EnvironmentType.Pipenv,
32-
EnvironmentType.Hatch,
34+
...workspaceVirtualEnvTypes,
35+
EnvironmentType.Hatch, // This is also a workspace virtual env, but we're not treating it as such as of today.
3336
EnvironmentType.Venv,
3437
EnvironmentType.VirtualEnvWrapper,
3538
EnvironmentType.Conda,

extensions/positron-python/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ suite('Python envs locator - Environments Resolver', () => {
5757
/**
5858
* Returns the expected environment to be returned by Environment info service
5959
*/
60-
function createExpectedEnvInfo(env: PythonEnvInfo, expectedDisplay: string): PythonEnvInfo {
60+
function createExpectedEnvInfo(
61+
env: PythonEnvInfo,
62+
expectedDisplay: string,
63+
expectedDetailedDisplay: string,
64+
): PythonEnvInfo {
6165
const updatedEnv = cloneDeep(env);
6266
updatedEnv.version = {
6367
...parseVersion('3.8.3-final'),
@@ -67,7 +71,7 @@ suite('Python envs locator - Environments Resolver', () => {
6771
updatedEnv.executable.sysPrefix = 'path';
6872
updatedEnv.arch = Architecture.x64;
6973
updatedEnv.display = expectedDisplay;
70-
updatedEnv.detailedDisplayName = expectedDisplay;
74+
updatedEnv.detailedDisplayName = expectedDetailedDisplay;
7175
if (env.kind === PythonEnvKind.Conda) {
7276
env.type = PythonEnvType.Conda;
7377
}
@@ -82,6 +86,7 @@ suite('Python envs locator - Environments Resolver', () => {
8286
location = '',
8387
display: string | undefined = undefined,
8488
type?: PythonEnvType,
89+
detailedDisplay?: string,
8590
): PythonEnvInfo {
8691
return {
8792
name,
@@ -94,7 +99,7 @@ suite('Python envs locator - Environments Resolver', () => {
9499
mtime: -1,
95100
},
96101
display,
97-
detailedDisplayName: display,
102+
detailedDisplayName: detailedDisplay ?? display,
98103
version,
99104
arch: Architecture.Unknown,
100105
distro: { org: '' },
@@ -134,8 +139,9 @@ suite('Python envs locator - Environments Resolver', () => {
134139
undefined,
135140
'win1',
136141
path.join(testVirtualHomeDir, '.venvs', 'win1'),
137-
"Python ('win1': venv)",
142+
"Python ('win1')",
138143
PythonEnvType.Virtual,
144+
"Python ('win1': venv)",
139145
);
140146
const envsReturnedByParentLocator = [env1];
141147
const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator);
@@ -170,7 +176,11 @@ suite('Python envs locator - Environments Resolver', () => {
170176
const envs = await getEnvsWithUpdates(iterator);
171177

172178
assertEnvsEqual(envs, [
173-
createExpectedEnvInfo(resolvedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"),
179+
createExpectedEnvInfo(
180+
resolvedEnvReturnedByBasicResolver,
181+
"Python 3.8.3 ('win1')",
182+
"Python 3.8.3 ('win1': venv)",
183+
),
174184
]);
175185
});
176186

@@ -237,7 +247,11 @@ suite('Python envs locator - Environments Resolver', () => {
237247

238248
// Assert
239249
assertEnvsEqual(envs, [
240-
createExpectedEnvInfo(resolvedUpdatedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"),
250+
createExpectedEnvInfo(
251+
resolvedUpdatedEnvReturnedByBasicResolver,
252+
"Python 3.8.3 ('win1')",
253+
"Python 3.8.3 ('win1': venv)",
254+
),
241255
]);
242256
didUpdate.dispose();
243257
});
@@ -377,7 +391,11 @@ suite('Python envs locator - Environments Resolver', () => {
377391

378392
assertEnvEqual(
379393
expected,
380-
createExpectedEnvInfo(resolvedEnvReturnedByBasicResolver, "Python 3.8.3 ('win1': venv)"),
394+
createExpectedEnvInfo(
395+
resolvedEnvReturnedByBasicResolver,
396+
"Python 3.8.3 ('win1')",
397+
"Python 3.8.3 ('win1': venv)",
398+
),
381399
);
382400
});
383401

extensions/positron-python/src/test/pythonEnvironments/base/locators/envTestUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,12 @@ export function assertBasicEnvsEqual(actualEnvs: BasicEnvInfo[], expectedEnvs: B
103103
const [actual, expected] = value;
104104
if (actual) {
105105
actual.source = actual.source ?? [];
106+
actual.searchLocation = actual.searchLocation ?? undefined;
106107
actual.source.sort();
107108
}
108109
if (expected) {
109110
expected.source = expected.source ?? [];
111+
expected.searchLocation = expected.searchLocation ?? undefined;
110112
expected.source.sort();
111113
}
112114
assert.deepStrictEqual(actual, expected);

0 commit comments

Comments
 (0)