Skip to content

Commit 0fba177

Browse files
Kartik Rajwesm
authored andcommitted
Select pyenv environment based on folder .python-version file (microsoft/vscode-python#23094)
1 parent 8d6f3b9 commit 0fba177

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

extensions/positron-python/src/client/interpreter/autoSelection/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio
209209
});
210210
}
211211

212+
await this.envTypeComparer.initialize(resource);
212213
const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment);
213214
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);
214215
let recommendedInterpreter: PythonEnvironment | undefined;

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pytho
1515
import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion';
1616
import { IInterpreterHelper } from '../contracts';
1717
import { IInterpreterComparer } from './types';
18+
import { getActivePyenvForDirectory } from '../../pythonEnvironments/common/environmentManagers/pyenv';
19+
import { arePathsSame } from '../../common/platform/fs-paths';
1820

1921
// --- Start Positron ---
2022
import { getPyenvDir } from '../../pythonEnvironments/common/environmentManagers/pyenv';
@@ -36,6 +38,8 @@ export enum EnvLocationHeuristic {
3638
export class EnvironmentTypeComparer implements IInterpreterComparer {
3739
private workspaceFolderPath: string;
3840

41+
private preferredPyenvInterpreterPath = new Map<string, string | undefined>();
42+
3943
constructor(@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) {
4044
this.workspaceFolderPath = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri.fsPath ?? '';
4145
}
@@ -64,6 +68,18 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
6468
return envLocationComparison;
6569
}
6670

71+
if (a.envType === EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) {
72+
const preferredPyenv = this.preferredPyenvInterpreterPath.get(this.workspaceFolderPath);
73+
if (preferredPyenv) {
74+
if (arePathsSame(preferredPyenv, b.path)) {
75+
return 1;
76+
}
77+
if (arePathsSame(preferredPyenv, a.path)) {
78+
return -1;
79+
}
80+
}
81+
}
82+
6783
// Check environment type.
6884
const envTypeComparison = compareEnvironmentType(a, b);
6985
if (envTypeComparison !== 0) {
@@ -95,6 +111,16 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
95111
return nameA > nameB ? 1 : -1;
96112
}
97113

114+
public async initialize(resource: Resource): Promise<void> {
115+
const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource);
116+
const cwd = workspaceUri?.folderUri.fsPath;
117+
if (!cwd) {
118+
return;
119+
}
120+
const preferredPyenvInterpreter = await getActivePyenvForDirectory(cwd);
121+
this.preferredPyenvInterpreterPath.set(cwd, preferredPyenvInterpreter);
122+
}
123+
98124
public getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined {
99125
// When recommending an intepreter for a workspace, we either want to return a local one
100126
// or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment
@@ -265,10 +291,17 @@ export function getEnvLocationHeuristic(environment: PythonEnvironment, workspac
265291
*/
266292
function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment): number {
267293
// --- Start Positron ---
268-
if (!a.type && !b.type && a.envType !== EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) {
269-
// Don't lump Pyenv environments together with all other global interpreters.
270-
// --- End Positron ---
271-
// Return 0 if two global interpreters are being compared.
294+
// if (!a.type && !b.type && a.envType !== EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) {
295+
// Don't lump Pyenv environments together with all other global interpreters.
296+
// --- End Positron ---
297+
if (!a.type && !b.type) {
298+
if (a.envType === EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) {
299+
return -1;
300+
}
301+
if (a.envType !== EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) {
302+
return 1;
303+
}
304+
272305
return 0;
273306
}
274307
const envTypeByPriority = getPrioritizedEnvironmentType();

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface ISpecialQuickPickItem extends QuickPickItem {
5858

5959
export const IInterpreterComparer = Symbol('IInterpreterComparer');
6060
export interface IInterpreterComparer {
61+
initialize(resource: Resource): Promise<void>;
6162
compare(a: PythonEnvironment, b: PythonEnvironment): number;
6263
getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined;
6364
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform';
3-
import { arePathsSame, isParentPath, pathExists } from '../externalDependencies';
3+
import { arePathsSame, isParentPath, pathExists, shellExecute } from '../externalDependencies';
4+
import { traceVerbose } from '../../../logging';
45

56
export function getPyenvDir(): string {
67
// Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix.
@@ -20,6 +21,29 @@ export function getPyenvDir(): string {
2021
return pyenvDir;
2122
}
2223

24+
async function getPyenvBinary(): Promise<string | undefined> {
25+
const pyenvDir = getPyenvDir();
26+
const pyenvBin = path.join(pyenvDir, 'bin', 'pyenv');
27+
if (await pathExists(pyenvBin)) {
28+
return pyenvBin;
29+
}
30+
return undefined;
31+
}
32+
33+
export async function getActivePyenvForDirectory(cwd: string): Promise<string | undefined> {
34+
const pyenvBin = await getPyenvBinary();
35+
if (!pyenvBin) {
36+
return undefined;
37+
}
38+
try {
39+
const pyenvInterpreterPath = await shellExecute(`${pyenvBin} which python`, { cwd });
40+
return pyenvInterpreterPath.stdout.trim();
41+
} catch (ex) {
42+
traceVerbose(ex);
43+
return undefined;
44+
}
45+
}
46+
2347
export function getPyenvVersionsDir(): string {
2448
return path.join(getPyenvDir(), 'versions');
2549
}

extensions/positron-python/src/test/configuration/environmentTypeComparer.unit.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../../client/interpreter/configuration/environmentTypeComparer';
1717
import { IInterpreterHelper } from '../../client/interpreter/contracts';
1818
import { PythonEnvType } from '../../client/pythonEnvironments/base/info';
19+
import * as pyenv from '../../client/pythonEnvironments/common/environmentManagers/pyenv';
1920
import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info';
2021
// --- Start Positron ---
2122
import * as externalDependencies from '../../client/pythonEnvironments/common/externalDependencies';
@@ -28,6 +29,7 @@ suite('Environment sorting', () => {
2829
let interpreterHelper: IInterpreterHelper;
2930
let getActiveWorkspaceUriStub: sinon.SinonStub;
3031
let getInterpreterTypeDisplayNameStub: sinon.SinonStub;
32+
const preferredPyenv = path.join('path', 'to', 'preferred', 'pyenv');
3133

3234
setup(() => {
3335
getActiveWorkspaceUriStub = sinon.stub().returns({ folderUri: { fsPath: workspacePath } });
@@ -37,6 +39,8 @@ suite('Environment sorting', () => {
3739
getActiveWorkspaceUri: getActiveWorkspaceUriStub,
3840
getInterpreterTypeDisplayName: getInterpreterTypeDisplayNameStub,
3941
} as unknown) as IInterpreterHelper;
42+
const getActivePyenvForDirectory = sinon.stub(pyenv, 'getActivePyenvForDirectory');
43+
getActivePyenvForDirectory.resolves(preferredPyenv);
4044
});
4145

4246
teardown(() => {
@@ -171,6 +175,33 @@ suite('Environment sorting', () => {
171175
expected: -1,
172176
},
173177
// --- End Positron ---
178+
{
179+
title: 'Preferred Pyenv interpreter should come before any global interpreter',
180+
envA: {
181+
envType: EnvironmentType.Pyenv,
182+
version: { major: 3, minor: 12, patch: 2 },
183+
path: preferredPyenv,
184+
} as PythonEnvironment,
185+
envB: {
186+
envType: EnvironmentType.Pyenv,
187+
version: { major: 3, minor: 10, patch: 2 },
188+
path: path.join('path', 'to', 'normal', 'pyenv'),
189+
} as PythonEnvironment,
190+
expected: -1,
191+
},
192+
{
193+
title: 'Pyenv interpreters should come first when there are global interpreters',
194+
envA: {
195+
envType: EnvironmentType.Global,
196+
version: { major: 3, minor: 10, patch: 2 },
197+
} as PythonEnvironment,
198+
envB: {
199+
envType: EnvironmentType.Pyenv,
200+
version: { major: 3, minor: 7, patch: 2 },
201+
path: path.join('path', 'to', 'normal', 'pyenv'),
202+
} as PythonEnvironment,
203+
expected: 1,
204+
},
174205
{
175206
title: 'Global environment should not come first when there are global envs',
176207
envA: {
@@ -307,8 +338,9 @@ suite('Environment sorting', () => {
307338
];
308339

309340
testcases.forEach(({ title, envA, envB, expected }) => {
310-
test(title, () => {
341+
test(title, async () => {
311342
const envTypeComparer = new EnvironmentTypeComparer(interpreterHelper);
343+
await envTypeComparer.initialize(undefined);
312344
const result = envTypeComparer.compare(envA, envB);
313345

314346
assert.strictEqual(result, expected);

0 commit comments

Comments
 (0)