Skip to content

Commit 0f1b4bc

Browse files
authored
Switch to directly invoke python-environments-ext api (#849)
* first draft * tests and split legacy python from python * rebased and fixed * support per file debug with correct interpreter * fix tests * add extra logging * fix tests and add type * remove extra comments * path fix * test fixes * fixes * error * format
1 parent 704dae6 commit 0f1b4bc

File tree

16 files changed

+3125
-332
lines changed

16 files changed

+3125
-332
lines changed

src/extension/common/application/commands/reportIssueCommand.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,39 @@
66
import * as fs from 'fs-extra';
77
import * as path from 'path';
88
import { executeCommand } from '../../vscodeapi';
9-
import { getActiveEnvironmentPath, resolveEnvironment } from '../../python';
109
import { EXTENSION_ROOT_DIR } from '../../constants';
11-
import { getRawVersion } from '../../settings';
1210
import { sendTelemetryEvent } from '../../../telemetry';
1311
import { EventName } from '../../../telemetry/constants';
12+
import { PythonEnvironment } from '../../../envExtApi';
13+
import { traceLog } from '../../log/logging';
14+
import { getActiveEnvironmentPath, resolveEnvironment } from '../../python';
1415

1516
/**
1617
* Allows the user to report an issue related to the Python Debugger extension using our template.
1718
*/
1819
export async function openReportIssue(): Promise<void> {
20+
traceLog('openReportIssue: Starting report issue flow');
1921
const templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md');
2022
const userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md');
2123
const template = await fs.readFile(templatePath, 'utf8');
2224
const userTemplate = await fs.readFile(userDataTemplatePath, 'utf8');
25+
// get active environment and resolve it
2326
const interpreterPath = await getActiveEnvironmentPath();
24-
const interpreter = await resolveEnvironment(interpreterPath);
25-
const virtualEnvKind = interpreter?.environment?.type || 'Unknown';
27+
let interpreter: PythonEnvironment | undefined = undefined;
28+
if (interpreterPath && 'environmentPath' in interpreterPath) {
29+
interpreter = await resolveEnvironment(interpreterPath.environmentPath.fsPath);
30+
} else if (interpreterPath && 'path' in interpreterPath) {
31+
interpreter = await resolveEnvironment(interpreterPath.path);
32+
}
33+
const virtualEnvKind = interpreter && interpreter.envId ? interpreter.envId.managerId : 'Unknown';
34+
const pythonVersion = interpreter?.version ?? 'unknown';
35+
traceLog(`openReportIssue: Resolved pythonVersion='${pythonVersion}' envKind='${virtualEnvKind}'`);
2636

27-
const pythonVersion = getRawVersion(interpreter?.version);
2837
await executeCommand('workbench.action.openIssueReporter', {
2938
extensionId: 'ms-python.debugpy',
3039
issueBody: template,
3140
data: userTemplate.replace('{0}', pythonVersion).replace('{1}', virtualEnvKind),
3241
});
3342
sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {});
43+
traceLog('openReportIssue: Issue reporter command executed');
3444
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/* eslint-disable @typescript-eslint/naming-convention */
5+
import {
6+
ActiveEnvironmentPathChangeEvent,
7+
Environment,
8+
EnvironmentPath,
9+
EnvironmentVariables,
10+
PythonExtension,
11+
ResolvedEnvironment,
12+
Resource,
13+
} from '@vscode/python-extension';
14+
import { EventEmitter, extensions, Uri, Disposable, Extension } from 'vscode';
15+
import { createDeferred } from './utils/async';
16+
import { traceError, traceLog } from './log/logging';
17+
18+
/**
19+
* Interface for the Python extension API.
20+
*/
21+
interface LegacyIExtensionApi {
22+
ready: Promise<void>;
23+
settings: {
24+
getExecutionDetails(resource?: Resource): { execCommand: string[] | undefined };
25+
};
26+
}
27+
28+
/**
29+
* Details about a Python interpreter.
30+
*/
31+
export interface LegacyIInterpreterDetails {
32+
/** Array of path components to the Python executable */
33+
path?: string[];
34+
/** The workspace resource associated with this interpreter */
35+
resource?: Uri;
36+
}
37+
38+
// /** Event emitter for Python interpreter changes */
39+
// const legacyOnDidChangePythonInterpreterEvent = new EventEmitter<LegacyIInterpreterDetails>();
40+
41+
// /** Event that fires when the active Python interpreter changes */
42+
// export const legacyOnDidChangePythonInterpreter: Event<LegacyIInterpreterDetails> =
43+
// legacyOnDidChangePythonInterpreterEvent.event;
44+
/**
45+
* Activates the Python extension and ensures it's ready for use.
46+
* @returns The activated Python extension instance
47+
*/
48+
async function legacyActivateExtension(): Promise<Extension<any> | undefined> {
49+
console.log('Activating Python extension...');
50+
activateEnvsExtension();
51+
const extension = extensions.getExtension('ms-python.python');
52+
if (extension) {
53+
if (!extension.isActive) {
54+
await extension.activate();
55+
}
56+
}
57+
console.log('Python extension activated.');
58+
return extension;
59+
}
60+
/**
61+
* Activates the Python environments extension.
62+
* @returns The activated Python environments extension instance
63+
*/
64+
async function activateEnvsExtension(): Promise<Extension<any> | undefined> {
65+
const extension = extensions.getExtension('ms-python.vscode-python-envs');
66+
if (extension) {
67+
if (!extension.isActive) {
68+
await extension.activate();
69+
}
70+
}
71+
return extension;
72+
}
73+
74+
/**
75+
* Gets the Python extension's API interface.
76+
* @returns The Python extension API or undefined if not available
77+
*/
78+
async function legacyGetPythonExtensionAPI(): Promise<LegacyIExtensionApi | undefined> {
79+
const extension = await legacyActivateExtension();
80+
return extension?.exports as LegacyIExtensionApi;
81+
}
82+
83+
/**
84+
* Gets the Python extension's environment API.
85+
* @returns The Python extension environment API
86+
*/
87+
async function legacyGetPythonExtensionEnviromentAPI(): Promise<PythonExtension> {
88+
// Load the Python extension API
89+
await legacyActivateExtension();
90+
return await PythonExtension.api();
91+
}
92+
93+
/**
94+
* Initializes Python integration by setting up event listeners and getting initial interpreter details.
95+
* @param disposables Array to store disposable resources for cleanup
96+
*/
97+
export async function legacyInitializePython(
98+
disposables: Disposable[],
99+
onDidChangePythonInterpreterEvent: EventEmitter<LegacyIInterpreterDetails>,
100+
): Promise<void> {
101+
try {
102+
traceLog('legacyInitializePython: Starting initialization');
103+
const api = await legacyGetPythonExtensionEnviromentAPI();
104+
105+
if (api) {
106+
disposables.push(
107+
// This event is triggered when the active environment setting changes.
108+
api.environments.onDidChangeActiveEnvironmentPath((e: ActiveEnvironmentPathChangeEvent) => {
109+
traceLog(`legacyInitializePython: Active environment path changed to '${e.path}'`);
110+
let resourceUri: Uri | undefined;
111+
if (e.resource instanceof Uri) {
112+
resourceUri = e.resource;
113+
}
114+
if (e.resource && 'uri' in e.resource) {
115+
// WorkspaceFolder type
116+
resourceUri = e.resource.uri;
117+
}
118+
onDidChangePythonInterpreterEvent.fire({ path: [e.path], resource: resourceUri });
119+
}),
120+
);
121+
122+
traceLog('Waiting for interpreter from python extension.');
123+
onDidChangePythonInterpreterEvent.fire(await legacyGetInterpreterDetails());
124+
traceLog('legacyInitializePython: Initial interpreter details fired');
125+
}
126+
} catch (error) {
127+
traceError('Error initializing python: ', error);
128+
}
129+
}
130+
131+
/**
132+
* Returns all the details needed to execute code within the selected environment,
133+
* corresponding to the specified resource taking into account any workspace-specific settings
134+
* for the workspace to which this resource belongs.
135+
* @param resource Optional workspace resource to get settings for
136+
* @returns Array of command components or undefined if not available
137+
*/
138+
export async function legacyGetSettingsPythonPath(resource?: Uri): Promise<string[] | undefined> {
139+
const api = await legacyGetPythonExtensionAPI();
140+
const execCommand = api?.settings.getExecutionDetails(resource).execCommand;
141+
traceLog(`legacyGetSettingsPythonPath: execCommand='${execCommand?.join(' ')}' resource='${resource?.fsPath}'`);
142+
return execCommand;
143+
}
144+
145+
/**
146+
* Returns the environment variables used by the extension for a resource, which includes the custom
147+
* variables configured by user in `.env` files.
148+
* @param resource Optional workspace resource to get environment variables for
149+
* @returns Environment variables object
150+
*/
151+
export async function legacyGetEnvironmentVariables(resource?: Resource): Promise<EnvironmentVariables> {
152+
const api = await legacyGetPythonExtensionEnviromentAPI();
153+
return Promise.resolve(api.environments.getEnvironmentVariables(resource));
154+
}
155+
156+
/**
157+
* Returns details for the given environment, or `undefined` if the env is invalid.
158+
* @param env Environment to resolve (can be Environment object, path, or string)
159+
* @returns Resolved environment details
160+
*/
161+
export async function legacyResolveEnvironment(
162+
env: Environment | EnvironmentPath | string,
163+
): Promise<ResolvedEnvironment | undefined> {
164+
const api = await legacyGetPythonExtensionEnviromentAPI();
165+
traceLog(`legacyResolveEnvironment: Resolving environment '${typeof env === 'string' ? env : (env as any).path}'`);
166+
const resolved = api.environments.resolveEnvironment(env);
167+
resolved.then((r) => traceLog(`legacyResolveEnvironment: Resolved executable='${r?.executable.uri?.fsPath}'`));
168+
return resolved;
169+
}
170+
171+
/**
172+
* Returns the environment configured by user in settings. Note that this can be an invalid environment, use
173+
* resolve the environment to get full details.
174+
* @param resource Optional workspace resource to get active environment for
175+
* @returns Path to the active environment
176+
*/
177+
export async function legacyGetActiveEnvironmentPath(resource?: Resource): Promise<EnvironmentPath> {
178+
const api = await legacyGetPythonExtensionEnviromentAPI();
179+
const active = api.environments.getActiveEnvironmentPath(resource);
180+
traceLog(
181+
`legacyGetActiveEnvironmentPath: activePath='${active.path}' resource='${
182+
(resource as any)?.uri?.fsPath || (resource as Uri)?.fsPath || ''
183+
}'`,
184+
);
185+
return active;
186+
}
187+
188+
/**
189+
* Gets detailed information about the active Python interpreter.
190+
* @param resource Optional workspace resource to get interpreter details for
191+
* @returns Interpreter details including path and resource information
192+
*/
193+
export async function legacyGetInterpreterDetails(resource?: Uri): Promise<LegacyIInterpreterDetails> {
194+
const api = await legacyGetPythonExtensionEnviromentAPI();
195+
const environment = await api.environments.resolveEnvironment(api.environments.getActiveEnvironmentPath(resource));
196+
if (environment?.executable.uri) {
197+
traceLog(
198+
`legacyGetInterpreterDetails: executable='${environment.executable.uri.fsPath}' resource='${resource?.fsPath}'`,
199+
);
200+
return { path: [environment?.executable.uri.fsPath], resource };
201+
}
202+
traceLog('legacyGetInterpreterDetails: No executable found');
203+
return { path: undefined, resource };
204+
}
205+
206+
/**
207+
* Checks if any Python interpreters are available in the system.
208+
* @returns True if interpreters are found, false otherwise
209+
*/
210+
export async function legacyHasInterpreters(): Promise<boolean> {
211+
const api = await legacyGetPythonExtensionEnviromentAPI();
212+
const onAddedToCollection = createDeferred();
213+
api.environments.onDidChangeEnvironments(async () => {
214+
if (api.environments.known) {
215+
onAddedToCollection.resolve();
216+
}
217+
});
218+
const initialEnvs = api.environments.known;
219+
if (initialEnvs.length > 0) {
220+
traceLog(`legacyHasInterpreters: Found ${initialEnvs.length} initial environments`);
221+
return true;
222+
}
223+
// Initiates a refresh of Python environments within the specified scope.
224+
await Promise.race([onAddedToCollection.promise, api?.environments.refreshEnvironments()]);
225+
const has = api.environments.known.length > 0;
226+
traceLog(`legacyHasInterpreters: After refresh count='${api.environments.known.length}' result='${has}'`);
227+
return has;
228+
}
229+
230+
/**
231+
* Gets environments known to the extension at the time of fetching the property. Note this may not
232+
* contain all environments in the system as a refresh might be going on.
233+
* @returns Array of known Python environments
234+
*/
235+
export async function legacyGetInterpreters(): Promise<readonly Environment[]> {
236+
const api = await legacyGetPythonExtensionEnviromentAPI();
237+
const known = api.environments.known || [];
238+
traceLog(`legacyGetInterpreters: returning ${known.length} environments`);
239+
return known;
240+
}

0 commit comments

Comments
 (0)