Skip to content

Commit 09e98ba

Browse files
authored
Merge branch 'main' into fixCondaEnvResfresh
2 parents 83de4ed + e2fde2a commit 09e98ba

12 files changed

Lines changed: 240 additions & 39 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
applyTo: '**'
3+
---
4+
5+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.# Coding Instructions for vscode-python-environments
6+
7+
## Localization
8+
9+
- Localize all user-facing messages using VS Code’s `l10n` API.
10+
- Internal log messages do not require localization.
11+
12+
## Logging
13+
14+
- Use the extension’s logging utilities (`traceLog`, `traceVerbose`) for internal logs.
15+
- Do not use `console.log` or `console.warn` for logging.
16+
17+
## Settings Precedence
18+
19+
- Always consider VS Code settings precedence:
20+
1. Workspace folder
21+
2. Workspace
22+
3. User/global
23+
- Remove or update settings from the highest precedence scope first.
24+
25+
## Error Handling & User Notifications
26+
27+
- Avoid showing the same error message multiple times in a session; track state with a module-level variable.
28+
- Use clear, actionable error messages and offer relevant buttons (e.g., "Open settings", "Close").
29+
30+
## Documentation
31+
32+
- Add clear docstrings to public functions, describing their purpose, parameters, and behavior.

.github/workflows/issue-labels.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Issue labels
2+
3+
on:
4+
issues:
5+
types: [opened, reopened]
6+
7+
env:
8+
TRIAGERS: '["karthiknadig","eleanorjboyd"]'
9+
10+
permissions:
11+
issues: write
12+
13+
jobs:
14+
# From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue.
15+
add-classify-label:
16+
name: "Add 'triage-needed' and remove assignees"
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout Actions
20+
uses: actions/checkout@v4
21+
with:
22+
repository: 'microsoft/vscode-github-triage-actions'
23+
ref: stable
24+
path: ./actions
25+
26+
- name: Install Actions
27+
run: npm install --production --prefix ./actions
28+
29+
- name: "Add 'triage-needed' and remove assignees"
30+
uses: ./actions/python-issue-labels
31+
with:
32+
triagers: ${{ env.TRIAGERS }}
33+
token: ${{secrets.GITHUB_TOKEN}}

src/extension.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -563,10 +563,10 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
563563
sysPythonManager.resolve(sysMgr);
564564
await Promise.all([
565565
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
566-
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
567-
registerPyenvFeatures(nativeFinder, context.subscriptions),
568-
registerPipenvFeatures(nativeFinder, context.subscriptions),
569-
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel),
566+
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
567+
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
568+
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
569+
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
570570
shellStartupVarsMgr.initialize(),
571571
]);
572572

@@ -596,15 +596,9 @@ export async function disposeAll(disposables: IDisposable[]): Promise<void> {
596596
}
597597

598598
/**
599-
* Resolves and sets the default Python interpreter for the workspace based on the
600-
* 'python.defaultInterpreterPath' setting and the selected environment manager.
601-
* If the setting is present and no default environment manager is set (or is venv),
602-
* attempts to resolve the interpreter path using the native finder. If the resolved
603-
* path differs from the configured path, then creates and sets a PythonEnvironment
604-
* object for the workspace.
605-
*
606-
* @param nativeFinder - The NativePythonFinder instance used to resolve interpreter paths.
607-
* @param envManagers - The EnvironmentManagers instance containing all registered managers.
599+
* Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager' or it is set to venv.
600+
* @param nativeFinder - used to resolve interpreter paths.
601+
* @param envManagers - contains all registered managers.
608602
* @param api - The PythonEnvironmentApi for environment resolution and setting.
609603
*/
610604
async function resolveDefaultInterpreter(
@@ -615,9 +609,13 @@ async function resolveDefaultInterpreter(
615609
const defaultInterpreterPath = getConfiguration('python').get<string>('defaultInterpreterPath');
616610

617611
if (defaultInterpreterPath) {
618-
const defaultManager = getConfiguration('python-envs').get<string>('defaultEnvManager', 'undefined');
619-
traceInfo(`resolveDefaultInterpreter setting exists; found defaultEnvManager: ${defaultManager}`);
620-
if (!defaultManager || defaultManager === 'ms-python.python:venv') {
612+
const config = getConfiguration('python-envs');
613+
const inspect = config.inspect<string>('defaultEnvManager');
614+
const userDefinedDefaultManager =
615+
inspect?.workspaceFolderValue !== undefined ||
616+
inspect?.workspaceValue !== undefined ||
617+
inspect?.globalValue !== undefined;
618+
if (!userDefinedDefaultManager) {
621619
try {
622620
const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath);
623621
if (resolved && resolved.executable) {

src/features/settings/settingHelpers.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from 'vscode';
1010
import { PythonProject } from '../../api';
1111
import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants';
12-
import { traceError, traceInfo } from '../../common/logging';
12+
import { traceError, traceInfo, traceWarn } from '../../common/logging';
1313
import { getWorkspaceFile, getWorkspaceFolders } from '../../common/workspace.apis';
1414
import { PythonProjectManager, PythonProjectSettings } from '../../internal.api';
1515

@@ -31,17 +31,34 @@ function getSettings(
3131
return undefined;
3232
}
3333

34+
let DEFAULT_ENV_MANAGER_BROKEN = false;
35+
let hasShownDefaultEnvManagerBrokenWarn = false;
36+
37+
export function setDefaultEnvManagerBroken(broken: boolean) {
38+
DEFAULT_ENV_MANAGER_BROKEN = broken;
39+
}
40+
export function isDefaultEnvManagerBroken(): boolean {
41+
return DEFAULT_ENV_MANAGER_BROKEN;
42+
}
43+
3444
export function getDefaultEnvManagerSetting(wm: PythonProjectManager, scope?: Uri): string {
3545
const config = workspace.getConfiguration('python-envs', scope);
3646
const settings = getSettings(wm, config, scope);
3747
if (settings && settings.envManager.length > 0) {
3848
return settings.envManager;
3949
}
40-
50+
// Only show the warning once per session
51+
if (isDefaultEnvManagerBroken()) {
52+
if (!hasShownDefaultEnvManagerBrokenWarn) {
53+
traceWarn(`Default environment manager is broken, using system default: ${DEFAULT_ENV_MANAGER_ID}`);
54+
hasShownDefaultEnvManagerBrokenWarn = true;
55+
}
56+
return DEFAULT_ENV_MANAGER_ID;
57+
}
4158
const defaultManager = config.get<string>('defaultEnvManager');
4259
if (defaultManager === undefined || defaultManager === null || defaultManager === '') {
4360
traceError('No default environment manager set. Check setting python-envs.defaultEnvManager');
44-
traceInfo(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`);
61+
traceWarn(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`);
4562
return DEFAULT_ENV_MANAGER_ID;
4663
}
4764
return defaultManager;

src/managers/builtin/venvUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export async function findVirtualEnvironments(
180180

181181
for (const e of envs) {
182182
if (!(e.prefix && e.executable && e.version)) {
183-
log.warn(`Invalid conda environment: ${JSON.stringify(e)}`);
183+
log.warn(`Invalid venv environment: ${JSON.stringify(e)}`);
184184
continue;
185185
}
186186

src/managers/common/nativePythonFinder.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { PythonProjectApi } from '../../api';
88
import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants';
99
import { getExtension } from '../../common/extension.apis';
1010
import { traceVerbose } from '../../common/logging';
11-
import { getUserHomeDir, untildify } from '../../common/utils/pathUtils';
11+
import { untildify } from '../../common/utils/pathUtils';
1212
import { isWindows } from '../../common/utils/platformUtils';
1313
import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool';
1414
import { getConfiguration } from '../../common/workspace.apis';
@@ -184,14 +184,20 @@ class NativePythonFinderImpl implements NativePythonFinder {
184184
}
185185

186186
private getRefreshOptions(options?: NativePythonEnvironmentKind | Uri[]): RefreshOptions | undefined {
187+
// settings on where else to search
188+
const venvFolders = getPythonSettingAndUntildify<string[]>('venvFolders') ?? [];
187189
if (options) {
188190
if (typeof options === 'string') {
191+
// kind
189192
return { searchKind: options };
190193
}
191194
if (Array.isArray(options)) {
192-
return { searchPaths: options.map((item) => item.fsPath) };
195+
const uriSearchPaths = options.map((item) => item.fsPath);
196+
uriSearchPaths.push(...venvFolders);
197+
return { searchPaths: uriSearchPaths };
193198
}
194199
}
200+
// return undefined to use configured defaults (for nativeFinder refresh)
195201
return undefined;
196202
}
197203

@@ -355,10 +361,9 @@ function getCustomVirtualEnvDirs(): string[] {
355361
venvDirs.push(untildify(venvPath));
356362
}
357363
const venvFolders = getPythonSettingAndUntildify<string[]>('venvFolders') ?? [];
358-
const homeDir = getUserHomeDir();
359-
if (homeDir) {
360-
venvFolders.map((item) => path.join(homeDir, item)).forEach((d) => venvDirs.push(d));
361-
}
364+
venvFolders.forEach((item) => {
365+
venvDirs.push(item);
366+
});
362367
return Array.from(new Set(venvDirs));
363368
}
364369

src/managers/common/utils.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import * as fs from 'fs-extra';
22
import path from 'path';
3-
import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api';
3+
import { commands, ConfigurationTarget, l10n, window, workspace } from 'vscode';
4+
import { PythonCommandRunConfiguration, PythonEnvironment, PythonEnvironmentApi } from '../../api';
5+
import { traceLog, traceVerbose } from '../../common/logging';
46
import { isWindows } from '../../common/utils/platformUtils';
57
import { ShellConstants } from '../../features/common/shellConstants';
8+
import { getDefaultEnvManagerSetting, setDefaultEnvManagerBroken } from '../../features/settings/settingHelpers';
9+
import { PythonProjectManager } from '../../internal.api';
610
import { Installable } from './types';
711

812
export function noop() {
@@ -194,3 +198,104 @@ export async function getShellActivationCommands(binDir: string): Promise<{
194198
shellDeactivation,
195199
};
196200
}
201+
202+
// Tracks if the broken defaultEnvManager error message has been shown this session
203+
let hasShownBrokenDefaultEnvManagerError = false;
204+
205+
/**
206+
* Checks if the given managerId is set as the default environment manager for the project.
207+
* If so, marks the default manager as broken, refreshes environments, and shows an error message to the user.
208+
* The error message offers to reset the setting, view the setting, or close.
209+
* The error message is only shown once per session.
210+
*
211+
* @param managerId The environment manager id to check.
212+
* @param projectManager The Python project manager instance.
213+
* @param api The Python environment API instance.
214+
*/
215+
export async function notifyMissingManagerIfDefault(
216+
managerId: string,
217+
projectManager: PythonProjectManager,
218+
api: PythonEnvironmentApi,
219+
) {
220+
const defaultEnvManager = getDefaultEnvManagerSetting(projectManager);
221+
if (defaultEnvManager === managerId) {
222+
if (hasShownBrokenDefaultEnvManagerError) {
223+
return;
224+
}
225+
hasShownBrokenDefaultEnvManagerError = true;
226+
setDefaultEnvManagerBroken(true);
227+
await api.refreshEnvironments(undefined);
228+
window
229+
.showErrorMessage(
230+
l10n.t(
231+
"The default environment manager is set to '{0}', but the {1} executable could not be found.",
232+
defaultEnvManager,
233+
managerId.split(':')[1],
234+
),
235+
l10n.t('Reset setting'),
236+
l10n.t('View setting'),
237+
l10n.t('Close'),
238+
)
239+
.then(async (selection) => {
240+
if (selection === 'Reset setting') {
241+
const result = await removeFirstDefaultEnvManagerSettingDetailed(managerId);
242+
if (!result.found) {
243+
window
244+
.showErrorMessage(
245+
l10n.t(
246+
"Could not find a setting for 'defaultEnvManager' set to '{0}' to reset.",
247+
managerId,
248+
),
249+
l10n.t('Open settings'),
250+
l10n.t('Close'),
251+
)
252+
.then((sel) => {
253+
if (sel === 'Open settings') {
254+
commands.executeCommand(
255+
'workbench.action.openSettings',
256+
'python-envs.defaultEnvManager',
257+
);
258+
}
259+
});
260+
}
261+
}
262+
if (selection === 'View setting') {
263+
commands.executeCommand('workbench.action.openSettings', 'python-envs.defaultEnvManager');
264+
}
265+
});
266+
}
267+
}
268+
269+
/**
270+
* Removes the first occurrence of 'defaultEnvManager' set to managerId, returns where it was removed, and logs the action.
271+
* @param managerId The manager id to match and remove.
272+
* @returns { found: boolean, scope?: string }
273+
*/
274+
export async function removeFirstDefaultEnvManagerSettingDetailed(
275+
managerId: string,
276+
): Promise<{ found: boolean; scope?: string }> {
277+
const config = workspace.getConfiguration('python-envs');
278+
const inspect = config.inspect('defaultEnvManager');
279+
280+
// Workspace folder settings (multi-root)
281+
if (inspect?.workspaceFolderValue !== undefined && inspect.workspaceFolderValue === managerId) {
282+
await config.update('defaultEnvManager', undefined, ConfigurationTarget.WorkspaceFolder);
283+
traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace Folder settings.");
284+
return { found: true, scope: 'Workspace Folder' };
285+
}
286+
// Workspace settings
287+
if (inspect?.workspaceValue !== undefined && inspect.workspaceValue === managerId) {
288+
await config.update('defaultEnvManager', undefined, ConfigurationTarget.Workspace);
289+
traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace settings.");
290+
return { found: true, scope: 'Workspace' };
291+
}
292+
// User/global settings
293+
if (inspect?.globalValue !== undefined && inspect.globalValue === managerId) {
294+
await config.update('defaultEnvManager', undefined, ConfigurationTarget.Global);
295+
traceLog("[python-envs] Removed 'defaultEnvManager' from User/Global settings.");
296+
return { found: true, scope: 'User/Global' };
297+
}
298+
// No matching setting found
299+
traceVerbose(`[python-envs] Could not find 'defaultEnvManager' set to '${managerId}' in any scope.`);
300+
return { found: false };
301+
}

src/managers/conda/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { Disposable, LogOutputChannel } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
44
import { getPythonApi } from '../../features/pythonApi';
5+
import { PythonProjectManager } from '../../internal.api';
56
import { NativePythonFinder } from '../common/nativePythonFinder';
7+
import { notifyMissingManagerIfDefault } from '../common/utils';
68
import { CondaEnvManager } from './condaEnvManager';
79
import { CondaPackageManager } from './condaPackageManager';
810
import { CondaSourcingStatus, constructCondaSourcingStatus } from './condaSourcingUtils';
@@ -12,6 +14,7 @@ export async function registerCondaFeatures(
1214
nativeFinder: NativePythonFinder,
1315
disposables: Disposable[],
1416
log: LogOutputChannel,
17+
projectManager: PythonProjectManager,
1518
): Promise<void> {
1619
const api: PythonEnvironmentApi = await getPythonApi();
1720

@@ -34,5 +37,6 @@ export async function registerCondaFeatures(
3437
);
3538
} catch (ex) {
3639
traceInfo('Conda not found, turning off conda features.', ex);
40+
await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api);
3741
}
3842
}

src/managers/pipenv/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { Disposable } from 'vscode';
22
import { PythonEnvironmentApi } from '../../api';
33
import { traceInfo } from '../../common/logging';
44
import { getPythonApi } from '../../features/pythonApi';
5+
import { PythonProjectManager } from '../../internal.api';
56
import { NativePythonFinder } from '../common/nativePythonFinder';
67
import { PipenvManager } from './pipenvManager';
78
import { getPipenv } from './pipenvUtils';
89

10+
import { notifyMissingManagerIfDefault } from '../common/utils';
11+
912
export async function registerPipenvFeatures(
1013
nativeFinder: NativePythonFinder,
1114
disposables: Disposable[],
15+
projectManager: PythonProjectManager,
1216
): Promise<void> {
1317
const api: PythonEnvironmentApi = await getPythonApi();
1418

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

1822
if (pipenv) {
1923
const mgr = new PipenvManager(nativeFinder, api);
20-
2124
disposables.push(mgr, api.registerEnvironmentManager(mgr));
2225
} else {
2326
traceInfo('Pipenv not found, turning off pipenv features.');
27+
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
2428
}
2529
} catch (ex) {
2630
traceInfo('Pipenv not found, turning off pipenv features.', ex);
31+
await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api);
2732
}
2833
}

0 commit comments

Comments
 (0)