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
32 changes: 32 additions & 0 deletions .github/instructions/generic.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
applyTo: '**'
---

Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.# Coding Instructions for vscode-python-environments

## Localization

- Localize all user-facing messages using VS Code’s `l10n` API.
- Internal log messages do not require localization.

## Logging

- Use the extension’s logging utilities (`traceLog`, `traceVerbose`) for internal logs.
- Do not use `console.log` or `console.warn` for logging.

## Settings Precedence

- Always consider VS Code settings precedence:
1. Workspace folder
2. Workspace
3. User/global
- Remove or update settings from the highest precedence scope first.

## Error Handling & User Notifications

- Avoid showing the same error message multiple times in a session; track state with a module-level variable.
- Use clear, actionable error messages and offer relevant buttons (e.g., "Open settings", "Close").

## Documentation

- Add clear docstrings to public functions, describing their purpose, parameters, and behavior.
10 changes: 5 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
sysPythonManager.resolve(sysMgr);
await Promise.all([
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
registerPyenvFeatures(nativeFinder, context.subscriptions),
registerPipenvFeatures(nativeFinder, context.subscriptions),
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
shellStartupVarsMgr.initialize(),
]);

Expand Down Expand Up @@ -616,7 +616,7 @@ async function resolveDefaultInterpreter(

if (defaultInterpreterPath) {
const defaultManager = getConfiguration('python-envs').get<string>('defaultEnvManager', 'undefined');
traceInfo(`resolveDefaultInterpreter setting exists; found defaultEnvManager: ${defaultManager}`);
traceInfo(`resolveDefaultInterpreter setting exists; found defaultEnvManager: ${defaultManager}. `);
if (!defaultManager || defaultManager === 'ms-python.python:venv') {
try {
const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath);
Expand Down
23 changes: 20 additions & 3 deletions src/features/settings/settingHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from 'vscode';
import { PythonProject } from '../../api';
import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants';
import { traceError, traceInfo } from '../../common/logging';
import { traceError, traceInfo, traceWarn } from '../../common/logging';
import { getWorkspaceFile, getWorkspaceFolders } from '../../common/workspace.apis';
import { PythonProjectManager, PythonProjectSettings } from '../../internal.api';

Expand All @@ -31,17 +31,34 @@ function getSettings(
return undefined;
}

let DEFAULT_ENV_MANAGER_BROKEN = false;
let hasShownDefaultEnvManagerBrokenWarn = false;

export function setDefaultEnvManagerBroken(broken: boolean) {
DEFAULT_ENV_MANAGER_BROKEN = broken;
}
export function isDefaultEnvManagerBroken(): boolean {
return DEFAULT_ENV_MANAGER_BROKEN;
}

export function getDefaultEnvManagerSetting(wm: PythonProjectManager, scope?: Uri): string {
const config = workspace.getConfiguration('python-envs', scope);
const settings = getSettings(wm, config, scope);
if (settings && settings.envManager.length > 0) {
return settings.envManager;
}

// Only show the warning once per session
if (isDefaultEnvManagerBroken()) {
if (!hasShownDefaultEnvManagerBrokenWarn) {
traceWarn(`Default environment manager is broken, using system default: ${DEFAULT_ENV_MANAGER_ID}`);
hasShownDefaultEnvManagerBrokenWarn = true;
}
return DEFAULT_ENV_MANAGER_ID;
}
const defaultManager = config.get<string>('defaultEnvManager');
if (defaultManager === undefined || defaultManager === null || defaultManager === '') {
traceError('No default environment manager set. Check setting python-envs.defaultEnvManager');
traceInfo(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`);
traceWarn(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`);
return DEFAULT_ENV_MANAGER_ID;
}
return defaultManager;
Expand Down
107 changes: 106 additions & 1 deletion src/managers/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as fs from 'fs-extra';
import path from 'path';
import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api';
import { commands, ConfigurationTarget, l10n, window, workspace } from 'vscode';
import { PythonCommandRunConfiguration, PythonEnvironment, PythonEnvironmentApi } from '../../api';
import { traceLog, traceVerbose } from '../../common/logging';
import { isWindows } from '../../common/utils/platformUtils';
import { ShellConstants } from '../../features/common/shellConstants';
import { getDefaultEnvManagerSetting, setDefaultEnvManagerBroken } from '../../features/settings/settingHelpers';
import { PythonProjectManager } from '../../internal.api';
import { Installable } from './types';

export function noop() {
Expand Down Expand Up @@ -194,3 +198,104 @@ export async function getShellActivationCommands(binDir: string): Promise<{
shellDeactivation,
};
}

// Tracks if the broken defaultEnvManager error message has been shown this session
let hasShownBrokenDefaultEnvManagerError = false;

/**
* Checks if the given managerId is set as the default environment manager for the project.
* If so, marks the default manager as broken, refreshes environments, and shows an error message to the user.
* The error message offers to reset the setting, view the setting, or close.
* The error message is only shown once per session.
*
* @param managerId The environment manager id to check.
* @param projectManager The Python project manager instance.
* @param api The Python environment API instance.
*/
export async function notifyMissingManagerIfDefault(
managerId: string,
projectManager: PythonProjectManager,
api: PythonEnvironmentApi,
) {
const defaultEnvManager = getDefaultEnvManagerSetting(projectManager);
if (defaultEnvManager === managerId) {
if (hasShownBrokenDefaultEnvManagerError) {
return;
}
hasShownBrokenDefaultEnvManagerError = true;
setDefaultEnvManagerBroken(true);
await api.refreshEnvironments(undefined);
window
.showErrorMessage(
l10n.t(
"The default environment manager is set to '{0}', but the {1} executable could not be found.",
defaultEnvManager,
managerId.split(':')[1],
),
l10n.t('Reset setting'),
l10n.t('View setting'),
l10n.t('Close'),
)
.then(async (selection) => {
if (selection === 'Reset setting') {
const result = await removeFirstDefaultEnvManagerSettingDetailed(managerId);
if (!result.found) {
window
.showErrorMessage(
l10n.t(
"Could not find a setting for 'defaultEnvManager' set to '{0}' to reset.",
managerId,
),
l10n.t('Open settings'),
l10n.t('Close'),
)
.then((sel) => {
if (sel === 'Open settings') {
commands.executeCommand(
'workbench.action.openSettings',
'python-envs.defaultEnvManager',
);
}
});
}
}
if (selection === 'View setting') {
commands.executeCommand('workbench.action.openSettings', 'python-envs.defaultEnvManager');
}
});
}
}

/**
* Removes the first occurrence of 'defaultEnvManager' set to managerId, returns where it was removed, and logs the action.
* @param managerId The manager id to match and remove.
* @returns { found: boolean, scope?: string }
*/
export async function removeFirstDefaultEnvManagerSettingDetailed(
managerId: string,
): Promise<{ found: boolean; scope?: string }> {
const config = workspace.getConfiguration('python-envs');
const inspect = config.inspect('defaultEnvManager');

// Workspace folder settings (multi-root)
if (inspect?.workspaceFolderValue !== undefined && inspect.workspaceFolderValue === managerId) {
await config.update('defaultEnvManager', undefined, ConfigurationTarget.WorkspaceFolder);
traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace Folder settings.");
return { found: true, scope: 'Workspace Folder' };
}
// Workspace settings
if (inspect?.workspaceValue !== undefined && inspect.workspaceValue === managerId) {
await config.update('defaultEnvManager', undefined, ConfigurationTarget.Workspace);
traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace settings.");
return { found: true, scope: 'Workspace' };
}
// User/global settings
if (inspect?.globalValue !== undefined && inspect.globalValue === managerId) {
await config.update('defaultEnvManager', undefined, ConfigurationTarget.Global);
traceLog("[python-envs] Removed 'defaultEnvManager' from User/Global settings.");
return { found: true, scope: 'User/Global' };
}
// No matching setting found
traceVerbose(`[python-envs] Could not find 'defaultEnvManager' set to '${managerId}' in any scope.`);
return { found: false };
}
4 changes: 4 additions & 0 deletions src/managers/conda/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Disposable, LogOutputChannel } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { notifyMissingManagerIfDefault } from '../common/utils';
import { CondaEnvManager } from './condaEnvManager';
import { CondaPackageManager } from './condaPackageManager';
import { CondaSourcingStatus, constructCondaSourcingStatus } from './condaSourcingUtils';
Expand All @@ -12,6 +14,7 @@ export async function registerCondaFeatures(
nativeFinder: NativePythonFinder,
disposables: Disposable[],
log: LogOutputChannel,
projectManager: PythonProjectManager,
): Promise<void> {
const api: PythonEnvironmentApi = await getPythonApi();

Expand All @@ -34,5 +37,6 @@ export async function registerCondaFeatures(
);
} catch (ex) {
traceInfo('Conda not found, turning off conda features.', ex);
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
}
}
7 changes: 6 additions & 1 deletion src/managers/pipenv/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { Disposable } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { PipenvManager } from './pipenvManager';
import { getPipenv } from './pipenvUtils';

import { notifyMissingManagerIfDefault } from '../common/utils';

export async function registerPipenvFeatures(
nativeFinder: NativePythonFinder,
disposables: Disposable[],
projectManager: PythonProjectManager,
): Promise<void> {
const api: PythonEnvironmentApi = await getPythonApi();

Expand All @@ -17,12 +21,13 @@ export async function registerPipenvFeatures(

if (pipenv) {
const mgr = new PipenvManager(nativeFinder, api);

disposables.push(mgr, api.registerEnvironmentManager(mgr));
} else {
traceInfo('Pipenv not found, turning off pipenv features.');
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
}
} catch (ex) {
traceInfo('Pipenv not found, turning off pipenv features.', ex);
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
}
}
7 changes: 7 additions & 0 deletions src/managers/poetry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Disposable, LogOutputChannel } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { notifyMissingManagerIfDefault } from '../common/utils';
import { PoetryManager } from './poetryManager';
import { PoetryPackageManager } from './poetryPackageManager';
import { getPoetry, getPoetryVersion } from './poetryUtils';
Expand All @@ -11,6 +13,7 @@ export async function registerPoetryFeatures(
nativeFinder: NativePythonFinder,
disposables: Disposable[],
outputChannel: LogOutputChannel,
projectManager: PythonProjectManager,
): Promise<void> {
const api: PythonEnvironmentApi = await getPythonApi();

Expand All @@ -31,8 +34,12 @@ export async function registerPoetryFeatures(
api.registerEnvironmentManager(envManager),
api.registerPackageManager(pkgManager),
);
} else {
traceInfo('Poetry not found, turning off poetry features.');
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
}
} catch (ex) {
traceInfo('Poetry not found, turning off poetry features.', ex);
await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api);
}
}
7 changes: 6 additions & 1 deletion src/managers/pyenv/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,31 @@ import { Disposable } from 'vscode';
import { PythonEnvironmentApi } from '../../api';
import { traceInfo } from '../../common/logging';
import { getPythonApi } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { notifyMissingManagerIfDefault } from '../common/utils';
import { PyEnvManager } from './pyenvManager';
import { getPyenv } from './pyenvUtils';

export async function registerPyenvFeatures(
nativeFinder: NativePythonFinder,
disposables: Disposable[],
projectManager: PythonProjectManager,
): Promise<void> {
const api: PythonEnvironmentApi = await getPythonApi();

try {
const pyenv = await getPyenv(nativeFinder);

if (pyenv) {
const mgr = new PyEnvManager(nativeFinder, api);
disposables.push(mgr, api.registerEnvironmentManager(mgr));
} else {
traceInfo('Pyenv not found, turning off pyenv features.');
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
}
} catch (ex) {
traceInfo('Pyenv not found, turning off pyenv features.', ex);
await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api);
}
}
Loading