From 5b2966955a257fa1bf5b884fd2a54ae6619650a7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:31:25 +0000 Subject: [PATCH 01/12] Refresh environment managers automatically when expanding tree node (#783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When expanding environment manager nodes in the Environment Managers view, newly created environments (such as those created by `pipenv install pytest` or `conda create -n myenv`) were not appearing until the user manually refreshed the view. This change adds automatic refresh functionality for all environment managers when their tree node is expanded. The implementation: - Calls the existing `manager.refresh(undefined)` method before retrieving environments for any manager type - Affects all environment managers (Pipenv, Conda, Venv, Poetry, etc.) providing consistent behavior - Uses the established refresh mechanism that properly updates collections and fires change events **Before:** 1. Run `pipenv install pytest` or create a new environment with any manager 2. Open Environment Managers view 3. Expand environment manager node 4. New environment is not visible until manual refresh **After:** 1. Run `pipenv install pytest` or create a new environment with any manager 2. Open Environment Managers view 3. Expand environment manager node 4. New environment appears immediately The fix is minimal and targeted, modifying only 1 line of code in the `getChildren` method in `EnvManagerView`. Unit tests have been updated to validate the core logic for any manager type. Fixes #782. --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/features/views/envManagersView.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/features/views/envManagersView.ts b/src/features/views/envManagersView.ts index c418fed7..c6f68a29 100644 --- a/src/features/views/envManagersView.ts +++ b/src/features/views/envManagersView.ts @@ -95,6 +95,10 @@ export class EnvManagerView implements TreeDataProvider, Disposable if (element.kind === EnvTreeItemKind.manager) { const manager = (element as EnvManagerTreeItem).manager; const views: EnvTreeItem[] = []; + + // Refresh manager when expanded to pick up newly created environments + await manager.refresh(undefined); + const envs = await manager.getEnvironments('all'); envs.filter((e) => !e.group).forEach((env) => { const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem, this.selected.get(env.envId.id)); From e2fde2abf618963b564221f9941efb98c68b55d3 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:15:31 -0700 Subject: [PATCH 02/12] revert: remove unnecessary refresh call for environment managers on expansion (#811) reverting https://github.com/microsoft/vscode-python-environments/commit/5b2966955a257fa1bf5b884fd2a54ae6619650a7 as it is not the right way to do it and causes endless refresh looping --- src/features/views/envManagersView.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/features/views/envManagersView.ts b/src/features/views/envManagersView.ts index c6f68a29..c418fed7 100644 --- a/src/features/views/envManagersView.ts +++ b/src/features/views/envManagersView.ts @@ -95,10 +95,6 @@ export class EnvManagerView implements TreeDataProvider, Disposable if (element.kind === EnvTreeItemKind.manager) { const manager = (element as EnvManagerTreeItem).manager; const views: EnvTreeItem[] = []; - - // Refresh manager when expanded to pick up newly created environments - await manager.refresh(undefined); - const envs = await manager.getEnvironments('all'); envs.filter((e) => !e.group).forEach((env) => { const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem, this.selected.get(env.envId.id)); From d4bd75758ef40df2789409451f2bfb95da3fc3c9 Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Mon, 8 Sep 2025 18:42:34 -0300 Subject: [PATCH 03/12] Display activate button when a terminal is moved to the editor window (#764) Fixes https://github.com/microsoft/vscode-python-environments/issues/631 Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/package.json b/package.json index e207b20b..9c7b41e4 100644 --- a/package.json +++ b/package.json @@ -497,6 +497,18 @@ "when": "explorerViewletVisible && resourceExtname == .py" } ], + "editor/title": [ + { + "command": "python-envs.terminal.activate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && !pythonTerminalActivated" + }, + { + "command": "python-envs.terminal.deactivate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && pythonTerminalActivated" + } + ], "editor/title/run": [ { "command": "python-envs.runAsTask", From 8d5f9536839c332fe12b6137431d45728f4dd438 Mon Sep 17 00:00:00 2001 From: Abdelrahman AL MAROUK <72821992+almarouk@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:17:22 +0200 Subject: [PATCH 04/12] fix conda env refresh not waiting for promises (#751) This was causing the refresh function to return with empty or missing environments before waiting for all async calls that update the env collection. This bug was somehow causing duplicate environments to appear in the list of environments. Co-authored-by: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> --- src/managers/conda/condaUtils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index c6cf3672..9d09e34c 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -694,12 +694,14 @@ export async function refreshCondaEnvs( .filter((e) => e.kind === NativePythonEnvironmentKind.conda); const collection: PythonEnvironment[] = []; - envs.forEach(async (e) => { - const environment = await nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); - if (environment) { - collection.push(environment); - } - }); + await Promise.all( + envs.map(async (e) => { + const environment = await nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); + if (environment) { + collection.push(environment); + } + }), + ); return sortEnvironments(collection); } From b141deaddd6e87f7678ac5730168e9039b35a076 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:49:03 -0700 Subject: [PATCH 05/12] debt: prepend '[pet]' to log messages for better context in NativePythonFinder (#814) --- src/managers/common/nativePythonFinder.ts | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/managers/common/nativePythonFinder.ts b/src/managers/common/nativePythonFinder.ts index 7f6fbd84..4a1306af 100644 --- a/src/managers/common/nativePythonFinder.ts +++ b/src/managers/common/nativePythonFinder.ts @@ -202,7 +202,7 @@ class NativePythonFinderImpl implements NativePythonFinder { } private start(): rpc.MessageConnection { - this.outputChannel.info(`Starting Python Locator ${this.toolPath} server`); + this.outputChannel.info(`[pet] Starting Python Locator ${this.toolPath} server`); // jsonrpc package cannot handle messages coming through too quickly. // Lets handle the messages and close the stream only when @@ -213,7 +213,7 @@ class NativePythonFinderImpl implements NativePythonFinder { try { const proc = ch.spawn(this.toolPath, ['server'], { env: process.env }); proc.stdout.pipe(readable, { end: false }); - proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + proc.stderr.on('data', (data) => this.outputChannel.error(`[pet] ${data.toString()}`)); writable.pipe(proc.stdin, { end: false }); disposables.push({ @@ -223,12 +223,12 @@ class NativePythonFinderImpl implements NativePythonFinder { proc.kill(); } } catch (ex) { - this.outputChannel.error('Error disposing finder', ex); + this.outputChannel.error('[pet] Error disposing finder', ex); } }, }); } catch (ex) { - this.outputChannel.error(`Error starting Python Finder ${this.toolPath} server`, ex); + this.outputChannel.error(`[pet] Error starting Python Finder ${this.toolPath} server`, ex); } const connection = rpc.createMessageConnection( new rpc.StreamMessageReader(readable), @@ -241,27 +241,28 @@ class NativePythonFinderImpl implements NativePythonFinder { writable.end(); }), connection.onError((ex) => { - this.outputChannel.error('Connection Error:', ex); + this.outputChannel.error('[pet] Connection Error:', ex); }), connection.onNotification('log', (data: NativeLog) => { + const msg = `[pet] ${data.message}`; switch (data.level) { case 'info': - this.outputChannel.info(data.message); + this.outputChannel.info(msg); break; case 'warning': - this.outputChannel.warn(data.message); + this.outputChannel.warn(msg); break; case 'error': - this.outputChannel.error(data.message); + this.outputChannel.error(msg); break; case 'debug': - this.outputChannel.debug(data.message); + this.outputChannel.debug(msg); break; default: - this.outputChannel.trace(data.message); + this.outputChannel.trace(msg); } }), - connection.onNotification('telemetry', (data) => this.outputChannel.info(`Telemetry: `, data)), + connection.onNotification('telemetry', (data) => this.outputChannel.info('[pet] Telemetry: ', data)), connection.onClose(() => { disposables.forEach((d) => d.dispose()); }), @@ -288,7 +289,9 @@ class NativePythonFinderImpl implements NativePythonFinder { executable: data.executable, }) .then((environment: NativeEnvInfo) => { - this.outputChannel.info(`Resolved ${environment.executable}`); + this.outputChannel.info( + `Resolved environment during PET refresh: ${environment.executable}`, + ); nativeInfo.push(environment); }) .catch((ex) => @@ -307,7 +310,7 @@ class NativePythonFinderImpl implements NativePythonFinder { await this.connection.sendRequest<{ duration: number }>('refresh', refreshOptions); await Promise.all(unresolved); } catch (ex) { - this.outputChannel.error('Error refreshing', ex); + this.outputChannel.error('[pet] Error refreshing', ex); throw ex; } finally { disposables.forEach((d) => d.dispose()); @@ -333,13 +336,15 @@ class NativePythonFinderImpl implements NativePythonFinder { }; // No need to send a configuration request, is there are no changes. if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + this.outputChannel.debug('[pet] configure: No changes detected, skipping configuration update.'); return; } + this.outputChannel.info('[pet] configure: Sending configuration update:', JSON.stringify(options)); try { this.lastConfiguration = options; await this.connection.sendRequest('configure', options); } catch (ex) { - this.outputChannel.error('Configuration error', ex); + this.outputChannel.error('[pet] configure: Configuration error', ex); } } } From 1917aaae347d38715cf63f9b005f7ab4dbdd4572 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:46:58 -0700 Subject: [PATCH 06/12] feat: enable random branch naming for Git (#817) --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a8790c9..5558607c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,6 @@ }, "prettier.tabWidth": 4, "python-envs.defaultEnvManager": "ms-python.python:venv", - "python-envs.pythonProjects": [] + "python-envs.pythonProjects": [], + "git.branchRandomName.enable": true } From 637f1c517f2494cd955778c04f805693498834b1 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:18:39 -0700 Subject: [PATCH 07/12] debt: refactor environment info collection and add helper file (#815) should help with diagnosing bugs to have the following information in logging --- src/extension.ts | 176 ++++---------------------------------------- src/helpers.ts | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 160 deletions(-) create mode 100644 src/helpers.ts diff --git a/src/extension.ts b/src/extension.ts index e7372df8..3506e87b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,8 @@ -import { commands, ExtensionContext, extensions, LogOutputChannel, Terminal, Uri, window, workspace } from 'vscode'; +import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri, window } from 'vscode'; +import { version as extensionVersion } from '../package.json'; import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api'; import { ensureCorrectVersion } from './common/extVersion'; -import { registerLogger, traceError, traceInfo, traceWarn } from './common/logging'; +import { registerLogger, traceError, traceInfo, traceVerbose, traceWarn } from './common/logging'; import { clearPersistentState, setPersistentState } from './common/persistentState'; import { newProjectSelection } from './common/pickers/managers'; import { StopWatch } from './common/stopWatch'; @@ -9,7 +10,6 @@ import { EventNames } from './common/telemetry/constants'; import { sendManagerSelectionTelemetry } from './common/telemetry/helpers'; import { sendTelemetryEvent } from './common/telemetry/sender'; import { createDeferred } from './common/utils/deferred'; -import { normalizePath } from './common/utils/pathUtils'; import { isWindows } from './common/utils/platformUtils'; import { activeTerminal, @@ -61,19 +61,23 @@ import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHand import { TerminalActivationImpl } from './features/terminal/terminalActivationState'; import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector'; import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager'; -import { getAutoActivationType, getEnvironmentForTerminal } from './features/terminal/utils'; +import { getEnvironmentForTerminal } from './features/terminal/utils'; import { EnvManagerView } from './features/views/envManagersView'; import { ProjectView } from './features/views/projectView'; import { PythonStatusBarImpl } from './features/views/pythonStatusBar'; import { updateViewsAndStatus } from './features/views/revealHandler'; import { ProjectItem } from './features/views/treeViewItems'; +import { + collectEnvironmentInfo, + getEnvManagerAndPackageManagerConfigLevels, + resolveDefaultInterpreter, +} from './helpers'; import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; import { createNativePythonFinder, getNativePythonToolsPath, - NativeEnvInfo, NativePythonFinder, } from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; @@ -82,89 +86,6 @@ import { registerPipenvFeatures } from './managers/pipenv/main'; import { registerPoetryFeatures } from './managers/poetry/main'; import { registerPyenvFeatures } from './managers/pyenv/main'; -/** - * Collects relevant Python environment information for issue reporting - */ -async function collectEnvironmentInfo( - context: ExtensionContext, - envManagers: EnvironmentManagers, - projectManager: PythonProjectManager, -): Promise { - const info: string[] = []; - - try { - // Extension version - const extensionVersion = context.extension?.packageJSON?.version || 'unknown'; - info.push(`Extension Version: ${extensionVersion}`); - - // Python extension version - const pythonExtension = extensions.getExtension('ms-python.python'); - const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed'; - info.push(`Python Extension Version: ${pythonVersion}`); - - // Environment managers - const managers = envManagers.managers; - info.push(`\nRegistered Environment Managers (${managers.length}):`); - managers.forEach((manager) => { - info.push(` - ${manager.id} (${manager.displayName})`); - }); - - // Available environments - const allEnvironments: PythonEnvironment[] = []; - for (const manager of managers) { - try { - const envs = await manager.getEnvironments('all'); - allEnvironments.push(...envs); - } catch (err) { - info.push(` Error getting environments from ${manager.id}: ${err}`); - } - } - - info.push(`\nTotal Available Environments: ${allEnvironments.length}`); - if (allEnvironments.length > 0) { - info.push('Environment Details:'); - allEnvironments.slice(0, 10).forEach((env, index) => { - info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`); - }); - if (allEnvironments.length > 10) { - info.push(` ... and ${allEnvironments.length - 10} more environments`); - } - } - - // Python projects - const projects = projectManager.getProjects(); - info.push(`\nPython Projects (${projects.length}):`); - for (let index = 0; index < projects.length; index++) { - const project = projects[index]; - info.push(` ${index + 1}. ${project.uri.fsPath}`); - try { - const env = await envManagers.getEnvironment(project.uri); - if (env) { - info.push(` Environment: ${env.displayName}`); - } - } catch (err) { - info.push(` Error getting environment: ${err}`); - } - } - - // Current settings (non-sensitive) - const config = workspace.getConfiguration('python-envs'); - const pyConfig = workspace.getConfiguration('python'); - info.push('\nExtension Settings:'); - info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`); - info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`); - const pyenvAct = config.get('terminal.autoActivationType', undefined); - const pythonAct = pyConfig.get('terminal.activateEnvironment', undefined); - info.push( - `Auto-activation is "${getAutoActivationType()}". Activation based on first 'py-env.terminal.autoActivationType' setting which is '${pyenvAct}' and 'python.terminal.activateEnvironment' if the first is undefined which is '${pythonAct}'.\n`, - ); - } catch (err) { - info.push(`\nError collecting environment information: ${err}`); - } - - return info.join('\n'); -} - export async function activate(context: ExtensionContext): Promise { const useEnvironmentsExtension = getConfiguration('python').get('useEnvironmentsExtension', true); traceInfo(`Experiment Status: useEnvironmentsExtension setting set to ${useEnvironmentsExtension}`); @@ -183,6 +104,13 @@ export async function activate(context: ExtensionContext): Promise { ); } -/** - * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager' or it is set to venv. - * @param nativeFinder - used to resolve interpreter paths. - * @param envManagers - contains all registered managers. - * @param api - The PythonEnvironmentApi for environment resolution and setting. - */ -async function resolveDefaultInterpreter( - nativeFinder: NativePythonFinder, - envManagers: EnvironmentManagers, - api: PythonEnvironmentApi, -) { - const defaultInterpreterPath = getConfiguration('python').get('defaultInterpreterPath'); - - if (defaultInterpreterPath) { - const config = getConfiguration('python-envs'); - const inspect = config.inspect('defaultEnvManager'); - const userDefinedDefaultManager = - inspect?.workspaceFolderValue !== undefined || - inspect?.workspaceValue !== undefined || - inspect?.globalValue !== undefined; - if (!userDefinedDefaultManager) { - try { - const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath); - if (resolved && resolved.executable) { - if (normalizePath(resolved.executable) === normalizePath(defaultInterpreterPath)) { - // no action required, the path is already correct - return; - } - const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); - traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); - - let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); - if (!findEnvManager) { - findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); - } - if (resolvedEnv) { - const newEnv: PythonEnvironment = { - envId: { - id: resolvedEnv?.envId.id, - managerId: resolvedEnv?.envId.managerId ?? '', - }, - name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - version: resolved.version ?? '', - displayPath: defaultInterpreterPath ?? '', - environmentPath: defaultInterpreterPath ? Uri.file(defaultInterpreterPath) : Uri.file(''), - sysPrefix: resolved.arch ?? '', - execInfo: { - run: { - executable: defaultInterpreterPath ?? '', - }, - }, - }; - if (workspace.workspaceFolders?.[0] && findEnvManager) { - traceInfo( - `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, - ); - await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); - } - } - } else { - traceWarn( - `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${defaultInterpreterPath}`, - ); - } - } catch (err) { - traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); - } - } - } -} - export async function deactivate(context: ExtensionContext) { await disposeAll(context.subscriptions); context.subscriptions.length = 0; // Clear subscriptions to prevent memory leaks diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..5a1e8977 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,188 @@ +import { ExtensionContext, extensions, Uri, workspace } from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi } from './api'; +import { traceError, traceInfo, traceWarn } from './common/logging'; +import { normalizePath } from './common/utils/pathUtils'; +import { getConfiguration } from './common/workspace.apis'; +import { getAutoActivationType } from './features/terminal/utils'; +import { EnvironmentManagers, PythonProjectManager } from './internal.api'; +import { NativeEnvInfo, NativePythonFinder } from './managers/common/nativePythonFinder'; + +/** + * Collects relevant Python environment information for issue reporting + */ +export async function collectEnvironmentInfo( + context: ExtensionContext, + envManagers: EnvironmentManagers, + projectManager: PythonProjectManager, +): Promise { + const info: string[] = []; + + try { + // Extension version + const extensionVersion = context.extension?.packageJSON?.version || 'unknown'; + info.push(`Extension Version: ${extensionVersion}`); + + // Python extension version + const pythonExtension = extensions.getExtension('ms-python.python'); + const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed'; + info.push(`Python Extension Version: ${pythonVersion}`); + + // Environment managers + const managers = envManagers.managers; + info.push(`\nRegistered Environment Managers (${managers.length}):`); + managers.forEach((manager) => { + info.push(` - ${manager.id} (${manager.displayName})`); + }); + + // Available environments + const allEnvironments: PythonEnvironment[] = []; + for (const manager of managers) { + try { + const envs = await manager.getEnvironments('all'); + allEnvironments.push(...envs); + } catch (err) { + info.push(` Error getting environments from ${manager.id}: ${err}`); + } + } + + info.push(`\nTotal Available Environments: ${allEnvironments.length}`); + if (allEnvironments.length > 0) { + info.push('Environment Details:'); + allEnvironments.slice(0, 10).forEach((env, index) => { + info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`); + }); + if (allEnvironments.length > 10) { + info.push(` ... and ${allEnvironments.length - 10} more environments`); + } + } + + // Python projects + const projects = projectManager.getProjects(); + info.push(`\nPython Projects (${projects.length}):`); + for (let index = 0; index < projects.length; index++) { + const project = projects[index]; + info.push(` ${index + 1}. ${project.uri.fsPath}`); + try { + const env = await envManagers.getEnvironment(project.uri); + if (env) { + info.push(` Environment: ${env.displayName}`); + } + } catch (err) { + info.push(` Error getting environment: ${err}`); + } + } + + // Current settings (non-sensitive) + const config = workspace.getConfiguration('python-envs'); + const pyConfig = workspace.getConfiguration('python'); + info.push('\nExtension Settings:'); + info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`); + info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`); + const pyenvAct = config.get('terminal.autoActivationType', undefined); + const pythonAct = pyConfig.get('terminal.activateEnvironment', undefined); + info.push( + `Auto-activation is "${getAutoActivationType()}". Activation based on first 'py-env.terminal.autoActivationType' setting which is '${pyenvAct}' and 'python.terminal.activateEnvironment' if the first is undefined which is '${pythonAct}'.\n`, + ); + } catch (err) { + info.push(`\nError collecting environment information: ${err}`); + } + + return info.join('\n'); +} + +/** + * Logs the values of defaultPackageManager and defaultEnvManager at all configuration levels (workspace folder, workspace, user/global, default). + */ +export function getEnvManagerAndPackageManagerConfigLevels() { + const config = getConfiguration('python-envs'); + const envManagerInspect = config.inspect('defaultEnvManager'); + const pkgManagerInspect = config.inspect('defaultPackageManager'); + + return { + section: 'Python Envs Configuration Levels', + defaultEnvManager: { + workspaceFolderValue: envManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: envManagerInspect?.workspaceValue ?? 'undefined', + globalValue: envManagerInspect?.globalValue ?? 'undefined', + defaultValue: envManagerInspect?.defaultValue ?? 'undefined', + }, + defaultPackageManager: { + workspaceFolderValue: pkgManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: pkgManagerInspect?.workspaceValue ?? 'undefined', + globalValue: pkgManagerInspect?.globalValue ?? 'undefined', + defaultValue: pkgManagerInspect?.defaultValue ?? 'undefined', + }, + }; +} + +/** + * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager' or it is set to venv. + * @param nativeFinder - used to resolve interpreter paths. + * @param envManagers - contains all registered managers. + * @param api - The PythonEnvironmentApi for environment resolution and setting. + */ +export async function resolveDefaultInterpreter( + nativeFinder: NativePythonFinder, + envManagers: EnvironmentManagers, + api: PythonEnvironmentApi, +) { + const defaultInterpreterPath = getConfiguration('python').get('defaultInterpreterPath'); + + if (defaultInterpreterPath) { + const config = getConfiguration('python-envs'); + const inspect = config.inspect('defaultEnvManager'); + const userDefinedDefaultManager = + inspect?.workspaceFolderValue !== undefined || + inspect?.workspaceValue !== undefined || + inspect?.globalValue !== undefined; + if (!userDefinedDefaultManager) { + try { + const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath); + if (resolved && resolved.executable) { + if (normalizePath(resolved.executable) === normalizePath(defaultInterpreterPath)) { + // no action required, the path is already correct + return; + } + const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); + traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); + + let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); + if (!findEnvManager) { + findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); + } + if (resolvedEnv) { + const newEnv: PythonEnvironment = { + envId: { + id: resolvedEnv?.envId.id, + managerId: resolvedEnv?.envId.managerId ?? '', + }, + name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + version: resolved.version ?? '', + displayPath: defaultInterpreterPath ?? '', + environmentPath: defaultInterpreterPath ? Uri.file(defaultInterpreterPath) : Uri.file(''), + sysPrefix: resolved.arch ?? '', + execInfo: { + run: { + executable: defaultInterpreterPath ?? '', + }, + }, + }; + if (workspace.workspaceFolders?.[0] && findEnvManager) { + traceInfo( + `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, + ); + await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); + } + } + } else { + traceWarn( + `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${defaultInterpreterPath}`, + ); + } + } catch (err) { + traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); + } + } + } +} From 93217b7b1b65baf103fdbd93cc95090c3e2ee9be Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:19:09 -0700 Subject: [PATCH 08/12] debt: add GitHub Actions workflow for automatic PR label management (#816) --- .github/workflows/pr-labels.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/pr-labels.yml diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 00000000..0f01dde3 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,24 @@ +name: 'PR labels' +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'labeled' + - 'unlabeled' + - 'synchronize' + +jobs: + add-pr-label: + name: 'Ensure Required Labels' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 + with: + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' From 69ff9d4825b01fb5f10ac451ccb0f376ea7e00b0 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:47:24 -0700 Subject: [PATCH 09/12] update create api doc string to be more explicit (#819) --- src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index f889f10c..c5c81865 100644 --- a/src/api.ts +++ b/src/api.ts @@ -379,7 +379,7 @@ export interface EnvironmentManager { quickCreateConfig?(): QuickCreateConfig | undefined; /** - * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. + * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. If a manager does not support environment creation, do not implement this method; the UI disables "create" options when `this.manager.create === undefined`. * @param scope - The scope within which to create the environment. * @param options - Optional parameters for creating the Python environment. * @returns A promise that resolves to the created Python environment, or undefined if creation failed. From 9e078b49ff02c1a1770dc66c8fa33d91469d0865 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:11:12 -0700 Subject: [PATCH 10/12] sort by version (desc) in conda create picker (#818) --- src/managers/conda/condaUtils.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 9d09e34c..418fb62e 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -31,6 +31,7 @@ import { Common, CondaStrings, PackageManagement, Pickers } from '../../common/l import { traceInfo, traceVerbose } from '../../common/logging'; import { getWorkspacePersistentState } from '../../common/persistentState'; import { pickProject } from '../../common/pickers/projects'; +import { StopWatch } from '../../common/stopWatch'; import { createDeferred } from '../../common/utils/deferred'; import { untildify } from '../../common/utils/pathUtils'; import { isWindows } from '../../common/utils/platformUtils'; @@ -56,7 +57,6 @@ import { Installable } from '../common/types'; import { shortVersion, sortEnvironments } from '../common/utils'; import { CondaEnvManager } from './condaEnvManager'; import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils'; -import { StopWatch } from '../../common/stopWatch'; export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`; export const CONDA_PREFIXES_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PREFIXES`; @@ -744,7 +744,6 @@ function trimVersionToMajorMinor(version: string): string { const match = version.match(/^(\d+\.\d+\.\d+)/); return match ? match[1] : version; } - export async function pickPythonVersion( api: PythonEnvironmentApi, token?: CancellationToken, @@ -757,11 +756,25 @@ export async function pickPythonVersion( .filter(Boolean) .map((v) => trimVersionToMajorMinor(v)), // cut to 3 digits ), - ) - .sort() - .reverse(); - if (!versions) { - versions = ['3.11', '3.10', '3.9', '3.12', '3.13']; + ); + + // Sort versions by major version (descending), ignoring minor/patch for simplicity + const parseMajorMinor = (v: string) => { + const m = v.match(/^(\d+)(?:\.(\d+))?/); + return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + }; + + versions = versions.sort((a, b) => { + const pa = parseMajorMinor(a); + const pb = parseMajorMinor(b); + if (pa.major !== pb.major) { + return pb.major - pa.major; + } // desc by major + return pb.minor - pa.minor; // desc by minor + }); + + if (!versions || versions.length === 0) { + versions = ['3.13', '3.12', '3.11', '3.10', '3.9']; } const items: QuickPickItem[] = versions.map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `$(star-full) Python` : 'Python', From 4e9b58e78f91bbd7e45028fefc140b083dd4eb94 Mon Sep 17 00:00:00 2001 From: Eleanor Boyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:33:08 -0700 Subject: [PATCH 11/12] bug: fix default interpreter resolution with user configuration (#825) --- src/extension.ts | 2 +- src/helpers.ts | 124 +++++++++++++++++++++++++++-------------------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 3506e87b..60381ed5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -498,7 +498,7 @@ export async function activate(context: ExtensionContext): Promise(section: string, key: string): T | undefined { + const config = getConfiguration(section); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; + } + if (inspect.workspaceFolderValue !== undefined) { + return inspect.workspaceFolderValue; + } + if (inspect.workspaceValue !== undefined) { + return inspect.workspaceValue; + } + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; +} + +/** + * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager'. * @param nativeFinder - used to resolve interpreter paths. * @param envManagers - contains all registered managers. * @param api - The PythonEnvironmentApi for environment resolution and setting. @@ -126,63 +148,61 @@ export async function resolveDefaultInterpreter( envManagers: EnvironmentManagers, api: PythonEnvironmentApi, ) { - const defaultInterpreterPath = getConfiguration('python').get('defaultInterpreterPath'); + const userSetdefaultInterpreter = getUserConfiguredSetting('python', 'defaultInterpreterPath'); + const userSetDefaultManager = getUserConfiguredSetting('python-envs', 'defaultEnvManager'); + traceInfo( + `[resolveDefaultInterpreter] User configured defaultInterpreterPath: ${userSetdefaultInterpreter} and defaultEnvManager: ${userSetDefaultManager}`, + ); - if (defaultInterpreterPath) { - const config = getConfiguration('python-envs'); - const inspect = config.inspect('defaultEnvManager'); - const userDefinedDefaultManager = - inspect?.workspaceFolderValue !== undefined || - inspect?.workspaceValue !== undefined || - inspect?.globalValue !== undefined; - if (!userDefinedDefaultManager) { - try { - const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath); - if (resolved && resolved.executable) { - if (normalizePath(resolved.executable) === normalizePath(defaultInterpreterPath)) { - // no action required, the path is already correct - return; - } - const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); - traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); + // Only proceed if the user has explicitly set defaultInterpreterPath but nothing is saved for defaultEnvManager + if (userSetdefaultInterpreter && !userSetDefaultManager) { + try { + const resolved: NativeEnvInfo = await nativeFinder.resolve(userSetdefaultInterpreter); + if (resolved && resolved.executable) { + if (normalizePath(resolved.executable) === normalizePath(userSetdefaultInterpreter)) { + // no action required, the path is already correct + return; + } + const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); + traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); - let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); - if (!findEnvManager) { - findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); - } - if (resolvedEnv) { - const newEnv: PythonEnvironment = { - envId: { - id: resolvedEnv?.envId.id, - managerId: resolvedEnv?.envId.managerId ?? '', - }, - name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - version: resolved.version ?? '', - displayPath: defaultInterpreterPath ?? '', - environmentPath: defaultInterpreterPath ? Uri.file(defaultInterpreterPath) : Uri.file(''), - sysPrefix: resolved.arch ?? '', - execInfo: { - run: { - executable: defaultInterpreterPath ?? '', - }, + let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); + if (!findEnvManager) { + findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); + } + const randomString = Math.random().toString(36).substring(2, 15); + if (resolvedEnv) { + const newEnv: PythonEnvironment = { + envId: { + id: `${userSetdefaultInterpreter}_${randomString}`, + managerId: resolvedEnv?.envId.managerId ?? '', + }, + name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + version: resolved.version ?? '', + displayPath: userSetdefaultInterpreter ?? '', + environmentPath: userSetdefaultInterpreter ? Uri.file(userSetdefaultInterpreter) : Uri.file(''), + sysPrefix: resolved.arch ?? '', + execInfo: { + run: { + executable: userSetdefaultInterpreter ?? '', }, - }; - if (workspace.workspaceFolders?.[0] && findEnvManager) { - traceInfo( - `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, - ); - await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); - } + }, + }; + if (workspace.workspaceFolders?.[0] && findEnvManager) { + traceInfo( + `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, + ); + await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); } - } else { - traceWarn( - `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${defaultInterpreterPath}`, - ); } - } catch (err) { - traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); + } else { + traceWarn( + `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${userSetdefaultInterpreter}`, + ); } + } catch (err) { + traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); } } } From 5a4433e377c0709f2273e820ff2d13a591233225 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:34:28 -0700 Subject: [PATCH 12/12] update version to 1.8.0 in package.json and package-lock.json --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35d8b20f..c4dc2735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-python-envs", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-python-envs", - "version": "1.7.0", + "version": "1.8.0", "dependencies": { "@iarna/toml": "^2.2.5", "@vscode/extension-telemetry": "^0.9.7", diff --git a/package.json b/package.json index e207b20b..76cd18bd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-python-envs", "displayName": "Python Environments", "description": "Provides a unified python environment experience", - "version": "1.7.0", + "version": "1.8.0", "publisher": "ms-python", "preview": true, "engines": {