Skip to content

Commit f82af4e

Browse files
authored
Add error handling for broken Python environments (#1189)
Fixes #1178 Implement error handling for broken Python environments, enhancing the user interface to display warnings and error messages. This allows users to diagnose and address issues more effectively. <img width="413" height="104" alt="image" src="https://github.com/user-attachments/assets/ef39a9d7-78ee-4049-b9bd-79d55703b9e6" />
1 parent 6e5b27b commit f82af4e

File tree

6 files changed

+101
-19
lines changed

6 files changed

+101
-19
lines changed

package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"publisher": "ms-python",
77
"preview": true,
88
"engines": {
9-
"vscode": "^1.110.0-20260204"
9+
"vscode": "^1.110.0-20260204"
1010
},
1111
"categories": [
1212
"Other"
@@ -457,6 +457,10 @@
457457
"command": "python-envs.remove",
458458
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*;remove;.*/"
459459
},
460+
{
461+
"command": "python-envs.remove",
462+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*;remove;.*/"
463+
},
460464
{
461465
"command": "python-envs.setEnv",
462466
"group": "inline",
@@ -491,6 +495,16 @@
491495
"group": "inline",
492496
"when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/ && viewItem =~ /.*copied.*/"
493497
},
498+
{
499+
"command": "python-envs.copyEnvPath",
500+
"group": "inline",
501+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /^((?!copied).)*$/"
502+
},
503+
{
504+
"command": "python-envs.copyEnvPathCopied",
505+
"group": "inline",
506+
"when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /.*copied.*/"
507+
},
494508
{
495509
"command": "python-envs.uninstallPackage",
496510
"group": "inline",

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: 27 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,26 @@ 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' : '';
8896
let remove = '';
8997
if (this.parent.kind === EnvTreeItemKind.environmentGroup) {
9098
remove = this.parent.parent.manager.supportsRemove ? 'remove' : '';
9199
} else if (this.parent.kind === EnvTreeItemKind.manager) {
92100
remove = this.parent.manager.supportsRemove ? 'remove' : '';
93101
}
94-
const parts = ['pythonEnvironment', remove, activatable].filter(Boolean);
102+
// Use different base context for broken environments so normal actions don't show
103+
const baseContext = isBroken ? 'pythonBrokenEnvironment' : 'pythonEnvironment';
104+
const parts = [baseContext, remove, activatable].filter(Boolean);
95105
return parts.join(';') + ';';
96106
}
97107
}
@@ -240,16 +250,22 @@ export class ProjectEnvironment implements ProjectTreeItem {
240250
public readonly kind = ProjectTreeItemKind.environment;
241251
public readonly id: string;
242252
public readonly treeItem: TreeItem;
243-
constructor(public readonly parent: ProjectItem, public readonly environment: PythonEnvironment) {
253+
constructor(
254+
public readonly parent: ProjectItem,
255+
public readonly environment: PythonEnvironment,
256+
) {
244257
this.id = this.getId(parent, environment);
258+
const isBroken = !!environment.error;
245259
const item = new TreeItem(
246260
this.environment.displayName ?? this.environment.name,
247261
TreeItemCollapsibleState.Collapsed,
248262
);
249-
item.contextValue = 'python-env';
250-
item.description = this.environment.description;
251-
item.tooltip = this.environment.tooltip;
252-
item.iconPath = this.environment.iconPath;
263+
item.contextValue = isBroken ? 'python-env;broken;' : 'python-env';
264+
// Show error message for broken environments
265+
item.description = isBroken ? this.environment.error : this.environment.description;
266+
item.tooltip = isBroken ? this.environment.error : this.environment.tooltip;
267+
// Show warning icon for broken environments
268+
item.iconPath = isBroken ? new ThemeIcon('warning') : this.environment.iconPath;
253269
this.treeItem = item;
254270
}
255271

src/internal.api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
338338
public readonly execInfo: PythonEnvironmentExecutionInfo;
339339
public readonly sysPrefix: string;
340340
public readonly group?: string | EnvironmentGroupInfo;
341+
public readonly error?: string;
341342

342343
constructor(
343344
public readonly envId: PythonEnvironmentId,
@@ -355,6 +356,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment {
355356
this.execInfo = info.execInfo;
356357
this.sysPrefix = info.sysPrefix;
357358
this.group = info.group;
359+
this.error = info.error;
358360
}
359361
}
360362

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)