Skip to content

Commit 501b182

Browse files
edvilmeGitHub CopilotCopilotCopilot
authored
Template Sync: Prefer Python Environments extension for interpreter resolution with legacy fallback (#643)
Co-authored-by: GitHub Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 6e3d69a commit 501b182

File tree

4 files changed

+194
-8
lines changed

4 files changed

+194
-8
lines changed

package-lock.json

Lines changed: 21 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
"dependencies": {
187187
"@vscode/python-extension": "^1.0.5",
188188
"fs-extra": "^11.3.1",
189+
"semver": "^7.7.4",
189190
"vscode-languageclient": "^9.0.1"
190191
},
191192
"devDependencies": {
@@ -195,6 +196,7 @@
195196
"@types/glob": "^9.0.0",
196197
"@types/mocha": "^10.0.10",
197198
"@types/node": "22.x",
199+
"@types/semver": "^7.7.1",
198200
"@types/sinon": "^17.0.4",
199201
"@types/vscode": "^1.74.0",
200202
"@typescript-eslint/eslint-plugin": "^8.40.0",

src/common/python.ts

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

4-
import { commands, Disposable, Event, EventEmitter, Uri } from 'vscode';
4+
import { commands, Disposable, Event, EventEmitter, extensions, Uri } from 'vscode';
55
import { traceError, traceLog } from './logging';
66
import { PythonExtension, ResolvedEnvironment } from '@vscode/python-extension';
7+
import * as semver from 'semver';
8+
import type { PythonEnvironment, PythonEnvironmentsAPI } from '../typings/pythonEnvironments';
79
import { PYTHON_MAJOR, PYTHON_MINOR, PYTHON_VERSION } from './constants';
810
import { getProjectRoot } from './utilities';
911

@@ -12,6 +14,35 @@ export interface IInterpreterDetails {
1214
resource?: Uri;
1315
}
1416

17+
function convertToResolvedEnvironment(environment: PythonEnvironment): ResolvedEnvironment | undefined {
18+
const runConfig = environment.execInfo?.activatedRun ?? environment.execInfo?.run;
19+
const executable = runConfig?.executable;
20+
if (!executable) {
21+
return undefined;
22+
}
23+
const coerced = semver.coerce(environment.version);
24+
return {
25+
id: environment.envId?.id ?? '',
26+
path: executable,
27+
executable: {
28+
uri: Uri.file(executable),
29+
bitness: 'Unknown',
30+
sysPrefix: environment.sysPrefix ?? '',
31+
},
32+
version: coerced
33+
? {
34+
major: coerced.major,
35+
minor: coerced.minor,
36+
micro: coerced.patch,
37+
release: { level: 'final', serial: 0 },
38+
sysVersion: environment.version ?? '',
39+
}
40+
: undefined,
41+
environment: undefined,
42+
tools: [],
43+
} as ResolvedEnvironment;
44+
}
45+
1546
const onDidChangePythonInterpreterEvent = new EventEmitter<void>();
1647
export const onDidChangePythonInterpreter: Event<void> = onDidChangePythonInterpreterEvent.event;
1748

@@ -24,6 +55,34 @@ async function getPythonExtensionAPI(): Promise<PythonExtension | undefined> {
2455
return _api;
2556
}
2657

58+
const PYTHON_ENVIRONMENTS_EXTENSION_ID = 'ms-python.vscode-python-envs';
59+
60+
let _envsApi: PythonEnvironmentsAPI | undefined;
61+
async function getEnvironmentsExtensionAPI(): Promise<PythonEnvironmentsAPI | undefined> {
62+
if (_envsApi) {
63+
return _envsApi;
64+
}
65+
const extension = extensions.getExtension(PYTHON_ENVIRONMENTS_EXTENSION_ID);
66+
if (!extension) {
67+
return undefined;
68+
}
69+
try {
70+
if (!extension.isActive) {
71+
await extension.activate();
72+
}
73+
const api = extension.exports;
74+
if (!api) {
75+
traceError('Python environments extension did not provide any exports.');
76+
return undefined;
77+
}
78+
_envsApi = api as PythonEnvironmentsAPI;
79+
return _envsApi;
80+
} catch (ex) {
81+
traceError('Failed to activate or retrieve API from Python environments extension.', ex as Error);
82+
return undefined;
83+
}
84+
}
85+
2786
function sameInterpreter(a: string[], b: string[]): boolean {
2887
if (a.length !== b.length) {
2988
return false;
@@ -63,6 +122,22 @@ async function refreshServerPython(): Promise<void> {
63122

64123
export async function initializePython(disposables: Disposable[]): Promise<void> {
65124
try {
125+
// Prefer the Python Environments extension if it's available, as it provides a more comprehensive view of the available environments.
126+
const envsApi = await getEnvironmentsExtensionAPI();
127+
128+
if (envsApi) {
129+
disposables.push(
130+
envsApi.onDidChangeEnvironment(async () => {
131+
await refreshServerPython();
132+
}),
133+
);
134+
135+
traceLog('Waiting for interpreter from Python environments extension.');
136+
await refreshServerPython();
137+
return;
138+
}
139+
140+
// Fall back to legacy ms-python.python extension API
66141
const api = await getPythonExtensionAPI();
67142

68143
if (api) {
@@ -80,12 +155,51 @@ export async function initializePython(disposables: Disposable[]): Promise<void>
80155
}
81156
}
82157

158+
// TODO: Unused code
83159
export async function resolveInterpreter(interpreter: string[]): Promise<ResolvedEnvironment | undefined> {
160+
const envsApi = await getEnvironmentsExtensionAPI();
161+
if (envsApi) {
162+
const environment = await envsApi.resolveEnvironment(Uri.file(interpreter[0]));
163+
if (!environment) {
164+
return undefined;
165+
}
166+
return convertToResolvedEnvironment(environment);
167+
}
84168
const api = await getPythonExtensionAPI();
85169
return api?.environments.resolveEnvironment(interpreter[0]);
86170
}
87171

88172
export async function getInterpreterDetails(resource?: Uri): Promise<IInterpreterDetails> {
173+
// Prefer the Python Environments extension if it's available, as it provides a more comprehensive view of the available environments.
174+
const envsApi = await getEnvironmentsExtensionAPI();
175+
if (envsApi) {
176+
try {
177+
const environment = await envsApi.getEnvironment(resource);
178+
if (environment) {
179+
const coerced = semver.coerce(environment.version);
180+
const runConfig = environment.execInfo?.activatedRun ?? environment.execInfo?.run;
181+
const executable = runConfig?.executable;
182+
const args = runConfig?.args ?? [];
183+
if (coerced && coerced.major === PYTHON_MAJOR && coerced.minor >= PYTHON_MINOR) {
184+
if (executable) {
185+
return { path: [executable, ...args], resource };
186+
}
187+
traceError('No executable found for selected Python environment.');
188+
return { path: undefined, resource };
189+
}
190+
traceError(`Python version ${environment.version} is not supported.`);
191+
traceError(`Selected python path: ${runConfig?.executable}`);
192+
traceError(`Supported versions are ${PYTHON_VERSION} and above.`);
193+
return { path: undefined, resource };
194+
}
195+
// No environment found via envs API, fall through to legacy resolver.
196+
} catch (error) {
197+
traceError('Error getting interpreter from Python environments extension: ', error);
198+
// Fall through to legacy resolver.
199+
}
200+
}
201+
202+
// Fall back to legacy ms-python.python extension API
89203
const api = await getPythonExtensionAPI();
90204
const environment = await api?.environments.resolveEnvironment(
91205
api?.environments.getActiveEnvironmentPath(resource),
@@ -96,13 +210,18 @@ export async function getInterpreterDetails(resource?: Uri): Promise<IInterprete
96210
return { path: undefined, resource };
97211
}
98212

213+
// TODO: The Python Environments extension does not expose a debug API yet; uses legacy ms-python.python
99214
export async function getDebuggerPath(): Promise<string | undefined> {
100215
const api = await getPythonExtensionAPI();
101216
return api?.debug.getDebuggerPackagePath();
102217
}
103218

219+
// TODO: Unused code
104220
export async function runPythonExtensionCommand(command: string, ...rest: unknown[]) {
105-
await getPythonExtensionAPI();
221+
const envsApi = await getEnvironmentsExtensionAPI();
222+
if (!envsApi) {
223+
await getPythonExtensionAPI();
224+
}
106225
return await commands.executeCommand(command, ...rest);
107226
}
108227

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/* eslint-disable @typescript-eslint/naming-convention */
5+
import { Event, Uri } from 'vscode';
6+
7+
export interface PythonCommandRunConfiguration {
8+
executable: string;
9+
args?: string[];
10+
}
11+
12+
export interface PythonEnvironmentExecutionInfo {
13+
run: PythonCommandRunConfiguration;
14+
activatedRun?: PythonCommandRunConfiguration;
15+
activation?: PythonCommandRunConfiguration[];
16+
}
17+
18+
export interface PythonEnvironmentInfo {
19+
name: string;
20+
displayName: string;
21+
version: string;
22+
environmentPath: Uri;
23+
execInfo: PythonEnvironmentExecutionInfo;
24+
sysPrefix: string;
25+
}
26+
27+
export interface PythonEnvironmentId {
28+
id: string;
29+
managerId: string;
30+
}
31+
32+
export interface PythonEnvironment extends PythonEnvironmentInfo {
33+
envId: PythonEnvironmentId;
34+
}
35+
36+
export type GetEnvironmentScope = undefined | Uri;
37+
38+
export interface DidChangeEnvironmentEventArgs {
39+
uri: Uri | undefined;
40+
old: PythonEnvironment | undefined;
41+
new: PythonEnvironment | undefined;
42+
}
43+
44+
export type ResolveEnvironmentContext = Uri;
45+
46+
export interface PythonEnvironmentsAPI {
47+
getEnvironment(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined>;
48+
resolveEnvironment(context: ResolveEnvironmentContext): Promise<PythonEnvironment | undefined>;
49+
onDidChangeEnvironment: Event<DidChangeEnvironmentEventArgs>;
50+
}

0 commit comments

Comments
 (0)