Skip to content

Commit 5250134

Browse files
committed
feat: add error handling for broken Python environments
1 parent 6e5b27b commit 5250134

File tree

4 files changed

+83
-18
lines changed

4 files changed

+83
-18
lines changed

src/api.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ export interface PythonEnvironmentInfo {
217217
* Optional `group` for this environment. This is used to group environments in the Environment Manager UI.
218218
*/
219219
readonly group?: string | EnvironmentGroupInfo;
220+
221+
/**
222+
* Error message if the environment is broken or invalid.
223+
* When set, indicates this environment has issues (e.g., broken symlinks, missing Python executable).
224+
* The UI should display a warning indicator and show this message to help users diagnose and fix the issue.
225+
*/
226+
readonly error?: string;
220227
}
221228

222229
/**
@@ -922,7 +929,8 @@ export interface PythonProjectEnvironmentApi {
922929
}
923930

924931
export interface PythonEnvironmentManagerApi
925-
extends PythonEnvironmentManagerRegistrationApi,
932+
extends
933+
PythonEnvironmentManagerRegistrationApi,
926934
PythonEnvironmentItemApi,
927935
PythonEnvironmentManagementApi,
928936
PythonEnvironmentsApi,
@@ -987,7 +995,8 @@ export interface PythonPackageManagementApi {
987995
}
988996

989997
export interface PythonPackageManagerApi
990-
extends PythonPackageManagerRegistrationApi,
998+
extends
999+
PythonPackageManagerRegistrationApi,
9911000
PythonPackageGetterApi,
9921001
PythonPackageManagementApi,
9931002
PythonPackageItemApi {}
@@ -1206,10 +1215,7 @@ export interface PythonBackgroundRunApi {
12061215
}
12071216

12081217
export interface PythonExecutionApi
1209-
extends PythonTerminalCreateApi,
1210-
PythonTerminalRunApi,
1211-
PythonTaskRunApi,
1212-
PythonBackgroundRunApi {}
1218+
extends PythonTerminalCreateApi, PythonTerminalRunApi, PythonTaskRunApi, PythonBackgroundRunApi {}
12131219

12141220
/**
12151221
* Event arguments for when the monitored `.env` files or any other sources change.
@@ -1258,7 +1264,8 @@ export interface PythonEnvironmentVariablesApi {
12581264
* The API for interacting with Python environments, package managers, and projects.
12591265
*/
12601266
export interface PythonEnvironmentApi
1261-
extends PythonEnvironmentManagerApi,
1267+
extends
1268+
PythonEnvironmentManagerApi,
12621269
PythonPackageManagerApi,
12631270
PythonProjectApi,
12641271
PythonExecutionApi,

src/features/views/treeViewItems.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export class EnvManagerTreeItem implements EnvTreeItem {
4545
export class PythonGroupEnvTreeItem implements EnvTreeItem {
4646
public readonly kind = EnvTreeItemKind.environmentGroup;
4747
public readonly treeItem: TreeItem;
48-
constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) {
48+
constructor(
49+
public readonly parent: EnvManagerTreeItem,
50+
public readonly group: string | EnvironmentGroupInfo,
51+
) {
4952
const label = typeof group === 'string' ? group : group.name;
5053
const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed);
5154
item.contextValue = `pythonEnvGroup;${this.parent.manager.id}:${label};`;
@@ -69,6 +72,8 @@ export class PythonEnvTreeItem implements EnvTreeItem {
6972
) {
7073
let name = environment.displayName ?? environment.name;
7174
let tooltip = environment.tooltip ?? environment.description;
75+
const isBroken = !!environment.error;
76+
7277
if (selected) {
7378
const selectedTooltip =
7479
selected === 'global' ? EnvViewStrings.selectedGlobalTooltip : EnvViewStrings.selectedWorkspaceTooltip;
@@ -77,21 +82,25 @@ export class PythonEnvTreeItem implements EnvTreeItem {
7782

7883
const item = new TreeItem(name, TreeItemCollapsibleState.Collapsed);
7984
item.contextValue = this.getContextValue();
80-
item.description = environment.description;
81-
item.tooltip = tooltip;
82-
item.iconPath = environment.iconPath;
85+
// Show error message for broken environments
86+
item.description = isBroken ? environment.error : environment.description;
87+
item.tooltip = isBroken ? environment.error : tooltip;
88+
// Show warning icon for broken environments
89+
item.iconPath = isBroken ? new ThemeIcon('warning') : environment.iconPath;
8390
this.treeItem = item;
8491
}
8592

8693
private getContextValue() {
87-
const activatable = isActivatableEnvironment(this.environment) ? 'activatable' : '';
94+
const isBroken = !!this.environment.error;
95+
const activatable = !isBroken && isActivatableEnvironment(this.environment) ? 'activatable' : '';
96+
const broken = isBroken ? 'broken' : '';
8897
let remove = '';
8998
if (this.parent.kind === EnvTreeItemKind.environmentGroup) {
9099
remove = this.parent.parent.manager.supportsRemove ? 'remove' : '';
91100
} else if (this.parent.kind === EnvTreeItemKind.manager) {
92101
remove = this.parent.manager.supportsRemove ? 'remove' : '';
93102
}
94-
const parts = ['pythonEnvironment', remove, activatable].filter(Boolean);
103+
const parts = ['pythonEnvironment', remove, activatable, broken].filter(Boolean);
95104
return parts.join(';') + ';';
96105
}
97106
}
@@ -240,16 +249,22 @@ export class ProjectEnvironment implements ProjectTreeItem {
240249
public readonly kind = ProjectTreeItemKind.environment;
241250
public readonly id: string;
242251
public readonly treeItem: TreeItem;
243-
constructor(public readonly parent: ProjectItem, public readonly environment: PythonEnvironment) {
252+
constructor(
253+
public readonly parent: ProjectItem,
254+
public readonly environment: PythonEnvironment,
255+
) {
244256
this.id = this.getId(parent, environment);
257+
const isBroken = !!environment.error;
245258
const item = new TreeItem(
246259
this.environment.displayName ?? this.environment.name,
247260
TreeItemCollapsibleState.Collapsed,
248261
);
249-
item.contextValue = 'python-env';
250-
item.description = this.environment.description;
251-
item.tooltip = this.environment.tooltip;
252-
item.iconPath = this.environment.iconPath;
262+
item.contextValue = isBroken ? 'python-env;broken;' : 'python-env';
263+
// Show error message for broken environments
264+
item.description = isBroken ? this.environment.error : this.environment.description;
265+
item.tooltip = isBroken ? this.environment.error : this.environment.tooltip;
266+
// Show warning icon for broken environments
267+
item.iconPath = isBroken ? new ThemeIcon('warning') : this.environment.iconPath;
253268
this.treeItem = item;
254269
}
255270

src/managers/builtin/venvUtils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,31 @@ function getName(binPath: string): string {
128128
}
129129

130130
async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo> {
131+
// Handle broken environments that have an error field
132+
if (env.error) {
133+
const venvName = env.name ?? (env.prefix ? path.basename(env.prefix) : 'Unknown');
134+
const name = `${venvName} (broken)`;
135+
136+
return {
137+
name: name,
138+
displayName: name,
139+
shortDisplayName: `(${venvName})`,
140+
displayPath: env.prefix ?? env.executable ?? 'Unknown path',
141+
version: env.version ?? 'Unknown',
142+
description: env.error,
143+
tooltip: env.error,
144+
environmentPath: Uri.file(env.prefix ?? env.executable ?? ''),
145+
iconPath: new ThemeIcon('warning'),
146+
sysPrefix: env.prefix ?? '',
147+
execInfo: {
148+
run: {
149+
executable: env.executable ?? '',
150+
},
151+
},
152+
error: env.error,
153+
};
154+
}
155+
131156
if (env.executable && env.version && env.prefix) {
132157
const venvName = env.name ?? getName(env.executable);
133158
const sv = shortVersion(env.version);
@@ -193,6 +218,19 @@ export async function findVirtualEnvironments(
193218
);
194219

195220
for (const e of envs) {
221+
// Include environments with errors (broken environments) so users can see and diagnose them
222+
if (e.error) {
223+
log.warn(`Broken venv environment detected: ${e.error} - ${JSON.stringify(e)}`);
224+
try {
225+
const env = api.createPythonEnvironmentItem(await getPythonInfo(e), manager);
226+
collection.push(env);
227+
log.info(`Found broken venv environment: ${env.name}`);
228+
} catch (err) {
229+
log.error(`Failed to create broken environment item: ${err}`);
230+
}
231+
continue;
232+
}
233+
196234
if (!(e.prefix && e.executable && e.version)) {
197235
log.warn(`Invalid venv environment: ${JSON.stringify(e)}`);
198236
continue;

src/managers/common/nativePythonFinder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export interface NativeEnvInfo {
5252
project?: string;
5353
arch?: 'x64' | 'x86';
5454
symlinks?: string[];
55+
/**
56+
* Error message if the environment is broken or invalid.
57+
* This is reported by PET when detecting issues like broken symlinks or missing executables.
58+
*/
59+
error?: string;
5560
}
5661

5762
export interface NativeEnvManagerInfo {

0 commit comments

Comments
 (0)