From ece76556dc9a0efec1fed91f3843c81ebad0ccf8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 7 May 2026 13:28:43 -0700 Subject: [PATCH 01/15] feature: Manage package versions from sidebar --- package-lock.json | 20 ++++++++++++++++++-- package.json | 16 ++++++++++++++++ package.nls.json | 1 + src/extension.ts | 4 ++++ src/features/envCommands.ts | 25 +++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 963f4761..b3ac4765 100644 --- a/package-lock.json +++ b/package-lock.json @@ -792,6 +792,7 @@ "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -1498,6 +1499,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1771,6 +1773,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2509,6 +2512,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4850,6 +4854,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5523,6 +5528,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5649,6 +5655,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -5697,6 +5704,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -6502,6 +6510,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.16.0", "@typescript-eslint/types": "8.16.0", @@ -7009,7 +7018,8 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-phases": { "version": "1.0.4", @@ -7196,6 +7206,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, + "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7706,6 +7717,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9387,6 +9399,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9843,7 +9856,8 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true + "dev": true, + "peer": true }, "uc.micro": { "version": "1.0.6", @@ -9929,6 +9943,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -9962,6 +9977,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", diff --git a/package.json b/package.json index 2e575183..b7876f61 100644 --- a/package.json +++ b/package.json @@ -279,6 +279,12 @@ "category": "Python Envs", "icon": "$(trash)" }, + { + "command": "python-envs.managePackageVersion", + "title": "%python-envs.managePackageVersion.title%", + "category": "Python Envs", + "icon": "$(gear)" + }, { "command": "python-envs.copyEnvPath", "title": "%python-envs.copyEnvPath.title%", @@ -511,6 +517,11 @@ "group": "inline", "when": "view == env-managers && viewItem =~ /.*pythonBrokenEnvironment.*/ && viewItem =~ /.*copied.*/" }, + { + "command": "python-envs.managePackageVersion", + "group": "inline", + "when": "view == env-managers && viewItem == python-package" + }, { "command": "python-envs.uninstallPackage", "group": "inline", @@ -553,6 +564,11 @@ "command": "python-envs.revealProjectInExplorer", "when": "view == python-projects && viewItem =~ /.*python-workspace.*/" }, + { + "command": "python-envs.managePackageVersion", + "group": "inline", + "when": "view == python-projects && viewItem == python-package" + }, { "command": "python-envs.uninstallPackage", "group": "inline", diff --git a/package.nls.json b/package.nls.json index 3a4ddcec..c172d371 100644 --- a/package.nls.json +++ b/package.nls.json @@ -45,5 +45,6 @@ "python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer", "python-envs.revealEnvInManagerView.title": "Reveal in Environment Managers View", "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal...", + "python-envs.managePackageVersion.title": "Manage Package Version", "python-envs.alwaysUseUv.description": "When set to true, uv will be used to manage all virtual environments if available. When set to false, uv will only manage virtual environments explicitly created by uv." } diff --git a/src/extension.ts b/src/extension.ts index ec8a24cd..0c3f6cb1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -50,6 +50,7 @@ import { createTerminalCommand, getPackageCommandOptions, handlePackageUninstall, + handlePackageVersionManagement, refreshPackagesCommand, removeEnvironmentCommand, removePythonProject, @@ -297,6 +298,9 @@ export async function activate(context: ExtensionContext): Promise { await handlePackageUninstall(context, envManagers); }), + commands.registerCommand('python-envs.managePackageVersion', async (context: unknown) => { + await handlePackageVersionManagement(context, envManagers); + }), commands.registerCommand('python-envs.set', async (item) => { await setEnvironmentCommand(item, envManagers, projectManager); }), diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index a10f8885..73083ba7 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -46,6 +46,7 @@ import { activeTextEditor, showErrorMessage, showInformationMessage, + showInputBox, showOpenDialog, withProgress, } from '../common/window.apis'; @@ -314,6 +315,30 @@ export async function handlePackageUninstall(context: unknown, em: EnvironmentMa traceError(`Invalid context for uninstall command: ${typeof context}`); } +export async function handlePackageVersionManagement(context: unknown, em: EnvironmentManagers) { + if (context instanceof PackageTreeItem || context instanceof ProjectPackage) { + const pkg = context.pkg; + const environment = context instanceof ProjectPackage ? context.parent.environment : context.parent.environment; + const packageManager = em.getPackageManager(environment); + + const version = await showInputBox({ + title: l10n.t('Manage Package Version'), + prompt: l10n.t('Enter the version for {0}', pkg.name), + value: pkg.version, + placeHolder: l10n.t('e.g. 1.2.3'), + }); + + if (version === undefined) { + return; + } + + await packageManager?.manage(environment, { + install: [`${pkg.name}==${version}`], + uninstall: [], + }); + } +} + export async function setEnvironmentCommand( context: unknown, em: EnvironmentManagers, From cc015f93b00babf00e1b24b34e1db43e88c3e9aa Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 7 May 2026 19:53:53 -0700 Subject: [PATCH 02/15] validate version pep440 --- src/features/envCommands.ts | 10 ++++++++++ src/managers/builtin/pipUtils.ts | 17 ++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 73083ba7..bad96e18 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -50,6 +50,7 @@ import { showOpenDialog, withProgress, } from '../common/window.apis'; +import { PEP440_VERSION_REGEX } from '../managers/builtin/pipUtils'; import { runAsTask } from './execution/runAsTask'; import { runInTerminal } from './terminal/runInTerminal'; import { TerminalManager } from './terminal/terminalManager'; @@ -326,6 +327,15 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir prompt: l10n.t('Enter the version for {0}', pkg.name), value: pkg.version, placeHolder: l10n.t('e.g. 1.2.3'), + validateInput: (value) => { + if (value.length === 0) { + return l10n.t('Version cannot be empty'); + } + if (!PEP440_VERSION_REGEX.test(value)) { + return l10n.t('Invalid PEP 440 version: {0}', value); + } + return undefined; + }, }); if (version === undefined) { diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 2e7a7f85..71621e84 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -22,6 +22,15 @@ export interface PyprojectToml { requires?: unknown; }; } +/** + * PEP 440 version regex. Versions must follow PEP 440 format (e.g., "1.0.0", "2.1a3"). + * See https://peps.python.org/pep-0440/ + * This regex is adapted from the official python 'packaging' library: + * https://github.com/pypa/packaging/blob/main/src/packaging/version.py + */ +export const PEP440_VERSION_REGEX = + /^v?([0-9]+!)?([0-9]+(?:\.[0-9]+)*)(?:[-_.]?(a|b|c|rc|alpha|beta|pre|preview)[-_.]?([0-9]+)?)?(?:(?:-([0-9]+))|(?:[-_.]?(post|rev|r)[-_.]?([0-9]+)?))?(?:[-_.]?(dev)[-_.]?([0-9]+)?)?(?:\+([a-z0-9]+(?:[-_.][a-z0-9]+)*))?$/i; + export function validatePyprojectToml(toml: PyprojectToml): string | undefined { // 1. Validate required "requires" field in [build-system] section (PEP 518) const buildSystem = toml['build-system']; @@ -56,13 +65,7 @@ export function validatePyprojectToml(toml: PyprojectToml): string | undefined { if (version.length === 0) { return l10n.t('Version cannot be empty in pyproject.toml.'); } - // PEP 440 version regex. Versions must follow PEP 440 format (e.g., "1.0.0", "2.1a3"). - // See https://peps.python.org/pep-0440/ - // This regex is adapted from the official python 'packaging' library: - // https://github.com/pypa/packaging/blob/main/src/packaging/version.py - const versionRegex = - /^v?([0-9]+!)?([0-9]+(?:\.[0-9]+)*)(?:[-_.]?(a|b|c|rc|alpha|beta|pre|preview)[-_.]?([0-9]+)?)?(?:(?:-([0-9]+))|(?:[-_.]?(post|rev|r)[-_.]?([0-9]+)?))?(?:[-_.]?(dev)[-_.]?([0-9]+)?)?(?:\+([a-z0-9]+(?:[-_.][a-z0-9]+)*))?$/i; - if (!versionRegex.test(version)) { + if (!PEP440_VERSION_REGEX.test(version)) { return l10n.t('Invalid version "{0}" in pyproject.toml.', version); } } From d2d74f82bbb9164d3611e0aec886c35cce236d75 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 7 May 2026 19:55:24 -0700 Subject: [PATCH 03/15] Version guards --- src/features/envCommands.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index bad96e18..56f9e769 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -308,7 +308,7 @@ export async function removeEnvironmentCommand(context: unknown, managers: Envir export async function handlePackageUninstall(context: unknown, em: EnvironmentManagers) { if (context instanceof PackageTreeItem || context instanceof ProjectPackage) { const moduleName = context.pkg.name; - const environment = context instanceof ProjectPackage ? context.parent.environment : context.parent.environment; + const environment = context.parent.environment; const packageManager = em.getPackageManager(environment); await packageManager?.manage(environment, { uninstall: [moduleName], install: [] }); return; @@ -319,7 +319,7 @@ export async function handlePackageUninstall(context: unknown, em: EnvironmentMa export async function handlePackageVersionManagement(context: unknown, em: EnvironmentManagers) { if (context instanceof PackageTreeItem || context instanceof ProjectPackage) { const pkg = context.pkg; - const environment = context instanceof ProjectPackage ? context.parent.environment : context.parent.environment; + const environment = context.parent.environment; const packageManager = em.getPackageManager(environment); const version = await showInputBox({ @@ -338,7 +338,7 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir }, }); - if (version === undefined) { + if (version === undefined || version === pkg.version) { return; } From c1d0e2f39c1593422badfe7654a80ef5080e2b8d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Fri, 8 May 2026 14:38:48 -0700 Subject: [PATCH 04/15] Add mechanisms for getting package versions --- package-lock.json | 16 ++++ package.json | 7 +- src/api.ts | 39 +++++++++- src/features/envCommands.ts | 59 ++++++++++---- src/features/pythonApi.ts | 20 +++++ src/internal.api.ts | 11 +++ src/managers/builtin/helpers.ts | 39 ++++++++++ src/managers/builtin/pipManager.ts | 77 +++++++++++++++++++ src/managers/conda/condaPackageManager.ts | 31 +++++++- src/managers/poetry/poetryPackageManager.ts | 19 ++++- .../managers/builtin/pipVersions.unit.test.ts | 35 +++++++++ 11 files changed, 333 insertions(+), 20 deletions(-) create mode 100644 src/test/managers/builtin/pipVersions.unit.test.ts diff --git a/package-lock.json b/package-lock.json index b3ac4765..f7b4f2fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", + "semver": "^7.7.4", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" @@ -22,6 +23,7 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "^22.15.1", + "@types/semver": "^7.7.1", "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/vscode": "^1.99.0", @@ -716,6 +718,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sinon": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", @@ -4888,6 +4897,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6455,6 +6465,12 @@ "undici-types": "~6.21.0" } }, + "@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, "@types/sinon": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", diff --git a/package.json b/package.json index b7876f61..196bbe2b 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,10 @@ "python-envs.workspaceSearchPaths": { "type": "array", "description": "%python-envs.workspaceSearchPaths.description%", - "default": [".venv", "*/.venv"], + "default": [ + ".venv", + "*/.venv" + ], "scope": "resource", "items": { "type": "string" @@ -709,6 +712,7 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "^22.15.1", + "@types/semver": "^7.7.1", "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/vscode": "^1.99.0", @@ -734,6 +738,7 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", + "semver": "^7.7.4", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" diff --git a/src/api.ts b/src/api.ts index b641ad3f..343245af 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import type * as semver from 'semver'; import type { Disposable, Event, @@ -685,6 +686,21 @@ export interface PackageManager { * @returns A promise that resolves when the cache is cleared. */ clearCache?(): Promise; + + /** + * Returns the version of the underlying package management tool (e.g., pip, conda). + * @returns A promise that resolves to a SemVer object, or `undefined` if not available. + */ + getVersion?(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of available versions for a given package. + * @param packageName - The name of the package to look up. + * @param environment - The Python environment context for the lookup. + * @returns A promise that resolves to an array of version strings (newest first), + * or `undefined` if this manager does not support version listing. + */ + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1053,12 +1069,33 @@ export interface PythonPackageManagementApi { managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; } +export interface PythonPackageVersionApi { + /** + * Get the version of the package manager tool associated with the given environment. + * + * @param environment The Python Environment whose package manager version is requested. + * @returns The SemVer version of the package manager tool, or `undefined` if not available. + */ + getPackageManagerVersion(environment: PythonEnvironment): Promise; + + /** + * Get the list of available versions for a package from the package manager + * associated with the given environment. + * + * @param packageName The name of the package. + * @param environment The Python Environment context for the lookup. + * @returns An array of version strings (newest first), or `undefined` if not supported. + */ + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; +} + export interface PythonPackageManagerApi extends PythonPackageManagerRegistrationApi, PythonPackageGetterApi, PythonPackageManagementApi, - PythonPackageItemApi {} + PythonPackageItemApi, + PythonPackageVersionApi {} export interface PythonProjectCreationApi { /** diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 56f9e769..7441ab31 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -48,6 +48,7 @@ import { showInformationMessage, showInputBox, showOpenDialog, + showQuickPick, withProgress, } from '../common/window.apis'; import { PEP440_VERSION_REGEX } from '../managers/builtin/pipUtils'; @@ -322,27 +323,53 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir const environment = context.parent.environment; const packageManager = em.getPackageManager(environment); - const version = await showInputBox({ - title: l10n.t('Manage Package Version'), - prompt: l10n.t('Enter the version for {0}', pkg.name), - value: pkg.version, - placeHolder: l10n.t('e.g. 1.2.3'), - validateInput: (value) => { - if (value.length === 0) { - return l10n.t('Version cannot be empty'); - } - if (!PEP440_VERSION_REGEX.test(value)) { - return l10n.t('Invalid PEP 440 version: {0}', value); - } - return undefined; - }, - }); + if (!packageManager) { + return; + } + + let version: string | undefined; + + // Try to fetch available versions for a QuickPick experience + const availableVersions = await withProgress( + { location: ProgressLocation.Window, title: l10n.t('Fetching available versions for {0}...', pkg.name) }, + () => packageManager.getAvailableVersions(pkg.name, environment), + ); + + if (availableVersions && availableVersions.length > 0) { + const items = availableVersions.map((v) => ({ + label: v, + description: v === pkg.version ? '$(check)' : undefined, + })); + + const selected = await showQuickPick(items, { + title: l10n.t('Select version for {0}', pkg.name), + placeHolder: l10n.t('Choose a version or press Escape to cancel'), + }); + version = selected?.label; + } else { + // Fallback to free-text input if version listing is not available + version = await showInputBox({ + title: l10n.t('Manage Package Version'), + prompt: l10n.t('Enter the version for {0}', pkg.name), + value: pkg.version, + placeHolder: l10n.t('e.g. 1.2.3'), + validateInput: (value) => { + if (value.length === 0) { + return l10n.t('Version cannot be empty'); + } + if (!PEP440_VERSION_REGEX.test(value)) { + return l10n.t('Invalid PEP 440 version: {0}', value); + } + return undefined; + }, + }); + } if (version === undefined || version === pkg.version) { return; } - await packageManager?.manage(environment, { + await packageManager.manage(environment, { install: [`${pkg.name}==${version}`], uninstall: [], }); diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index f071b0f5..cfc2281f 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -1,3 +1,4 @@ +import * as semver from 'semver'; import { Disposable, Event, EventEmitter, TaskExecution, Terminal, Uri } from 'vscode'; import { CreateEnvironmentOptions, @@ -266,6 +267,25 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { return manager.getPackages(context); } onDidChangePackages: Event = this._onDidChangePackages.event; + + async getPackageManagerVersion(environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); + const manager = this.envManagers.getPackageManager(environment); + if (!manager) { + return undefined; + } + return manager.getVersion(environment); + } + + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); + const manager = this.envManagers.getPackageManager(environment); + if (!manager) { + return undefined; + } + return manager.getAvailableVersions(packageName, environment); + } + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package { const mgr = this.envManagers.packageManagers.find((m) => m.equals(manager)); if (!mgr) { diff --git a/src/internal.api.ts b/src/internal.api.ts index 3d896c0c..7bde3c90 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -1,3 +1,4 @@ +import * as semver from 'semver'; import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, Uri } from 'vscode'; import { CreateEnvironmentOptions, @@ -378,6 +379,16 @@ export class InternalPackageManager implements PackageManager { equals(other: PackageManager): boolean { return this.manager === other; } + + getVersion(environment: PythonEnvironment): Promise { + return this.manager.getVersion ? this.manager.getVersion(environment) : Promise.resolve(undefined); + } + + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + return this.manager.getAvailableVersions + ? this.manager.getAvailableVersions(packageName, environment) + : Promise.resolve(undefined); + } } export interface PythonProjectManager extends Disposable { diff --git a/src/managers/builtin/helpers.ts b/src/managers/builtin/helpers.ts index 911bc603..5bbdd144 100644 --- a/src/managers/builtin/helpers.ts +++ b/src/managers/builtin/helpers.ts @@ -6,6 +6,45 @@ import { createDeferred } from '../../common/utils/deferred'; import { getConfiguration } from '../../common/workspace.apis'; import { getUvEnvironments } from './uvEnvironments'; +/** + * Result of running a process, capturing all output regardless of exit code. + */ +export interface ProcessResult { + stdout: string; + stderr: string; + exitCode: number | null; +} + +/** + * Runs a process and captures stdout/stderr regardless of exit code. + * Unlike runPython/runUV, this never rejects on non-zero exit codes. + */ +export function runProcessCaptureAll( + command: string, + args: string[], + log?: LogOutputChannel, + cwd?: string, +): Promise { + log?.info(`Running: ${command} ${args.join(' ')}`); + return new Promise((resolve) => { + const proc = spawnProcess(command, args, { cwd }); + let stdout = ''; + let stderr = ''; + proc.stdout?.on('data', (data) => { + stdout += data.toString('utf-8'); + }); + proc.stderr?.on('data', (data) => { + stderr += data.toString('utf-8'); + }); + proc.on('error', () => { + resolve({ stdout, stderr, exitCode: -1 }); + }); + proc.on('close', (code) => { + resolve({ stdout, stderr, exitCode: code }); + }); + }); +} + let available = createDeferred(); /** diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 81d26ea0..3c6b779f 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -1,3 +1,4 @@ +import * as semver from 'semver'; import { CancellationError, Disposable, @@ -19,6 +20,7 @@ import { PythonEnvironment, PythonEnvironmentApi, } from '../../api'; +import { runPython, runUV, shouldUseUv } from './helpers'; import { getWorkspacePackagesToInstall } from './pipUtils'; import { managePackages, refreshPackages } from './utils'; import { VenvManager } from './venvManager'; @@ -131,8 +133,83 @@ export class PipPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } + async getVersion(environment: PythonEnvironment): Promise { + try { + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + if (useUv) { + const result = await runUV(['--version'], undefined, this.log); + // "uv X.Y.Z" + const match = result.match(/^uv\s+(\d+\.\d+(?:\.\d+)*)/); + return match ? (semver.coerce(match[1]) ?? undefined) : undefined; + } + const result = await runPython( + environment.execInfo?.run?.executable ?? 'python', + ['-m', 'pip', '--version'], + undefined, + this.log, + ); + // "pip X.Y.Z from /path/to/pip (python X.Y)" + const match = result.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); + return match ? (semver.coerce(match[1]) ?? undefined) : undefined; + } catch { + return undefined; + } + } + + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + try { + const python = environment.execInfo?.run?.executable; + if (!python) { + return undefined; + } + + const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); + if (useUv) { + // uv does not have a way to get available versions of a package, so we return undefined to indicate that this information is not available. + return undefined; + } + + // `pip index versions` command was added in pip 21.2.0, so we need to check the version before trying to use it. + const pipVersion = await this.getVersion(environment); + if (pipVersion && semver.gte(pipVersion, '21.2.0')) { + const output = await runPython( + python, + ['-m', 'pip', 'index', 'versions', packageName, '--json'], + undefined, + this.log, + ); + return parsePipIndexVersionsJson(output); + } + + return undefined; + } catch { + return undefined; + } + } + dispose(): void { this._onDidChangePackages.dispose(); this.packages.clear(); } } + +/** + * Parses JSON output from `pip index versions --json`. + * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] } + */ +export function parsePipIndexVersionsJson(output: string): string[] | undefined { + // Only capture output between braces + const match = output.match(/{[\s\S]*}/); + if (!match) { + return undefined; + } + try { + const parsed = JSON.parse(match[0]); + if (parsed && Array.isArray(parsed.versions) && parsed.versions.length > 0) { + return parsed.versions; + } + return undefined; + } catch { + return undefined; + } +} diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index c012ea91..a5fcbc82 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -1,3 +1,4 @@ +import * as semver from 'semver'; import { CancellationError, Disposable, @@ -20,7 +21,7 @@ import { import { showErrorMessageWithLogs } from '../../common/errors/utils'; import { CondaStrings } from '../../common/localize'; import { withProgress } from '../../common/window.apis'; -import { getCommonCondaPackagesToInstall, managePackages, refreshPackages } from './condaUtils'; +import { getCommonCondaPackagesToInstall, managePackages, refreshPackages, runCondaExecutable } from './condaUtils'; function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { const changes: { kind: PackageChangeKind; pkg: Package }[] = []; @@ -122,6 +123,34 @@ export class CondaPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } + async getVersion(_environment: PythonEnvironment): Promise { + try { + const output = await runCondaExecutable(['--version'], this.log); + // "conda X.Y.Z" + const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); + return match ? semver.coerce(match[1]) ?? undefined : undefined; + } catch { + return undefined; + } + } + + async getAvailableVersions(packageName: string, _environment: PythonEnvironment): Promise { + try { + const output = await runCondaExecutable(['search', packageName, '--json'], this.log); + const parsed = JSON.parse(output); + if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { + const versions: string[] = parsed[packageName] + .map((entry: { version?: string }) => entry.version) + .filter((v: unknown): v is string => typeof v === 'string'); + // Deduplicate and return newest first + return [...new Set(versions)].reverse(); + } + return undefined; + } catch { + return undefined; + } + } + dispose() { this._onDidChangePackages.dispose(); this.packages.clear(); diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index 21d5fb82..d0d507b5 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -1,3 +1,4 @@ +import * as semver from 'semver'; import * as fsapi from 'fs-extra'; import * as path from 'path'; import { @@ -25,7 +26,7 @@ import { import { spawnProcess } from '../../common/childProcess.apis'; import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis'; import { PoetryManager } from './poetryManager'; -import { getPoetry } from './poetryUtils'; +import { getPoetry, getPoetryVersion } from './poetryUtils'; function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { const changes: { kind: PackageChangeKind; pkg: Package }[] = []; @@ -154,6 +155,22 @@ export class PoetryPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } + async getVersion(_environment: PythonEnvironment): Promise { + const poetry = await getPoetry(); + if (!poetry) { + return undefined; + } + const versionStr = await getPoetryVersion(poetry); + return versionStr ? semver.coerce(versionStr) ?? undefined : undefined; + } + + async getAvailableVersions(_packageName: string, _environment: PythonEnvironment): Promise { + // Poetry doesn't have a native "list available versions" command. + // Poetry 2.x supports `poetry search` but it was disabled on PyPI. + // Return undefined to indicate this manager doesn't support version listing. + return undefined; + } + dispose(): void { this._onDidChangePackages.dispose(); this.packages.clear(); diff --git a/src/test/managers/builtin/pipVersions.unit.test.ts b/src/test/managers/builtin/pipVersions.unit.test.ts new file mode 100644 index 00000000..2fad6329 --- /dev/null +++ b/src/test/managers/builtin/pipVersions.unit.test.ts @@ -0,0 +1,35 @@ +import assert from 'assert'; +import { parsePipIndexVersionsJson } from '../../../managers/builtin/pipManager'; + +suite('Pip Version Parsing', () => { + suite('parsePipIndexVersionsJson', () => { + test('parses valid JSON with versions array', () => { + const output = JSON.stringify({ name: 'requests', versions: ['2.31.0', '2.30.0', '2.29.0'] }); + const versions = parsePipIndexVersionsJson(output); + assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0']); + }); + + test('parses output with a single version', () => { + const output = JSON.stringify({ name: 'my-package', versions: ['1.0.0'] }); + const versions = parsePipIndexVersionsJson(output); + assert.deepStrictEqual(versions, ['1.0.0']); + }); + + test('returns undefined for empty versions array', () => { + const output = JSON.stringify({ name: 'pkg', versions: [] }); + const versions = parsePipIndexVersionsJson(output); + assert.strictEqual(versions, undefined); + }); + + test('returns undefined for invalid JSON', () => { + const versions = parsePipIndexVersionsJson('not json'); + assert.strictEqual(versions, undefined); + }); + + test('returns undefined when versions field is missing', () => { + const output = JSON.stringify({ name: 'pkg' }); + const versions = parsePipIndexVersionsJson(output); + assert.strictEqual(versions, undefined); + }); + }); +}); From 339d4c5fc95d4594c132f787939e4ffc4b162445 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 13:08:08 -0700 Subject: [PATCH 05/15] Add mechanism for uv --- src/managers/builtin/pipManager.ts | 39 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 3c6b779f..cb482f45 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -165,8 +165,13 @@ export class PipPackageManager implements PackageManager, Disposable { const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); if (useUv) { - // uv does not have a way to get available versions of a package, so we return undefined to indicate that this information is not available. - return undefined; + // use uvx + const output = await runUV( + ['tool', 'run', 'pip', 'index', 'versions', packageName, '--json'], + undefined, + this.log, + ); + return parsePipIndexVersionsJson(output); } // `pip index versions` command was added in pip 21.2.0, so we need to check the version before trying to use it. @@ -181,7 +186,16 @@ export class PipPackageManager implements PackageManager, Disposable { return parsePipIndexVersionsJson(output); } - return undefined; + // `pip install ==__invalid__` returns a list of available version in pip 20.3.4 and earlier. + if (pipVersion && semver.lte(pipVersion, '20.3.4')) { + const output = await runPython( + python, + ['-m', 'pip', 'install', `${packageName}==__invalid__`], + undefined, + this.log, + ); + return parsePipInstallVersions(output); + } } catch { return undefined; } @@ -193,6 +207,25 @@ export class PipPackageManager implements PackageManager, Disposable { } } +/** + * Parses the output of `pip install ==__invalid__` to extract available versions. + * Expected output format: + * ``` + * Collecting ==__invalid__ + * Could not find a version that satisfies the requirement ==__invalid__ (from versions: 1.2.3, 1.2.2, ...) + * No matching distribution found for ==__invalid__ + * ``` + */ +export function parsePipInstallVersions(output: string): string[] | undefined { + const match = output.match(/from versions:\s*([^\)]+)\)/); + if (match && match[1]) { + return match[1] + .split(',') + .map((v) => v.trim()) + .filter((v) => v.length > 0); + } +} + /** * Parses JSON output from `pip index versions --json`. * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] } From 0a0b9999fa6dda9815cbd986793a701483e9aa33 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 13:10:37 -0700 Subject: [PATCH 06/15] Improve comments --- src/managers/builtin/pipManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index cb482f45..69e648cf 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -163,9 +163,9 @@ export class PipPackageManager implements PackageManager, Disposable { return undefined; } + // uv - Run pip through pipx const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); if (useUv) { - // use uvx const output = await runUV( ['tool', 'run', 'pip', 'index', 'versions', packageName, '--json'], undefined, @@ -174,7 +174,7 @@ export class PipPackageManager implements PackageManager, Disposable { return parsePipIndexVersionsJson(output); } - // `pip index versions` command was added in pip 21.2.0, so we need to check the version before trying to use it. + // pip >= 21.2.0 - use `pip index versions --json` to get available versions in a machine readable format. const pipVersion = await this.getVersion(environment); if (pipVersion && semver.gte(pipVersion, '21.2.0')) { const output = await runPython( @@ -186,7 +186,7 @@ export class PipPackageManager implements PackageManager, Disposable { return parsePipIndexVersionsJson(output); } - // `pip install ==__invalid__` returns a list of available version in pip 20.3.4 and earlier. + // pip <= 20.3.4 - use `pip install ==__invalid__` to get available versions from error message. if (pipVersion && semver.lte(pipVersion, '20.3.4')) { const output = await runPython( python, From 7b0e8cbfe344b149f40b66816e8a5a0aaa0129ab Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 13:51:02 -0700 Subject: [PATCH 07/15] Address pr comments --- api/package-lock.json | 41 ++++++++++++++++++++++++++++++ api/package.json | 4 +++ api/src/main.ts | 40 ++++++++++++++++++++++++++++- src/features/envCommands.ts | 2 +- src/managers/builtin/pipManager.ts | 7 +++-- 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 3795b322..7f45ff68 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -8,7 +8,12 @@ "name": "@vscode/python-environments", "version": "1.0.0", "license": "MIT", + "dependencies": { + "semver": "^7.6.0" + }, "devDependencies": { + "@types/node": "^22.0.0", + "@types/semver": "^7.5.8", "@types/vscode": "^1.99.0", "typescript": "^5.1.3" }, @@ -17,6 +22,23 @@ "vscode": "^1.110.0" } }, + "node_modules/@types/node": { + "version": "22.19.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.109.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", @@ -24,6 +46,18 @@ "dev": true, "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -37,6 +71,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/api/package.json b/api/package.json index cd792cee..b32dd810 100644 --- a/api/package.json +++ b/api/package.json @@ -35,7 +35,11 @@ }, "devDependencies": { "@types/node": "^22.0.0", + "@types/semver": "^7.5.8", "@types/vscode": "^1.99.0", "typescript": "^5.1.3" + }, + "dependencies": { + "semver": "^7.6.0" } } diff --git a/api/src/main.ts b/api/src/main.ts index 32cc9ff9..640a6970 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import type * as semver from 'semver'; import { Disposable, Event, @@ -691,6 +692,22 @@ export interface PackageManager { * @returns A promise that resolves when the cache is cleared. */ clearCache?(): Promise; + + /** + * Returns the version of the underlying package management tool (e.g., pip, conda). + * @param environment - The Python environment context. + * @returns A promise that resolves to a SemVer object, or `undefined` if not available. + */ + getVersion?(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of available versions for a given package. + * @param packageName - The name of the package to look up. + * @param environment - The Python environment context for the lookup. + * @returns A promise that resolves to an array of version strings (newest first), + * or `undefined` if this manager does not support version listing. + */ + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1059,12 +1076,33 @@ export interface PythonPackageManagementApi { managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; } +export interface PythonPackageVersionApi { + /** + * Get the version of the package manager tool associated with the given environment. + * + * @param environment The Python Environment whose package manager version is requested. + * @returns The SemVer version of the package manager tool, or `undefined` if not available. + */ + getPackageManagerVersion(environment: PythonEnvironment): Promise; + + /** + * Get the list of available versions for a package from the package manager + * associated with the given environment. + * + * @param packageName The name of the package. + * @param environment The Python Environment context for the lookup. + * @returns An array of version strings (newest first), or `undefined` if not supported. + */ + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; +} + export interface PythonPackageManagerApi extends PythonPackageManagerRegistrationApi, PythonPackageGetterApi, PythonPackageManagementApi, - PythonPackageItemApi {} + PythonPackageItemApi, + PythonPackageVersionApi {} export interface PythonProjectCreationApi { /** diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 7441ab31..8c1f968c 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -338,7 +338,7 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir if (availableVersions && availableVersions.length > 0) { const items = availableVersions.map((v) => ({ label: v, - description: v === pkg.version ? '$(check)' : undefined, + description: v === pkg.version ? `$(check) ${l10n.t('Installed')}` : undefined, })); const selected = await showQuickPick(items, { diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 69e648cf..b5ed38fa 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -20,7 +20,7 @@ import { PythonEnvironment, PythonEnvironmentApi, } from '../../api'; -import { runPython, runUV, shouldUseUv } from './helpers'; +import { runProcessCaptureAll, runPython, runUV, shouldUseUv } from './helpers'; import { getWorkspacePackagesToInstall } from './pipUtils'; import { managePackages, refreshPackages } from './utils'; import { VenvManager } from './venvManager'; @@ -188,13 +188,12 @@ export class PipPackageManager implements PackageManager, Disposable { // pip <= 20.3.4 - use `pip install ==__invalid__` to get available versions from error message. if (pipVersion && semver.lte(pipVersion, '20.3.4')) { - const output = await runPython( + const result = await runProcessCaptureAll( python, ['-m', 'pip', 'install', `${packageName}==__invalid__`], - undefined, this.log, ); - return parsePipInstallVersions(output); + return parsePipInstallVersions(result.stdout + result.stderr); } } catch { return undefined; From a26314dbd0e4c87878a698de5726ef1ebaec2ee4 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 14:40:40 -0700 Subject: [PATCH 08/15] Update semver versions --- api/package-lock.json | 2 +- api/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 7f45ff68..ca26b12a 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "semver": "^7.6.0" + "semver": "^7.8.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/api/package.json b/api/package.json index b32dd810..4e05126e 100644 --- a/api/package.json +++ b/api/package.json @@ -40,6 +40,6 @@ "typescript": "^5.1.3" }, "dependencies": { - "semver": "^7.6.0" + "semver": "^7.8.0" } } From 375d775eb6750e1b1e16d9e85b6aa7d45505bcc6 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 16:04:26 -0700 Subject: [PATCH 09/15] Address pr comments --- api/src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main.ts b/api/src/main.ts index 640a6970..ba2939b6 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -707,7 +707,7 @@ export interface PackageManager { * @returns A promise that resolves to an array of version strings (newest first), * or `undefined` if this manager does not support version listing. */ - getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1093,7 +1093,7 @@ export interface PythonPackageVersionApi { * @param environment The Python Environment context for the lookup. * @returns An array of version strings (newest first), or `undefined` if not supported. */ - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } export interface PythonPackageManagerApi From 509bac32acc2f4e38d6eff9636b9890819a4fea7 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 16:06:52 -0700 Subject: [PATCH 10/15] Address pr comments --- package-lock.json | 14 +++++++------- package.json | 2 +- src/api.ts | 4 ++-- src/features/envCommands.ts | 4 ++-- src/features/pythonApi.ts | 2 +- src/internal.api.ts | 2 +- src/managers/builtin/pipManager.ts | 16 ++++++++++------ src/managers/conda/condaPackageManager.ts | 18 ++++++++++-------- src/managers/poetry/poetryPackageManager.ts | 2 +- .../managers/builtin/pipVersions.unit.test.ts | 5 +++-- 10 files changed, 38 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7b4f2fd..4582e381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", - "semver": "^7.7.4", + "semver": "^7.8.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" @@ -4894,9 +4894,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9441,9 +9441,9 @@ } }, "semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==" + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==" }, "serialize-javascript": { "version": "7.0.5", diff --git a/package.json b/package.json index 196bbe2b..f055f2d0 100644 --- a/package.json +++ b/package.json @@ -738,7 +738,7 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", - "semver": "^7.7.4", + "semver": "^7.8.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" diff --git a/src/api.ts b/src/api.ts index 343245af..9a233a5b 100644 --- a/src/api.ts +++ b/src/api.ts @@ -700,7 +700,7 @@ export interface PackageManager { * @returns A promise that resolves to an array of version strings (newest first), * or `undefined` if this manager does not support version listing. */ - getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1086,7 +1086,7 @@ export interface PythonPackageVersionApi { * @param environment The Python Environment context for the lookup. * @returns An array of version strings (newest first), or `undefined` if not supported. */ - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } export interface PythonPackageManagerApi diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 8c1f968c..a99a1a15 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -337,8 +337,8 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir if (availableVersions && availableVersions.length > 0) { const items = availableVersions.map((v) => ({ - label: v, - description: v === pkg.version ? `$(check) ${l10n.t('Installed')}` : undefined, + label: v.version, + description: v.version === pkg.version ? `$(check) ${l10n.t('Installed')}` : undefined, })); const selected = await showQuickPick(items, { diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index cfc2281f..3ea042f0 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -277,7 +277,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { return manager.getVersion(environment); } - async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { await waitForEnvManagerId([environment.envId.managerId]); const manager = this.envManagers.getPackageManager(environment); if (!manager) { diff --git a/src/internal.api.ts b/src/internal.api.ts index 7bde3c90..4426f31a 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -384,7 +384,7 @@ export class InternalPackageManager implements PackageManager { return this.manager.getVersion ? this.manager.getVersion(environment) : Promise.resolve(undefined); } - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { return this.manager.getAvailableVersions ? this.manager.getAvailableVersions(packageName, environment) : Promise.resolve(undefined); diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index b5ed38fa..551485ad 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -156,7 +156,7 @@ export class PipPackageManager implements PackageManager, Disposable { } } - async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { try { const python = environment.execInfo?.run?.executable; if (!python) { @@ -215,13 +215,14 @@ export class PipPackageManager implements PackageManager, Disposable { * No matching distribution found for ==__invalid__ * ``` */ -export function parsePipInstallVersions(output: string): string[] | undefined { +export function parsePipInstallVersions(output: string): semver.SemVer[] | undefined { const match = output.match(/from versions:\s*([^\)]+)\)/); if (match && match[1]) { return match[1] .split(',') - .map((v) => v.trim()) - .filter((v) => v.length > 0); + .filter((v) => !!v.trim()) + .map((v) => semver.coerce(v.trim()) as semver.SemVer) + .sort((a, b) => semver.rcompare(a, b)); } } @@ -229,7 +230,7 @@ export function parsePipInstallVersions(output: string): string[] | undefined { * Parses JSON output from `pip index versions --json`. * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] } */ -export function parsePipIndexVersionsJson(output: string): string[] | undefined { +export function parsePipIndexVersionsJson(output: string): semver.SemVer[] | undefined { // Only capture output between braces const match = output.match(/{[\s\S]*}/); if (!match) { @@ -238,7 +239,10 @@ export function parsePipIndexVersionsJson(output: string): string[] | undefined try { const parsed = JSON.parse(match[0]); if (parsed && Array.isArray(parsed.versions) && parsed.versions.length > 0) { - return parsed.versions; + return (parsed.versions as string[]) + .filter((v) => !!v.trim()) + .map((v) => semver.coerce(v) as semver.SemVer) + .sort((a, b) => semver.rcompare(a, b)); } return undefined; } catch { diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index a5fcbc82..aaf3cdb9 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -40,7 +40,10 @@ export class CondaPackageManager implements PackageManager, Disposable { private packages: Map = new Map(); - constructor(public readonly api: PythonEnvironmentApi, public readonly log: LogOutputChannel) { + constructor( + public readonly api: PythonEnvironmentApi, + public readonly log: LogOutputChannel, + ) { this.name = 'conda'; this.displayName = 'Conda'; this.description = CondaStrings.condaPackageMgr; @@ -128,22 +131,21 @@ export class CondaPackageManager implements PackageManager, Disposable { const output = await runCondaExecutable(['--version'], this.log); // "conda X.Y.Z" const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); - return match ? semver.coerce(match[1]) ?? undefined : undefined; + return match ? (semver.coerce(match[1]) ?? undefined) : undefined; } catch { return undefined; } } - async getAvailableVersions(packageName: string, _environment: PythonEnvironment): Promise { + async getAvailableVersions(packageName: string, _environment: PythonEnvironment): Promise { try { const output = await runCondaExecutable(['search', packageName, '--json'], this.log); const parsed = JSON.parse(output); if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { - const versions: string[] = parsed[packageName] - .map((entry: { version?: string }) => entry.version) - .filter((v: unknown): v is string => typeof v === 'string'); - // Deduplicate and return newest first - return [...new Set(versions)].reverse(); + return parsed[packageName] + .filter((entry: { version?: string }) => !!entry.version?.trim()) + .map((entry: { version?: string }) => semver.coerce(entry.version) as semver.SemVer) + .sort((a: semver.SemVer, b: semver.SemVer) => semver.rcompare(a, b)); } return undefined; } catch { diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index d0d507b5..e9b9c82f 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -164,7 +164,7 @@ export class PoetryPackageManager implements PackageManager, Disposable { return versionStr ? semver.coerce(versionStr) ?? undefined : undefined; } - async getAvailableVersions(_packageName: string, _environment: PythonEnvironment): Promise { + async getAvailableVersions(_packageName: string, _environment: PythonEnvironment): Promise { // Poetry doesn't have a native "list available versions" command. // Poetry 2.x supports `poetry search` but it was disabled on PyPI. // Return undefined to indicate this manager doesn't support version listing. diff --git a/src/test/managers/builtin/pipVersions.unit.test.ts b/src/test/managers/builtin/pipVersions.unit.test.ts index 2fad6329..718f5beb 100644 --- a/src/test/managers/builtin/pipVersions.unit.test.ts +++ b/src/test/managers/builtin/pipVersions.unit.test.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import * as semver from 'semver'; import { parsePipIndexVersionsJson } from '../../../managers/builtin/pipManager'; suite('Pip Version Parsing', () => { @@ -6,13 +7,13 @@ suite('Pip Version Parsing', () => { test('parses valid JSON with versions array', () => { const output = JSON.stringify({ name: 'requests', versions: ['2.31.0', '2.30.0', '2.29.0'] }); const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0']); + assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0'].map((v) => semver.coerce(v))); }); test('parses output with a single version', () => { const output = JSON.stringify({ name: 'my-package', versions: ['1.0.0'] }); const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, ['1.0.0']); + assert.deepStrictEqual(versions, [semver.coerce('1.0.0')]); }); test('returns undefined for empty versions array', () => { From c27641a4d2de039f06ec83c7192fdcaf371de4ae Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 16:51:29 -0700 Subject: [PATCH 11/15] Use renovatebot/pep440 --- api/package-lock.json | 32 +- api/package.json | 3 +- api/src/main.ts | 15 +- package-lock.json | 16 + package.json | 1 + src/api.ts | 16 +- src/features/envCommands.ts | 4 +- src/features/pythonApi.ts | 753 +++++++++--------- src/internal.api.ts | 6 +- src/managers/builtin/pipManager.ts | 29 +- src/managers/conda/condaPackageManager.ts | 14 +- src/managers/poetry/poetryPackageManager.ts | 9 +- .../managers/builtin/pipVersions.unit.test.ts | 6 +- 13 files changed, 460 insertions(+), 444 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index ca26b12a..764e8049 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,11 +9,10 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "semver": "^7.8.0" + "@renovatebot/pep440": "^3.1.0" }, "devDependencies": { "@types/node": "^22.0.0", - "@types/semver": "^7.5.8", "@types/vscode": "^1.99.0", "typescript": "^5.1.3" }, @@ -22,6 +21,16 @@ "vscode": "^1.110.0" } }, + "node_modules/@renovatebot/pep440": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-3.1.0.tgz", + "integrity": "sha512-Tx/wEv92j3HmFwlqfNp8Pq/BMJPVk8c5so/Ae8eHccceBeeZx4QDuLf6RYfXJ6kvw8H05K1KPoZSsigLQA7Rqg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.12.0 || ^20.0.0 || ^22.11.0", + "pnpm": "^8.6.11" + } + }, "node_modules/@types/node": { "version": "22.19.18", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", @@ -32,13 +41,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/vscode": { "version": "1.109.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.109.0.tgz", @@ -46,18 +48,6 @@ "dev": true, "license": "MIT" }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/api/package.json b/api/package.json index 4e05126e..bd05ec03 100644 --- a/api/package.json +++ b/api/package.json @@ -35,11 +35,10 @@ }, "devDependencies": { "@types/node": "^22.0.0", - "@types/semver": "^7.5.8", "@types/vscode": "^1.99.0", "typescript": "^5.1.3" }, "dependencies": { - "semver": "^7.8.0" + "@renovatebot/pep440": "^3.1.0" } } diff --git a/api/src/main.ts b/api/src/main.ts index ba2939b6..361bf962 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import type * as semver from 'semver'; import { Disposable, Event, @@ -21,6 +20,8 @@ import { * This is the public API for other extensions to interact with the Python Environments extension. */ +export type { Pep440Version } from '@renovatebot/pep440'; +import type { Pep440Version } from '@renovatebot/pep440'; /** * The path to an icon, or a theme-specific configuration of icons. */ @@ -698,7 +699,7 @@ export interface PackageManager { * @param environment - The Python environment context. * @returns A promise that resolves to a SemVer object, or `undefined` if not available. */ - getVersion?(environment: PythonEnvironment): Promise; + getVersion?(environment: PythonEnvironment): Promise; /** * Retrieves the list of available versions for a given package. @@ -707,7 +708,7 @@ export interface PackageManager { * @returns A promise that resolves to an array of version strings (newest first), * or `undefined` if this manager does not support version listing. */ - getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1076,14 +1077,14 @@ export interface PythonPackageManagementApi { managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; } -export interface PythonPackageVersionApi { +export interface Pep440VersionApi { /** * Get the version of the package manager tool associated with the given environment. * * @param environment The Python Environment whose package manager version is requested. * @returns The SemVer version of the package manager tool, or `undefined` if not available. */ - getPackageManagerVersion(environment: PythonEnvironment): Promise; + getPackageManagerVersion(environment: PythonEnvironment): Promise; /** * Get the list of available versions for a package from the package manager @@ -1093,7 +1094,7 @@ export interface PythonPackageVersionApi { * @param environment The Python Environment context for the lookup. * @returns An array of version strings (newest first), or `undefined` if not supported. */ - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } export interface PythonPackageManagerApi @@ -1102,7 +1103,7 @@ export interface PythonPackageManagerApi PythonPackageGetterApi, PythonPackageManagementApi, PythonPackageItemApi, - PythonPackageVersionApi {} + Pep440VersionApi {} export interface PythonProjectCreationApi { /** diff --git a/package-lock.json b/package-lock.json index 4582e381..0591f7ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.31.0", "dependencies": { "@iarna/toml": "^2.2.5", + "@renovatebot/pep440": "^3.1.0", "@vscode/extension-telemetry": "^0.9.7", "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", @@ -582,6 +583,16 @@ "node": ">=14" } }, + "node_modules/@renovatebot/pep440": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-3.1.0.tgz", + "integrity": "sha512-Tx/wEv92j3HmFwlqfNp8Pq/BMJPVk8c5so/Ae8eHccceBeeZx4QDuLf6RYfXJ6kvw8H05K1KPoZSsigLQA7Rqg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.12.0 || ^20.0.0 || ^22.11.0", + "pnpm": "^8.6.11" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -6336,6 +6347,11 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@renovatebot/pep440": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-3.1.0.tgz", + "integrity": "sha512-Tx/wEv92j3HmFwlqfNp8Pq/BMJPVk8c5so/Ae8eHccceBeeZx4QDuLf6RYfXJ6kvw8H05K1KPoZSsigLQA7Rqg==" + }, "@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", diff --git a/package.json b/package.json index f055f2d0..4c27f565 100644 --- a/package.json +++ b/package.json @@ -734,6 +734,7 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", + "@renovatebot/pep440": "^3.1.0", "@vscode/extension-telemetry": "^0.9.7", "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", diff --git a/src/api.ts b/src/api.ts index 9a233a5b..dc0e8ff6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import type * as semver from 'semver'; import type { Disposable, Event, @@ -15,6 +14,9 @@ import type { Uri, } from 'vscode'; +export type { Pep440Version } from '@renovatebot/pep440'; +import type { Pep440Version } from '@renovatebot/pep440'; + /** * The path to an icon, or a theme-specific configuration of icons. */ @@ -691,7 +693,7 @@ export interface PackageManager { * Returns the version of the underlying package management tool (e.g., pip, conda). * @returns A promise that resolves to a SemVer object, or `undefined` if not available. */ - getVersion?(environment: PythonEnvironment): Promise; + getVersion?(environment: PythonEnvironment): Promise; /** * Retrieves the list of available versions for a given package. @@ -700,7 +702,7 @@ export interface PackageManager { * @returns A promise that resolves to an array of version strings (newest first), * or `undefined` if this manager does not support version listing. */ - getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; } /** @@ -1069,14 +1071,14 @@ export interface PythonPackageManagementApi { managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; } -export interface PythonPackageVersionApi { +export interface Pep440VersionApi { /** * Get the version of the package manager tool associated with the given environment. * * @param environment The Python Environment whose package manager version is requested. * @returns The SemVer version of the package manager tool, or `undefined` if not available. */ - getPackageManagerVersion(environment: PythonEnvironment): Promise; + getPackageManagerVersion(environment: PythonEnvironment): Promise; /** * Get the list of available versions for a package from the package manager @@ -1086,7 +1088,7 @@ export interface PythonPackageVersionApi { * @param environment The Python Environment context for the lookup. * @returns An array of version strings (newest first), or `undefined` if not supported. */ - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } export interface PythonPackageManagerApi @@ -1095,7 +1097,7 @@ export interface PythonPackageManagerApi PythonPackageGetterApi, PythonPackageManagementApi, PythonPackageItemApi, - PythonPackageVersionApi {} + Pep440VersionApi {} export interface PythonProjectCreationApi { /** diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index a99a1a15..f1064e29 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -337,8 +337,8 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir if (availableVersions && availableVersions.length > 0) { const items = availableVersions.map((v) => ({ - label: v.version, - description: v.version === pkg.version ? `$(check) ${l10n.t('Installed')}` : undefined, + label: v.public, + description: v.public === pkg.version ? `$(check) ${l10n.t('Installed')}` : undefined, })); const selected = await showQuickPick(items, { diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index 3ea042f0..66f0c0d9 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -1,376 +1,377 @@ -import * as semver from 'semver'; -import { Disposable, Event, EventEmitter, TaskExecution, Terminal, Uri } from 'vscode'; -import { - CreateEnvironmentOptions, - CreateEnvironmentScope, - DidChangeEnvironmentEventArgs, - DidChangeEnvironmentsEventArgs, - DidChangeEnvironmentVariablesEventArgs, - DidChangePackagesEventArgs, - DidChangePythonProjectsEventArgs, - EnvironmentManager, - GetEnvironmentScope, - GetEnvironmentsScope, - Package, - PackageId, - PackageInfo, - PackageManagementOptions, - PackageManager, - PythonBackgroundRunOptions, - PythonEnvironment, - PythonEnvironmentApi, - PythonEnvironmentId, - PythonEnvironmentInfo, - PythonProcess, - PythonProject, - PythonProjectCreator, - PythonTaskExecutionOptions, - PythonTerminalCreateOptions, - PythonTerminalExecutionOptions, - RefreshEnvironmentsScope, - ResolveEnvironmentContext, - SetEnvironmentScope, -} from '../api'; -import { traceError, traceInfo } from '../common/logging'; -import { pickEnvironmentManager } from '../common/pickers/managers'; -import { createDeferred } from '../common/utils/deferred'; -import { checkUri } from '../common/utils/pathUtils'; -import { handlePythonPath } from '../common/utils/pythonPath'; -import { - EnvironmentManagers, - InternalEnvironmentManager, - ProjectCreators, - PythonEnvironmentImpl, - PythonPackageImpl, - PythonProjectManager, -} from '../internal.api'; -import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady'; -import { EnvVarManager } from './execution/envVariableManager'; -import { runAsTask } from './execution/runAsTask'; -import { runInBackground } from './execution/runInBackground'; -import { runInTerminal } from './terminal/runInTerminal'; -import { TerminalManager } from './terminal/terminalManager'; - -class PythonEnvironmentApiImpl implements PythonEnvironmentApi { - private readonly _onDidChangeEnvironments = new EventEmitter(); - private readonly _onDidChangeEnvironment = new EventEmitter(); - private readonly _onDidChangePythonProjects = new EventEmitter(); - private readonly _onDidChangePackages = new EventEmitter(); - private readonly _onDidChangeEnvironmentVariables = new EventEmitter(); - - constructor( - private readonly envManagers: EnvironmentManagers, - private readonly projectManager: PythonProjectManager, - private readonly projectCreators: ProjectCreators, - private readonly terminalManager: TerminalManager, - private readonly envVarManager: EnvVarManager, - private readonly disposables: Disposable[] = [], - ) { - this.disposables.push( - this._onDidChangeEnvironment, - this._onDidChangeEnvironments, - this._onDidChangePythonProjects, - this._onDidChangePackages, - this._onDidChangeEnvironmentVariables, - this.envManagers.onDidChangeActiveEnvironment((e) => { - this._onDidChangeEnvironment.fire(e); - const location = e.uri?.fsPath ?? 'global'; - traceInfo( - `Python API: Changed environment from ${e.old?.displayName} to ${e.new?.displayName} for: ${location}`, - ); - }), - this.envVarManager.onDidChangeEnvironmentVariables((e) => this._onDidChangeEnvironmentVariables.fire(e)), - ); - } - - registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable { - const disposables: Disposable[] = []; - disposables.push(this.envManagers.registerEnvironmentManager(manager, options)); - if (manager.onDidChangeEnvironments) { - disposables.push(manager.onDidChangeEnvironments((e) => this._onDidChangeEnvironments.fire(e))); - } - if (manager.onDidChangeEnvironment) { - disposables.push( - manager.onDidChangeEnvironment((e) => { - setImmediate(() => { - // Refresh the central cache for this scope. This ensures that only the - // *selected* manager's changes propagate (refreshEnvironment checks - // getEnvironmentManager(scope) internally). It updates the cache and - // fires onDidChangeActiveEnvironment, which the Python API listens to. - this.envManagers.refreshEnvironment(e.uri).catch((err) => - traceError('Failed to refresh environment on change:', err), - ); - }); - }), - ); - } - return new Disposable(() => disposables.forEach((d) => d.dispose())); - } - - createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment { - const mgr = this.envManagers.managers.find((m) => m.equals(manager)); - if (!mgr) { - throw new Error('Environment manager not found'); - } - const randomStr = Math.random().toString(36).substring(2); - const envId: PythonEnvironmentId = { - managerId: mgr.id, - id: `${info.name}-${randomStr}`, - }; - return new PythonEnvironmentImpl(envId, info); - } - - async createEnvironment( - scope: CreateEnvironmentScope, - options: CreateEnvironmentOptions | undefined, - ): Promise { - if (scope === 'global' || (!Array.isArray(scope) && scope instanceof Uri)) { - await waitForEnvManager(scope === 'global' ? undefined : [scope]); - const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope); - if (!manager) { - throw new Error('No environment manager found'); - } - if (!manager.supportsCreate) { - throw new Error(`Environment manager does not support creating environments: ${manager.id}`); - } - return manager.create(scope, options); - } else if (Array.isArray(scope) && scope.length === 1 && scope[0] instanceof Uri) { - return this.createEnvironment(scope[0], options); - } else if (Array.isArray(scope) && scope.length > 0 && scope.every((s) => s instanceof Uri)) { - await waitForEnvManager(scope); - const managers: InternalEnvironmentManager[] = []; - scope.forEach((s) => { - const manager = this.envManagers.getEnvironmentManager(s); - if (manager && !managers.includes(manager) && manager.supportsCreate) { - managers.push(manager); - } - }); - - if (managers.length === 0) { - throw new Error('No environment managers found'); - } - - const managerId = await pickEnvironmentManager(managers); - if (!managerId) { - throw new Error('No environment manager selected'); - } - - const manager = managers.find((m) => m.id === managerId); - if (!manager) { - throw new Error('No environment manager found'); - } - - const result = await manager.create(scope, options); - return result; - } - } - async removeEnvironment(environment: PythonEnvironment): Promise { - await waitForEnvManagerId([environment.envId.managerId]); - const manager = this.envManagers.getEnvironmentManager(environment); - if (!manager) { - return Promise.reject(new Error('No environment manager found')); - } - return manager.remove(environment); - } - async refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { - const currentScope = checkUri(scope) as RefreshEnvironmentsScope; - - if (currentScope === undefined) { - await waitForAllEnvManagers(); - await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(currentScope))); - return Promise.resolve(); - } - - await waitForEnvManager([currentScope]); - const manager = this.envManagers.getEnvironmentManager(currentScope); - if (!manager) { - return Promise.reject(new Error(`No environment manager found for: ${currentScope.fsPath}`)); - } - return manager.refresh(currentScope); - } - async getEnvironments(scope: GetEnvironmentsScope): Promise { - const currentScope = checkUri(scope) as GetEnvironmentsScope; - if (currentScope === 'all' || currentScope === 'global') { - await waitForAllEnvManagers(); - const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(currentScope)); - const items = await Promise.all(promises); - return items.flat(); - } - - await waitForEnvManager([currentScope]); - const manager = this.envManagers.getEnvironmentManager(currentScope); - if (!manager) { - return []; - } - - const items = await manager.getEnvironments(currentScope); - return items; - } - onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; - async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { - const currentScope = checkUri(scope) as SetEnvironmentScope; - await waitForEnvManager( - currentScope ? (currentScope instanceof Uri ? [currentScope] : currentScope) : undefined, - ); - return this.envManagers.setEnvironment(currentScope, environment); - } - async getEnvironment(scope: GetEnvironmentScope): Promise { - const currentScope = checkUri(scope) as GetEnvironmentScope; - await waitForEnvManager(currentScope ? [currentScope] : undefined); - return this.envManagers.getEnvironment(currentScope); - } - onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; - async resolveEnvironment(context: ResolveEnvironmentContext): Promise { - await waitForAllEnvManagers(); - const projects = this.projectManager.getProjects(); - const projectEnvManagers: InternalEnvironmentManager[] = []; - projects.forEach((p) => { - const manager = this.envManagers.getEnvironmentManager(p.uri); - if (manager && !projectEnvManagers.includes(manager)) { - projectEnvManagers.push(manager); - } - }); - - return await handlePythonPath(context, this.envManagers.managers, projectEnvManagers); - } - - registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable { - const disposables: Disposable[] = []; - disposables.push(this.envManagers.registerPackageManager(manager, options)); - if (manager.onDidChangePackages) { - disposables.push(manager.onDidChangePackages((e) => this._onDidChangePackages.fire(e))); - } - return new Disposable(() => disposables.forEach((d) => d.dispose())); - } - async managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise { - await waitForEnvManagerId([context.envId.managerId]); - const manager = this.envManagers.getPackageManager(context); - if (!manager) { - return Promise.reject(new Error('No package manager found')); - } - return manager.manage(context, options); - } - async refreshPackages(context: PythonEnvironment): Promise { - await waitForEnvManagerId([context.envId.managerId]); - const manager = this.envManagers.getPackageManager(context); - if (!manager) { - return Promise.reject(new Error('No package manager found')); - } - return manager.refresh(context); - } - async getPackages(context: PythonEnvironment): Promise { - await waitForEnvManagerId([context.envId.managerId]); - const manager = this.envManagers.getPackageManager(context); - if (!manager) { - return Promise.resolve(undefined); - } - return manager.getPackages(context); - } - onDidChangePackages: Event = this._onDidChangePackages.event; - - async getPackageManagerVersion(environment: PythonEnvironment): Promise { - await waitForEnvManagerId([environment.envId.managerId]); - const manager = this.envManagers.getPackageManager(environment); - if (!manager) { - return undefined; - } - return manager.getVersion(environment); - } - - async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { - await waitForEnvManagerId([environment.envId.managerId]); - const manager = this.envManagers.getPackageManager(environment); - if (!manager) { - return undefined; - } - return manager.getAvailableVersions(packageName, environment); - } - - createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package { - const mgr = this.envManagers.packageManagers.find((m) => m.equals(manager)); - if (!mgr) { - throw new Error('Package manager not found'); - } - const randomStr = Math.random().toString(36).substring(2); - const pkg: PackageId = { - managerId: mgr.id, - environmentId: environment.envId.id, - id: `${info.name}-${randomStr}`, - }; - return new PythonPackageImpl(pkg, info); - } - - addPythonProject(projects: PythonProject | PythonProject[]): void { - this.projectManager.add(projects); - } - removePythonProject(pyWorkspace: PythonProject): void { - this.projectManager.remove(pyWorkspace); - } - getPythonProjects(): readonly PythonProject[] { - return this.projectManager.getProjects(); - } - onDidChangePythonProjects: Event = this._onDidChangePythonProjects.event; - getPythonProject(uri: Uri): PythonProject | undefined { - return this.projectManager.get(checkUri(uri) as Uri); - } - registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { - return this.projectCreators.registerPythonProjectCreator(creator); - } - async createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise { - return this.terminalManager.create(environment, options); - } - async runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise { - const terminal = await this.terminalManager.getProjectTerminal( - options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), - environment, - ); - await runInTerminal(environment, terminal, options); - return terminal; - } - async runInDedicatedTerminal( - terminalKey: Uri | string, - environment: PythonEnvironment, - options: PythonTerminalExecutionOptions, - ): Promise { - const terminal = await this.terminalManager.getDedicatedTerminal( - terminalKey, - options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), - environment, - ); - await runInTerminal(environment, terminal, options); - return Promise.resolve(terminal); - } - runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise { - return runAsTask(environment, options); - } - runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise { - return runInBackground(environment, options); - } - - onDidChangeEnvironmentVariables: Event = - this._onDidChangeEnvironmentVariables.event; - getEnvironmentVariables( - uri: Uri, - overrides?: ({ [key: string]: string | undefined } | Uri)[], - baseEnvVar?: { [key: string]: string | undefined }, - ): Promise<{ [key: string]: string | undefined }> { - return this.envVarManager.getEnvironmentVariables(checkUri(uri) as Uri, overrides, baseEnvVar); - } -} - -let _deferred = createDeferred(); -export function setPythonApi( - envMgr: EnvironmentManagers, - projectMgr: PythonProjectManager, - projectCreators: ProjectCreators, - terminalManager: TerminalManager, - envVarManager: EnvVarManager, -) { - _deferred.resolve( - new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators, terminalManager, envVarManager), - ); -} - -export function getPythonApi(): Promise { - return _deferred.promise; -} + +import type { Pep440Version } from '@renovatebot/pep440'; +import { Disposable, Event, EventEmitter, TaskExecution, Terminal, Uri } from 'vscode'; +import { + CreateEnvironmentOptions, + CreateEnvironmentScope, + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + DidChangeEnvironmentVariablesEventArgs, + DidChangePackagesEventArgs, + DidChangePythonProjectsEventArgs, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + Package, + PackageId, + PackageInfo, + PackageManagementOptions, + PackageManager, + PythonBackgroundRunOptions, + PythonEnvironment, + PythonEnvironmentApi, + PythonEnvironmentId, + PythonEnvironmentInfo, + PythonProcess, + PythonProject, + PythonProjectCreator, + PythonTaskExecutionOptions, + PythonTerminalCreateOptions, + PythonTerminalExecutionOptions, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../api'; +import { traceError, traceInfo } from '../common/logging'; +import { pickEnvironmentManager } from '../common/pickers/managers'; +import { createDeferred } from '../common/utils/deferred'; +import { checkUri } from '../common/utils/pathUtils'; +import { handlePythonPath } from '../common/utils/pythonPath'; +import { + EnvironmentManagers, + InternalEnvironmentManager, + ProjectCreators, + PythonEnvironmentImpl, + PythonPackageImpl, + PythonProjectManager, +} from '../internal.api'; +import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady'; +import { EnvVarManager } from './execution/envVariableManager'; +import { runAsTask } from './execution/runAsTask'; +import { runInBackground } from './execution/runInBackground'; +import { runInTerminal } from './terminal/runInTerminal'; +import { TerminalManager } from './terminal/terminalManager'; + +class PythonEnvironmentApiImpl implements PythonEnvironmentApi { + private readonly _onDidChangeEnvironments = new EventEmitter(); + private readonly _onDidChangeEnvironment = new EventEmitter(); + private readonly _onDidChangePythonProjects = new EventEmitter(); + private readonly _onDidChangePackages = new EventEmitter(); + private readonly _onDidChangeEnvironmentVariables = new EventEmitter(); + + constructor( + private readonly envManagers: EnvironmentManagers, + private readonly projectManager: PythonProjectManager, + private readonly projectCreators: ProjectCreators, + private readonly terminalManager: TerminalManager, + private readonly envVarManager: EnvVarManager, + private readonly disposables: Disposable[] = [], + ) { + this.disposables.push( + this._onDidChangeEnvironment, + this._onDidChangeEnvironments, + this._onDidChangePythonProjects, + this._onDidChangePackages, + this._onDidChangeEnvironmentVariables, + this.envManagers.onDidChangeActiveEnvironment((e) => { + this._onDidChangeEnvironment.fire(e); + const location = e.uri?.fsPath ?? 'global'; + traceInfo( + `Python API: Changed environment from ${e.old?.displayName} to ${e.new?.displayName} for: ${location}`, + ); + }), + this.envVarManager.onDidChangeEnvironmentVariables((e) => this._onDidChangeEnvironmentVariables.fire(e)), + ); + } + + registerEnvironmentManager(manager: EnvironmentManager, options?: { extensionId?: string }): Disposable { + const disposables: Disposable[] = []; + disposables.push(this.envManagers.registerEnvironmentManager(manager, options)); + if (manager.onDidChangeEnvironments) { + disposables.push(manager.onDidChangeEnvironments((e) => this._onDidChangeEnvironments.fire(e))); + } + if (manager.onDidChangeEnvironment) { + disposables.push( + manager.onDidChangeEnvironment((e) => { + setImmediate(() => { + // Refresh the central cache for this scope. This ensures that only the + // *selected* manager's changes propagate (refreshEnvironment checks + // getEnvironmentManager(scope) internally). It updates the cache and + // fires onDidChangeActiveEnvironment, which the Python API listens to. + this.envManagers.refreshEnvironment(e.uri).catch((err) => + traceError('Failed to refresh environment on change:', err), + ); + }); + }), + ); + } + return new Disposable(() => disposables.forEach((d) => d.dispose())); + } + + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment { + const mgr = this.envManagers.managers.find((m) => m.equals(manager)); + if (!mgr) { + throw new Error('Environment manager not found'); + } + const randomStr = Math.random().toString(36).substring(2); + const envId: PythonEnvironmentId = { + managerId: mgr.id, + id: `${info.name}-${randomStr}`, + }; + return new PythonEnvironmentImpl(envId, info); + } + + async createEnvironment( + scope: CreateEnvironmentScope, + options: CreateEnvironmentOptions | undefined, + ): Promise { + if (scope === 'global' || (!Array.isArray(scope) && scope instanceof Uri)) { + await waitForEnvManager(scope === 'global' ? undefined : [scope]); + const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope); + if (!manager) { + throw new Error('No environment manager found'); + } + if (!manager.supportsCreate) { + throw new Error(`Environment manager does not support creating environments: ${manager.id}`); + } + return manager.create(scope, options); + } else if (Array.isArray(scope) && scope.length === 1 && scope[0] instanceof Uri) { + return this.createEnvironment(scope[0], options); + } else if (Array.isArray(scope) && scope.length > 0 && scope.every((s) => s instanceof Uri)) { + await waitForEnvManager(scope); + const managers: InternalEnvironmentManager[] = []; + scope.forEach((s) => { + const manager = this.envManagers.getEnvironmentManager(s); + if (manager && !managers.includes(manager) && manager.supportsCreate) { + managers.push(manager); + } + }); + + if (managers.length === 0) { + throw new Error('No environment managers found'); + } + + const managerId = await pickEnvironmentManager(managers); + if (!managerId) { + throw new Error('No environment manager selected'); + } + + const manager = managers.find((m) => m.id === managerId); + if (!manager) { + throw new Error('No environment manager found'); + } + + const result = await manager.create(scope, options); + return result; + } + } + async removeEnvironment(environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); + const manager = this.envManagers.getEnvironmentManager(environment); + if (!manager) { + return Promise.reject(new Error('No environment manager found')); + } + return manager.remove(environment); + } + async refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { + const currentScope = checkUri(scope) as RefreshEnvironmentsScope; + + if (currentScope === undefined) { + await waitForAllEnvManagers(); + await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(currentScope))); + return Promise.resolve(); + } + + await waitForEnvManager([currentScope]); + const manager = this.envManagers.getEnvironmentManager(currentScope); + if (!manager) { + return Promise.reject(new Error(`No environment manager found for: ${currentScope.fsPath}`)); + } + return manager.refresh(currentScope); + } + async getEnvironments(scope: GetEnvironmentsScope): Promise { + const currentScope = checkUri(scope) as GetEnvironmentsScope; + if (currentScope === 'all' || currentScope === 'global') { + await waitForAllEnvManagers(); + const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(currentScope)); + const items = await Promise.all(promises); + return items.flat(); + } + + await waitForEnvManager([currentScope]); + const manager = this.envManagers.getEnvironmentManager(currentScope); + if (!manager) { + return []; + } + + const items = await manager.getEnvironments(currentScope); + return items; + } + onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; + async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + const currentScope = checkUri(scope) as SetEnvironmentScope; + await waitForEnvManager( + currentScope ? (currentScope instanceof Uri ? [currentScope] : currentScope) : undefined, + ); + return this.envManagers.setEnvironment(currentScope, environment); + } + async getEnvironment(scope: GetEnvironmentScope): Promise { + const currentScope = checkUri(scope) as GetEnvironmentScope; + await waitForEnvManager(currentScope ? [currentScope] : undefined); + return this.envManagers.getEnvironment(currentScope); + } + onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; + async resolveEnvironment(context: ResolveEnvironmentContext): Promise { + await waitForAllEnvManagers(); + const projects = this.projectManager.getProjects(); + const projectEnvManagers: InternalEnvironmentManager[] = []; + projects.forEach((p) => { + const manager = this.envManagers.getEnvironmentManager(p.uri); + if (manager && !projectEnvManagers.includes(manager)) { + projectEnvManagers.push(manager); + } + }); + + return await handlePythonPath(context, this.envManagers.managers, projectEnvManagers); + } + + registerPackageManager(manager: PackageManager, options?: { extensionId?: string }): Disposable { + const disposables: Disposable[] = []; + disposables.push(this.envManagers.registerPackageManager(manager, options)); + if (manager.onDidChangePackages) { + disposables.push(manager.onDidChangePackages((e) => this._onDidChangePackages.fire(e))); + } + return new Disposable(() => disposables.forEach((d) => d.dispose())); + } + async managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise { + await waitForEnvManagerId([context.envId.managerId]); + const manager = this.envManagers.getPackageManager(context); + if (!manager) { + return Promise.reject(new Error('No package manager found')); + } + return manager.manage(context, options); + } + async refreshPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); + const manager = this.envManagers.getPackageManager(context); + if (!manager) { + return Promise.reject(new Error('No package manager found')); + } + return manager.refresh(context); + } + async getPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); + const manager = this.envManagers.getPackageManager(context); + if (!manager) { + return Promise.resolve(undefined); + } + return manager.getPackages(context); + } + onDidChangePackages: Event = this._onDidChangePackages.event; + + async getPackageManagerVersion(environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); + const manager = this.envManagers.getPackageManager(environment); + if (!manager) { + return undefined; + } + return manager.getVersion(environment); + } + + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); + const manager = this.envManagers.getPackageManager(environment); + if (!manager) { + return undefined; + } + return manager.getAvailableVersions(packageName, environment); + } + + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package { + const mgr = this.envManagers.packageManagers.find((m) => m.equals(manager)); + if (!mgr) { + throw new Error('Package manager not found'); + } + const randomStr = Math.random().toString(36).substring(2); + const pkg: PackageId = { + managerId: mgr.id, + environmentId: environment.envId.id, + id: `${info.name}-${randomStr}`, + }; + return new PythonPackageImpl(pkg, info); + } + + addPythonProject(projects: PythonProject | PythonProject[]): void { + this.projectManager.add(projects); + } + removePythonProject(pyWorkspace: PythonProject): void { + this.projectManager.remove(pyWorkspace); + } + getPythonProjects(): readonly PythonProject[] { + return this.projectManager.getProjects(); + } + onDidChangePythonProjects: Event = this._onDidChangePythonProjects.event; + getPythonProject(uri: Uri): PythonProject | undefined { + return this.projectManager.get(checkUri(uri) as Uri); + } + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { + return this.projectCreators.registerPythonProjectCreator(creator); + } + async createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise { + return this.terminalManager.create(environment, options); + } + async runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise { + const terminal = await this.terminalManager.getProjectTerminal( + options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), + environment, + ); + await runInTerminal(environment, terminal, options); + return terminal; + } + async runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise { + const terminal = await this.terminalManager.getDedicatedTerminal( + terminalKey, + options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), + environment, + ); + await runInTerminal(environment, terminal, options); + return Promise.resolve(terminal); + } + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise { + return runAsTask(environment, options); + } + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise { + return runInBackground(environment, options); + } + + onDidChangeEnvironmentVariables: Event = + this._onDidChangeEnvironmentVariables.event; + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }> { + return this.envVarManager.getEnvironmentVariables(checkUri(uri) as Uri, overrides, baseEnvVar); + } +} + +let _deferred = createDeferred(); +export function setPythonApi( + envMgr: EnvironmentManagers, + projectMgr: PythonProjectManager, + projectCreators: ProjectCreators, + terminalManager: TerminalManager, + envVarManager: EnvVarManager, +) { + _deferred.resolve( + new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators, terminalManager, envVarManager), + ); +} + +export function getPythonApi(): Promise { + return _deferred.promise; +} diff --git a/src/internal.api.ts b/src/internal.api.ts index 4426f31a..fa4d8b20 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -1,4 +1,3 @@ -import * as semver from 'semver'; import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, Uri } from 'vscode'; import { CreateEnvironmentOptions, @@ -28,6 +27,7 @@ import { ResolveEnvironmentContext, SetEnvironmentScope, } from './api'; +import type { Pep440Version } from '@renovatebot/pep440'; import { ISSUES_URL } from './common/constants'; import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError'; import { traceWarn } from './common/logging'; @@ -380,11 +380,11 @@ export class InternalPackageManager implements PackageManager { return this.manager === other; } - getVersion(environment: PythonEnvironment): Promise { + getVersion(environment: PythonEnvironment): Promise { return this.manager.getVersion ? this.manager.getVersion(environment) : Promise.resolve(undefined); } - getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { return this.manager.getAvailableVersions ? this.manager.getAvailableVersions(packageName, environment) : Promise.resolve(undefined); diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 551485ad..8afd490a 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -1,4 +1,5 @@ -import * as semver from 'semver'; +import { compare, explain as parse, rcompare } from '@renovatebot/pep440'; +import type { Pep440Version } from '@renovatebot/pep440'; import { CancellationError, Disposable, @@ -133,14 +134,14 @@ export class PipPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } - async getVersion(environment: PythonEnvironment): Promise { + async getVersion(environment: PythonEnvironment): Promise { try { const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); if (useUv) { const result = await runUV(['--version'], undefined, this.log); // "uv X.Y.Z" const match = result.match(/^uv\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? (semver.coerce(match[1]) ?? undefined) : undefined; + return match ? parse(match[1]) ?? undefined : undefined; } const result = await runPython( environment.execInfo?.run?.executable ?? 'python', @@ -150,13 +151,13 @@ export class PipPackageManager implements PackageManager, Disposable { ); // "pip X.Y.Z from /path/to/pip (python X.Y)" const match = result.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? (semver.coerce(match[1]) ?? undefined) : undefined; + return match ? parse(match[1]) ?? undefined : undefined; } catch { return undefined; } } - async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { try { const python = environment.execInfo?.run?.executable; if (!python) { @@ -176,7 +177,7 @@ export class PipPackageManager implements PackageManager, Disposable { // pip >= 21.2.0 - use `pip index versions --json` to get available versions in a machine readable format. const pipVersion = await this.getVersion(environment); - if (pipVersion && semver.gte(pipVersion, '21.2.0')) { + if (pipVersion && compare(pipVersion.public, '21.2.0') >= 0) { const output = await runPython( python, ['-m', 'pip', 'index', 'versions', packageName, '--json'], @@ -187,7 +188,7 @@ export class PipPackageManager implements PackageManager, Disposable { } // pip <= 20.3.4 - use `pip install ==__invalid__` to get available versions from error message. - if (pipVersion && semver.lte(pipVersion, '20.3.4')) { + if (pipVersion && compare(pipVersion.public, '20.3.4') <= 0) { const result = await runProcessCaptureAll( python, ['-m', 'pip', 'install', `${packageName}==__invalid__`], @@ -215,14 +216,15 @@ export class PipPackageManager implements PackageManager, Disposable { * No matching distribution found for ==__invalid__ * ``` */ -export function parsePipInstallVersions(output: string): semver.SemVer[] | undefined { +export function parsePipInstallVersions(output: string): Pep440Version[] | undefined { const match = output.match(/from versions:\s*([^\)]+)\)/); if (match && match[1]) { return match[1] .split(',') .filter((v) => !!v.trim()) - .map((v) => semver.coerce(v.trim()) as semver.SemVer) - .sort((a, b) => semver.rcompare(a, b)); + .map((v) => parse(v.trim())) + .filter((v): v is Pep440Version => v !== null) + .sort((a, b) => rcompare(a.public, b.public)); } } @@ -230,7 +232,7 @@ export function parsePipInstallVersions(output: string): semver.SemVer[] | undef * Parses JSON output from `pip index versions --json`. * Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] } */ -export function parsePipIndexVersionsJson(output: string): semver.SemVer[] | undefined { +export function parsePipIndexVersionsJson(output: string): Pep440Version[] | undefined { // Only capture output between braces const match = output.match(/{[\s\S]*}/); if (!match) { @@ -241,8 +243,9 @@ export function parsePipIndexVersionsJson(output: string): semver.SemVer[] | und if (parsed && Array.isArray(parsed.versions) && parsed.versions.length > 0) { return (parsed.versions as string[]) .filter((v) => !!v.trim()) - .map((v) => semver.coerce(v) as semver.SemVer) - .sort((a, b) => semver.rcompare(a, b)); + .map((v) => parse(v)) + .filter((v): v is Pep440Version => v !== null) + .sort((a, b) => rcompare(a.public, b.public)); } return undefined; } catch { diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index aaf3cdb9..dd4d9f7a 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -1,4 +1,5 @@ -import * as semver from 'semver'; +import { explain as parse, rcompare } from '@renovatebot/pep440'; +import type { Pep440Version } from '@renovatebot/pep440'; import { CancellationError, Disposable, @@ -126,26 +127,27 @@ export class CondaPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } - async getVersion(_environment: PythonEnvironment): Promise { + async getVersion(_environment: PythonEnvironment): Promise { try { const output = await runCondaExecutable(['--version'], this.log); // "conda X.Y.Z" const match = output.match(/conda\s+(\d+\.\d+(?:\.\d+)*)/i); - return match ? (semver.coerce(match[1]) ?? undefined) : undefined; + return match ? parse(match[1]) ?? undefined : undefined; } catch { return undefined; } } - async getAvailableVersions(packageName: string, _environment: PythonEnvironment): Promise { + async getAvailableVersions(packageName: string, _environment: PythonEnvironment): Promise { try { const output = await runCondaExecutable(['search', packageName, '--json'], this.log); const parsed = JSON.parse(output); if (parsed && typeof parsed === 'object' && Array.isArray(parsed[packageName])) { return parsed[packageName] .filter((entry: { version?: string }) => !!entry.version?.trim()) - .map((entry: { version?: string }) => semver.coerce(entry.version) as semver.SemVer) - .sort((a: semver.SemVer, b: semver.SemVer) => semver.rcompare(a, b)); + .map((entry: { version?: string }) => parse(entry.version!)) + .filter((v: Pep440Version | null): v is Pep440Version => v !== null) + .sort((a: Pep440Version, b: Pep440Version) => rcompare(a.public, b.public)); } return undefined; } catch { diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index e9b9c82f..e43c61db 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -1,4 +1,3 @@ -import * as semver from 'semver'; import * as fsapi from 'fs-extra'; import * as path from 'path'; import { @@ -23,6 +22,8 @@ import { PythonEnvironment, PythonEnvironmentApi, } from '../../api'; +import { explain as parse } from '@renovatebot/pep440'; +import type { Pep440Version } from '@renovatebot/pep440'; import { spawnProcess } from '../../common/childProcess.apis'; import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis'; import { PoetryManager } from './poetryManager'; @@ -155,16 +156,16 @@ export class PoetryPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } - async getVersion(_environment: PythonEnvironment): Promise { + async getVersion(_environment: PythonEnvironment): Promise { const poetry = await getPoetry(); if (!poetry) { return undefined; } const versionStr = await getPoetryVersion(poetry); - return versionStr ? semver.coerce(versionStr) ?? undefined : undefined; + return versionStr ? parse(versionStr) ?? undefined : undefined; } - async getAvailableVersions(_packageName: string, _environment: PythonEnvironment): Promise { + async getAvailableVersions(_packageName: string, _environment: PythonEnvironment): Promise { // Poetry doesn't have a native "list available versions" command. // Poetry 2.x supports `poetry search` but it was disabled on PyPI. // Return undefined to indicate this manager doesn't support version listing. diff --git a/src/test/managers/builtin/pipVersions.unit.test.ts b/src/test/managers/builtin/pipVersions.unit.test.ts index 718f5beb..d8d042c6 100644 --- a/src/test/managers/builtin/pipVersions.unit.test.ts +++ b/src/test/managers/builtin/pipVersions.unit.test.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import * as semver from 'semver'; +import { explain } from '@renovatebot/pep440'; import { parsePipIndexVersionsJson } from '../../../managers/builtin/pipManager'; suite('Pip Version Parsing', () => { @@ -7,13 +7,13 @@ suite('Pip Version Parsing', () => { test('parses valid JSON with versions array', () => { const output = JSON.stringify({ name: 'requests', versions: ['2.31.0', '2.30.0', '2.29.0'] }); const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0'].map((v) => semver.coerce(v))); + assert.deepStrictEqual(versions, ['2.31.0', '2.30.0', '2.29.0'].map((v) => explain(v))); }); test('parses output with a single version', () => { const output = JSON.stringify({ name: 'my-package', versions: ['1.0.0'] }); const versions = parsePipIndexVersionsJson(output); - assert.deepStrictEqual(versions, [semver.coerce('1.0.0')]); + assert.deepStrictEqual(versions, [explain('1.0.0')]); }); test('returns undefined for empty versions array', () => { From f47f4f3bdd96a2138ca5933c4345a9ba3950c65f Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 11 May 2026 17:00:31 -0700 Subject: [PATCH 12/15] Add formatInstalSpec for custom package-version strings used by different package managers --- api/src/main.ts | 13 +++++++++++++ src/api.ts | 13 +++++++++++++ src/features/envCommands.ts | 2 +- src/internal.api.ts | 6 ++++++ src/managers/conda/condaPackageManager.ts | 5 +++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/api/src/main.ts b/api/src/main.ts index 361bf962..8fe5e49b 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -709,6 +709,19 @@ export interface PackageManager { * or `undefined` if this manager does not support version listing. */ getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + + /** + * Formats a versioned install specification for this package manager. + * + * Different package managers use different syntax (e.g. pip uses `name==version`, + * conda uses `name=version`). Implement this method to return the correct format. + * When absent, callers should default to `name==version`. + * + * @param packageName - The name of the package. + * @param version - The version string. + * @returns The install specification string (e.g. `"requests==2.31.0"` or `"requests=2.31.0"`). + */ + formatInstallSpec?(packageName: string, version: string): string; } /** diff --git a/src/api.ts b/src/api.ts index dc0e8ff6..1b6cbf49 100644 --- a/src/api.ts +++ b/src/api.ts @@ -703,6 +703,19 @@ export interface PackageManager { * or `undefined` if this manager does not support version listing. */ getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; + + /** + * Formats a versioned install specification for this package manager. + * + * Different package managers use different syntax (e.g. pip uses `name==version`, + * conda uses `name=version`). Implement this method to return the correct format. + * When absent, callers should default to `name==version`. + * + * @param packageName - The name of the package. + * @param version - The version string. + * @returns The install specification string (e.g. `"requests==2.31.0"` or `"requests=2.31.0"`). + */ + formatInstallSpec?(packageName: string, version: string): string; } /** diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index f1064e29..676ad419 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -370,7 +370,7 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir } await packageManager.manage(environment, { - install: [`${pkg.name}==${version}`], + install: [packageManager.formatInstallSpec(pkg.name, version)], uninstall: [], }); } diff --git a/src/internal.api.ts b/src/internal.api.ts index fa4d8b20..83860c9f 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -389,6 +389,12 @@ export class InternalPackageManager implements PackageManager { ? this.manager.getAvailableVersions(packageName, environment) : Promise.resolve(undefined); } + + formatInstallSpec(packageName: string, version: string): string { + return this.manager.formatInstallSpec + ? this.manager.formatInstallSpec(packageName, version) + : `${packageName}==${version}`; + } } export interface PythonProjectManager extends Disposable { diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index dd4d9f7a..53430ad7 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -127,6 +127,11 @@ export class CondaPackageManager implements PackageManager, Disposable { return this.packages.get(environment.envId.id); } + formatInstallSpec(packageName: string, version: string): string { + // conda match spec syntax uses a single `=` for version pinning + return `${packageName}=${version}`; + } + async getVersion(_environment: PythonEnvironment): Promise { try { const output = await runCondaExecutable(['--version'], this.log); From 28850fbd973fab3a9b00d3b6396d4ec2ab9f6879 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 12 May 2026 09:47:13 -0700 Subject: [PATCH 13/15] Use poetry package[at]version syntax --- src/managers/poetry/poetryPackageManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts index e43c61db..cb3a9e04 100644 --- a/src/managers/poetry/poetryPackageManager.ts +++ b/src/managers/poetry/poetryPackageManager.ts @@ -172,6 +172,11 @@ export class PoetryPackageManager implements PackageManager, Disposable { return undefined; } + formatInstallSpec(packageName: string, version: string): string { + // Poetry uses `package@version` syntax for version-pinned installs + return `${packageName}@${version}`; + } + dispose(): void { this._onDidChangePackages.dispose(); this.packages.clear(); From e6be5b51e616b4700efc99869a75bf235d77cb3d Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 12 May 2026 10:00:07 -0700 Subject: [PATCH 14/15] Address pr comments --- api/src/main.ts | 8 ++-- package-lock.json | 15 ------ package.json | 2 - src/api.ts | 8 ++-- src/features/envCommands.ts | 2 + src/managers/builtin/pipManager.ts | 3 +- .../managers/builtin/pipVersions.unit.test.ts | 46 ++++++++++++++++++- 7 files changed, 57 insertions(+), 27 deletions(-) diff --git a/api/src/main.ts b/api/src/main.ts index 8fe5e49b..2bd35cac 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -697,7 +697,7 @@ export interface PackageManager { /** * Returns the version of the underlying package management tool (e.g., pip, conda). * @param environment - The Python environment context. - * @returns A promise that resolves to a SemVer object, or `undefined` if not available. + * @returns A promise that resolves to a {@link Pep440Version} object, or `undefined` if not available. */ getVersion?(environment: PythonEnvironment): Promise; @@ -705,7 +705,7 @@ export interface PackageManager { * Retrieves the list of available versions for a given package. * @param packageName - The name of the package to look up. * @param environment - The Python environment context for the lookup. - * @returns A promise that resolves to an array of version strings (newest first), + * @returns A promise that resolves to an array of {@link Pep440Version} objects (newest first), * or `undefined` if this manager does not support version listing. */ getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; @@ -1095,7 +1095,7 @@ export interface Pep440VersionApi { * Get the version of the package manager tool associated with the given environment. * * @param environment The Python Environment whose package manager version is requested. - * @returns The SemVer version of the package manager tool, or `undefined` if not available. + * @returns The {@link Pep440Version} of the package manager tool, or `undefined` if not available. */ getPackageManagerVersion(environment: PythonEnvironment): Promise; @@ -1105,7 +1105,7 @@ export interface Pep440VersionApi { * * @param packageName The name of the package. * @param environment The Python Environment context for the lookup. - * @returns An array of version strings (newest first), or `undefined` if not supported. + * @returns An array of {@link Pep440Version} objects (newest first), or `undefined` if not supported. */ getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } diff --git a/package-lock.json b/package-lock.json index 0591f7ab..97410c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", - "semver": "^7.8.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" @@ -24,7 +23,6 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "^22.15.1", - "@types/semver": "^7.7.1", "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/vscode": "^1.99.0", @@ -729,13 +727,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sinon": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", @@ -6481,12 +6472,6 @@ "undici-types": "~6.21.0" } }, - "@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true - }, "@types/sinon": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", diff --git a/package.json b/package.json index 4c27f565..e1dbdf2e 100644 --- a/package.json +++ b/package.json @@ -712,7 +712,6 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "^22.15.1", - "@types/semver": "^7.7.1", "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", "@types/vscode": "^1.99.0", @@ -739,7 +738,6 @@ "@vscode/test-cli": "^0.0.10", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", - "semver": "^7.8.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", "which": "^4.0.0" diff --git a/src/api.ts b/src/api.ts index 1b6cbf49..3d32fbc6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -691,7 +691,7 @@ export interface PackageManager { /** * Returns the version of the underlying package management tool (e.g., pip, conda). - * @returns A promise that resolves to a SemVer object, or `undefined` if not available. + * @returns A promise that resolves to a {@link Pep440Version} object, or `undefined` if not available. */ getVersion?(environment: PythonEnvironment): Promise; @@ -699,7 +699,7 @@ export interface PackageManager { * Retrieves the list of available versions for a given package. * @param packageName - The name of the package to look up. * @param environment - The Python environment context for the lookup. - * @returns A promise that resolves to an array of version strings (newest first), + * @returns A promise that resolves to an array of {@link Pep440Version} objects (newest first), * or `undefined` if this manager does not support version listing. */ getAvailableVersions?(packageName: string, environment: PythonEnvironment): Promise; @@ -1089,7 +1089,7 @@ export interface Pep440VersionApi { * Get the version of the package manager tool associated with the given environment. * * @param environment The Python Environment whose package manager version is requested. - * @returns The SemVer version of the package manager tool, or `undefined` if not available. + * @returns The {@link Pep440Version} of the package manager tool, or `undefined` if not available. */ getPackageManagerVersion(environment: PythonEnvironment): Promise; @@ -1099,7 +1099,7 @@ export interface Pep440VersionApi { * * @param packageName The name of the package. * @param environment The Python Environment context for the lookup. - * @returns An array of version strings (newest first), or `undefined` if not supported. + * @returns An array of {@link Pep440Version} objects (newest first), or `undefined` if not supported. */ getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise; } diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 676ad419..dffee6a6 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -373,6 +373,8 @@ export async function handlePackageVersionManagement(context: unknown, em: Envir install: [packageManager.formatInstallSpec(pkg.name, version)], uninstall: [], }); + } else { + traceError(`Invalid context for manage package version command: ${typeof context}`); } } diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 8afd490a..8f5b7275 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -219,12 +219,13 @@ export class PipPackageManager implements PackageManager, Disposable { export function parsePipInstallVersions(output: string): Pep440Version[] | undefined { const match = output.match(/from versions:\s*([^\)]+)\)/); if (match && match[1]) { - return match[1] + const versions = match[1] .split(',') .filter((v) => !!v.trim()) .map((v) => parse(v.trim())) .filter((v): v is Pep440Version => v !== null) .sort((a, b) => rcompare(a.public, b.public)); + return versions.length > 0 ? versions : undefined; } } diff --git a/src/test/managers/builtin/pipVersions.unit.test.ts b/src/test/managers/builtin/pipVersions.unit.test.ts index d8d042c6..962f1de0 100644 --- a/src/test/managers/builtin/pipVersions.unit.test.ts +++ b/src/test/managers/builtin/pipVersions.unit.test.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { explain } from '@renovatebot/pep440'; -import { parsePipIndexVersionsJson } from '../../../managers/builtin/pipManager'; +import { parsePipIndexVersionsJson, parsePipInstallVersions } from '../../../managers/builtin/pipManager'; suite('Pip Version Parsing', () => { suite('parsePipIndexVersionsJson', () => { @@ -33,4 +33,48 @@ suite('Pip Version Parsing', () => { assert.strictEqual(versions, undefined); }); }); + + suite('parsePipInstallVersions', () => { + test('parses versions from pip error output', () => { + const output = `Collecting requests==__invalid__\n Could not find a version that satisfies the requirement requests==__invalid__ (from versions: 2.31.0, 2.30.0, 2.28.2)\n No matching distribution found for requests==__invalid__`; + const versions = parsePipInstallVersions(output); + assert.ok(versions); + assert.strictEqual(versions!.length, 3); + assert.strictEqual(versions![0].public, '2.31.0'); + assert.strictEqual(versions![1].public, '2.30.0'); + assert.strictEqual(versions![2].public, '2.28.2'); + }); + + test('handles PEP 440 pre/post/dev versions', () => { + const output = `Could not find a version that satisfies the requirement pkg==__invalid__ (from versions: 1.0.0, 1.0.0a1, 1.0.0.post1, 1.0.0.dev1)\nNo matching distribution found for pkg==__invalid__`; + const versions = parsePipInstallVersions(output); + assert.ok(versions); + assert.ok(versions!.length === 4); + // newest first: 1.0.0.post1 > 1.0.0 > 1.0.0a1 > 1.0.0.dev1 + assert.strictEqual(versions![0].public, '1.0.0.post1'); + assert.strictEqual(versions![1].public, '1.0.0'); + assert.strictEqual(versions![2].public, '1.0.0a1'); + assert.strictEqual(versions![3].public, '1.0.0.dev1'); + }); + + test('returns undefined when pip reports "from versions: none"', () => { + const output = `Could not find a version that satisfies the requirement nonexistent-pkg==__invalid__ (from versions: none)\nNo matching distribution found for nonexistent-pkg==__invalid__`; + const versions = parsePipInstallVersions(output); + assert.strictEqual(versions, undefined); + }); + + test('returns undefined when output has no version list', () => { + const versions = parsePipInstallVersions('some unrelated error output'); + assert.strictEqual(versions, undefined); + }); + + test('handles surrounding stderr noise', () => { + const output = `WARNING: some deprecation warning\nERROR: Could not find a version that satisfies the requirement pkg==__invalid__ (from versions: 1.2.3, 1.2.2)\nERROR: No matching distribution found`; + const versions = parsePipInstallVersions(output); + assert.ok(versions); + assert.strictEqual(versions!.length, 2); + assert.strictEqual(versions![0].public, '1.2.3'); + }); + }); }); + From e654a7cb6677909878b0402b743cc517b44ff5b2 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Sat, 16 May 2026 12:54:34 -0700 Subject: [PATCH 15/15] Add python version --- src/managers/builtin/pipManager.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/managers/builtin/pipManager.ts b/src/managers/builtin/pipManager.ts index 8f5b7275..81be29cf 100644 --- a/src/managers/builtin/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -1,5 +1,5 @@ -import { compare, explain as parse, rcompare } from '@renovatebot/pep440'; import type { Pep440Version } from '@renovatebot/pep440'; +import { compare, explain as parse, rcompare } from '@renovatebot/pep440'; import { CancellationError, Disposable, @@ -141,7 +141,7 @@ export class PipPackageManager implements PackageManager, Disposable { const result = await runUV(['--version'], undefined, this.log); // "uv X.Y.Z" const match = result.match(/^uv\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? parse(match[1]) ?? undefined : undefined; + return match ? (parse(match[1]) ?? undefined) : undefined; } const result = await runPython( environment.execInfo?.run?.executable ?? 'python', @@ -151,13 +151,16 @@ export class PipPackageManager implements PackageManager, Disposable { ); // "pip X.Y.Z from /path/to/pip (python X.Y)" const match = result.match(/^pip\s+(\d+\.\d+(?:\.\d+)*)/); - return match ? parse(match[1]) ?? undefined : undefined; + return match ? (parse(match[1]) ?? undefined) : undefined; } catch { return undefined; } } - async getAvailableVersions(packageName: string, environment: PythonEnvironment): Promise { + async getAvailableVersions( + packageName: string, + environment: PythonEnvironment, + ): Promise { try { const python = environment.execInfo?.run?.executable; if (!python) { @@ -168,7 +171,17 @@ export class PipPackageManager implements PackageManager, Disposable { const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath); if (useUv) { const output = await runUV( - ['tool', 'run', 'pip', 'index', 'versions', packageName, '--json'], + [ + 'tool', + 'run', + 'pip', + 'index', + 'versions', + packageName, + '--json', + '--python-version', + environment.version, + ], undefined, this.log, ); @@ -180,7 +193,7 @@ export class PipPackageManager implements PackageManager, Disposable { if (pipVersion && compare(pipVersion.public, '21.2.0') >= 0) { const output = await runPython( python, - ['-m', 'pip', 'index', 'versions', packageName, '--json'], + ['-m', 'pip', 'index', 'versions', packageName, '--json', '--python-version', environment.version], undefined, this.log, );