From 5250134db5b9e0e7b741df02c81f6cf5a4c8f0ab Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 5 Feb 2026 13:35:43 -0800 Subject: [PATCH 1/2] feat: add error handling for broken Python environments --- src/api.ts | 21 ++++++++----- src/features/views/treeViewItems.ts | 37 +++++++++++++++------- src/managers/builtin/venvUtils.ts | 38 +++++++++++++++++++++++ src/managers/common/nativePythonFinder.ts | 5 +++ 4 files changed, 83 insertions(+), 18 deletions(-) diff --git a/src/api.ts b/src/api.ts index 0b60339b..f9231db0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -217,6 +217,13 @@ export interface PythonEnvironmentInfo { * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. */ readonly group?: string | EnvironmentGroupInfo; + + /** + * Error message if the environment is broken or invalid. + * When set, indicates this environment has issues (e.g., broken symlinks, missing Python executable). + * The UI should display a warning indicator and show this message to help users diagnose and fix the issue. + */ + readonly error?: string; } /** @@ -922,7 +929,8 @@ export interface PythonProjectEnvironmentApi { } export interface PythonEnvironmentManagerApi - extends PythonEnvironmentManagerRegistrationApi, + extends + PythonEnvironmentManagerRegistrationApi, PythonEnvironmentItemApi, PythonEnvironmentManagementApi, PythonEnvironmentsApi, @@ -987,7 +995,8 @@ export interface PythonPackageManagementApi { } export interface PythonPackageManagerApi - extends PythonPackageManagerRegistrationApi, + extends + PythonPackageManagerRegistrationApi, PythonPackageGetterApi, PythonPackageManagementApi, PythonPackageItemApi {} @@ -1206,10 +1215,7 @@ export interface PythonBackgroundRunApi { } export interface PythonExecutionApi - extends PythonTerminalCreateApi, - PythonTerminalRunApi, - PythonTaskRunApi, - PythonBackgroundRunApi {} + extends PythonTerminalCreateApi, PythonTerminalRunApi, PythonTaskRunApi, PythonBackgroundRunApi {} /** * Event arguments for when the monitored `.env` files or any other sources change. @@ -1258,7 +1264,8 @@ export interface PythonEnvironmentVariablesApi { * The API for interacting with Python environments, package managers, and projects. */ export interface PythonEnvironmentApi - extends PythonEnvironmentManagerApi, + extends + PythonEnvironmentManagerApi, PythonPackageManagerApi, PythonProjectApi, PythonExecutionApi, diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index ca9b2dbe..8320d57f 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -45,7 +45,10 @@ export class EnvManagerTreeItem implements EnvTreeItem { export class PythonGroupEnvTreeItem implements EnvTreeItem { public readonly kind = EnvTreeItemKind.environmentGroup; public readonly treeItem: TreeItem; - constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) { + constructor( + public readonly parent: EnvManagerTreeItem, + public readonly group: string | EnvironmentGroupInfo, + ) { const label = typeof group === 'string' ? group : group.name; const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); item.contextValue = `pythonEnvGroup;${this.parent.manager.id}:${label};`; @@ -69,6 +72,8 @@ export class PythonEnvTreeItem implements EnvTreeItem { ) { let name = environment.displayName ?? environment.name; let tooltip = environment.tooltip ?? environment.description; + const isBroken = !!environment.error; + if (selected) { const selectedTooltip = selected === 'global' ? EnvViewStrings.selectedGlobalTooltip : EnvViewStrings.selectedWorkspaceTooltip; @@ -77,21 +82,25 @@ export class PythonEnvTreeItem implements EnvTreeItem { const item = new TreeItem(name, TreeItemCollapsibleState.Collapsed); item.contextValue = this.getContextValue(); - item.description = environment.description; - item.tooltip = tooltip; - item.iconPath = environment.iconPath; + // Show error message for broken environments + item.description = isBroken ? environment.error : environment.description; + item.tooltip = isBroken ? environment.error : tooltip; + // Show warning icon for broken environments + item.iconPath = isBroken ? new ThemeIcon('warning') : environment.iconPath; this.treeItem = item; } private getContextValue() { - const activatable = isActivatableEnvironment(this.environment) ? 'activatable' : ''; + const isBroken = !!this.environment.error; + const activatable = !isBroken && isActivatableEnvironment(this.environment) ? 'activatable' : ''; + const broken = isBroken ? 'broken' : ''; let remove = ''; if (this.parent.kind === EnvTreeItemKind.environmentGroup) { remove = this.parent.parent.manager.supportsRemove ? 'remove' : ''; } else if (this.parent.kind === EnvTreeItemKind.manager) { remove = this.parent.manager.supportsRemove ? 'remove' : ''; } - const parts = ['pythonEnvironment', remove, activatable].filter(Boolean); + const parts = ['pythonEnvironment', remove, activatable, broken].filter(Boolean); return parts.join(';') + ';'; } } @@ -240,16 +249,22 @@ export class ProjectEnvironment implements ProjectTreeItem { public readonly kind = ProjectTreeItemKind.environment; public readonly id: string; public readonly treeItem: TreeItem; - constructor(public readonly parent: ProjectItem, public readonly environment: PythonEnvironment) { + constructor( + public readonly parent: ProjectItem, + public readonly environment: PythonEnvironment, + ) { this.id = this.getId(parent, environment); + const isBroken = !!environment.error; const item = new TreeItem( this.environment.displayName ?? this.environment.name, TreeItemCollapsibleState.Collapsed, ); - item.contextValue = 'python-env'; - item.description = this.environment.description; - item.tooltip = this.environment.tooltip; - item.iconPath = this.environment.iconPath; + item.contextValue = isBroken ? 'python-env;broken;' : 'python-env'; + // Show error message for broken environments + item.description = isBroken ? this.environment.error : this.environment.description; + item.tooltip = isBroken ? this.environment.error : this.environment.tooltip; + // Show warning icon for broken environments + item.iconPath = isBroken ? new ThemeIcon('warning') : this.environment.iconPath; this.treeItem = item; } diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts index 76ce3ad4..54137832 100644 --- a/src/managers/builtin/venvUtils.ts +++ b/src/managers/builtin/venvUtils.ts @@ -128,6 +128,31 @@ function getName(binPath: string): string { } async function getPythonInfo(env: NativeEnvInfo): Promise { + // Handle broken environments that have an error field + if (env.error) { + const venvName = env.name ?? (env.prefix ? path.basename(env.prefix) : 'Unknown'); + const name = `${venvName} (broken)`; + + return { + name: name, + displayName: name, + shortDisplayName: `(${venvName})`, + displayPath: env.prefix ?? env.executable ?? 'Unknown path', + version: env.version ?? 'Unknown', + description: env.error, + tooltip: env.error, + environmentPath: Uri.file(env.prefix ?? env.executable ?? ''), + iconPath: new ThemeIcon('warning'), + sysPrefix: env.prefix ?? '', + execInfo: { + run: { + executable: env.executable ?? '', + }, + }, + error: env.error, + }; + } + if (env.executable && env.version && env.prefix) { const venvName = env.name ?? getName(env.executable); const sv = shortVersion(env.version); @@ -193,6 +218,19 @@ export async function findVirtualEnvironments( ); for (const e of envs) { + // Include environments with errors (broken environments) so users can see and diagnose them + if (e.error) { + log.warn(`Broken venv environment detected: ${e.error} - ${JSON.stringify(e)}`); + try { + const env = api.createPythonEnvironmentItem(await getPythonInfo(e), manager); + collection.push(env); + log.info(`Found broken venv environment: ${env.name}`); + } catch (err) { + log.error(`Failed to create broken environment item: ${err}`); + } + continue; + } + if (!(e.prefix && e.executable && e.version)) { log.warn(`Invalid venv environment: ${JSON.stringify(e)}`); continue; diff --git a/src/managers/common/nativePythonFinder.ts b/src/managers/common/nativePythonFinder.ts index b0534079..9f99bd13 100644 --- a/src/managers/common/nativePythonFinder.ts +++ b/src/managers/common/nativePythonFinder.ts @@ -52,6 +52,11 @@ export interface NativeEnvInfo { project?: string; arch?: 'x64' | 'x86'; symlinks?: string[]; + /** + * Error message if the environment is broken or invalid. + * This is reported by PET when detecting issues like broken symlinks or missing executables. + */ + error?: string; } export interface NativeEnvManagerInfo { From 4d3846939a52fef3deed1e2e11ba7aeec1af7e26 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 5 Feb 2026 14:55:38 -0800 Subject: [PATCH 2/2] feat: enhance handling of broken Python environments in tree view --- package.json | 16 +++++++++++++++- src/features/views/treeViewItems.ts | 5 +++-- src/internal.api.ts | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a793afaa..a5d4e1f8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "publisher": "ms-python", "preview": true, "engines": { - "vscode": "^1.110.0-20260204" + "vscode": "^1.110.0-20260204" }, "categories": [ "Other" @@ -457,6 +457,10 @@ "command": "python-envs.remove", "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*;remove;.*/" }, + { + "command": "python-envs.remove", + "when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*;remove;.*/" + }, { "command": "python-envs.setEnv", "group": "inline", @@ -491,6 +495,16 @@ "group": "inline", "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/ && viewItem =~ /.*copied.*/" }, + { + "command": "python-envs.copyEnvPath", + "group": "inline", + "when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /^((?!copied).)*$/" + }, + { + "command": "python-envs.copyEnvPathCopied", + "group": "inline", + "when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /.*copied.*/" + }, { "command": "python-envs.uninstallPackage", "group": "inline", diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index 8320d57f..ac7e5b1b 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -93,14 +93,15 @@ export class PythonEnvTreeItem implements EnvTreeItem { private getContextValue() { const isBroken = !!this.environment.error; const activatable = !isBroken && isActivatableEnvironment(this.environment) ? 'activatable' : ''; - const broken = isBroken ? 'broken' : ''; let remove = ''; if (this.parent.kind === EnvTreeItemKind.environmentGroup) { remove = this.parent.parent.manager.supportsRemove ? 'remove' : ''; } else if (this.parent.kind === EnvTreeItemKind.manager) { remove = this.parent.manager.supportsRemove ? 'remove' : ''; } - const parts = ['pythonEnvironment', remove, activatable, broken].filter(Boolean); + // Use different base context for broken environments so normal actions don't show + const baseContext = isBroken ? 'pythonBrokenEnvironment' : 'pythonEnvironment'; + const parts = [baseContext, remove, activatable].filter(Boolean); return parts.join(';') + ';'; } } diff --git a/src/internal.api.ts b/src/internal.api.ts index 2573a6ee..2dcd9589 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -338,6 +338,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment { public readonly execInfo: PythonEnvironmentExecutionInfo; public readonly sysPrefix: string; public readonly group?: string | EnvironmentGroupInfo; + public readonly error?: string; constructor( public readonly envId: PythonEnvironmentId, @@ -355,6 +356,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment { this.execInfo = info.execInfo; this.sysPrefix = info.sysPrefix; this.group = info.group; + this.error = info.error; } }