Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"publisher": "ms-python",
"preview": true,
"engines": {
"vscode": "^1.110.0-20260204"
"vscode": "^1.110.0-20260204"
},
"categories": [
"Other"
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 14 additions & 7 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -922,7 +929,8 @@ export interface PythonProjectEnvironmentApi {
}

export interface PythonEnvironmentManagerApi
extends PythonEnvironmentManagerRegistrationApi,
extends
PythonEnvironmentManagerRegistrationApi,
PythonEnvironmentItemApi,
PythonEnvironmentManagementApi,
PythonEnvironmentsApi,
Expand Down Expand Up @@ -987,7 +995,8 @@ export interface PythonPackageManagementApi {
}

export interface PythonPackageManagerApi
extends PythonPackageManagerRegistrationApi,
extends
PythonPackageManagerRegistrationApi,
PythonPackageGetterApi,
PythonPackageManagementApi,
PythonPackageItemApi {}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 27 additions & 11 deletions src/features/views/treeViewItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};`;
Expand All @@ -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;
Expand All @@ -77,21 +82,26 @@ 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;
}
Comment on lines 75 to 91
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error handling logic in PythonEnvTreeItem and ProjectEnvironment for displaying broken environments lacks test coverage. The existing test suite (treeViewItems.unit.test.ts) tests context values, labels, and other properties for normal environments, but doesn't cover broken environments. Tests should be added to verify:

  1. When an environment has an error field, the context value is pythonBrokenEnvironment instead of pythonEnvironment
  2. The description and tooltip show the error message
  3. The icon is a warning icon
  4. Activatable is not set for broken environments

Copilot uses AI. Check for mistakes.

private getContextValue() {
const activatable = isActivatableEnvironment(this.environment) ? 'activatable' : '';
const isBroken = !!this.environment.error;
const activatable = !isBroken && isActivatableEnvironment(this.environment) ? 'activatable' : '';
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);
// 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(';') + ';';
}
}
Expand Down Expand Up @@ -240,16 +250,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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/internal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
}

Expand Down
38 changes: 38 additions & 0 deletions src/managers/builtin/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,31 @@ function getName(binPath: string): string {
}

async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo> {
// 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)`;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user-facing string "(broken)" should be localized using the l10n API. According to the coding guidelines, all user-facing messages must be localized. Consider adding something like l10n.t('broken') in the localize.ts file and using it here instead of the hardcoded "(broken)" string. Also ensure "(broken)" is properly formatted with parentheses for localization (e.g., l10n.t('{0} (broken)', venvName) to handle languages that might format this differently).

Copilot generated this review using guidance from repository custom instructions.

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 ?? ''),
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical cross-platform path handling issue: When env.prefix and env.executable are both undefined/empty, Uri.file('') is called which will fail on Windows. According to the coding guidelines, this extension must work on Windows, macOS, and Linux.

For broken environments, at least one of env.prefix, env.executable, or a fallback should be guaranteed to exist. If truly none are available, the environment should not be created or a safe fallback path should be used instead of an empty string.

Copilot uses AI. Check for mistakes.
iconPath: new ThemeIcon('warning'),
sysPrefix: env.prefix ?? '',
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sysPrefix field is being set to env.prefix ?? '', which means a broken environment could have an empty string as its sysPrefix. This could lead to issues if other code expects sysPrefix to be a valid directory path. Consider whether broken environments should set this to a valid path or if downstream code should handle empty sysPrefix appropriately.

Suggested change
sysPrefix: env.prefix ?? '',
sysPrefix: env.prefix ?? (env.executable ? path.dirname(env.executable) : ''),

Copilot uses AI. Check for mistakes.
execInfo: {
run: {
executable: env.executable ?? '',
},
},
error: env.error,
};
}
Comment on lines +131 to +154
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error handling logic for broken environments in getPythonInfo() and findVirtualEnvironments() lacks test coverage. The codebase has comprehensive test coverage for other parts of venvUtils (see venvUtils.removeVenv.unit.test.ts and venvUtils.uvTracking.unit.test.ts), so tests should be added for:

  1. getPythonInfo() with an environment that has an error field set
  2. findVirtualEnvironments() encountering a broken environment
  3. Verifying the returned broken environment has the correct properties (warning icon, error message in description/tooltip, etc.)

Copilot uses AI. Check for mistakes.

if (env.executable && env.version && env.prefix) {
const venvName = env.name ?? getName(env.executable);
const sv = shortVersion(env.version);
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/managers/common/nativePythonFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading