Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cfe1e49
Add package manager command smoke tests
edvilme Jul 28, 2026
b18db3d
Adopt package manager command classes
edvilme Jul 28, 2026
a135bc8
Parse pep440 versions
edvilme Jul 28, 2026
e292a5d
Address #1686 review comments
edvilme Jul 31, 2026
d82423c
Add parametrized package manager roundtrip integration test
edvilme Jul 31, 2026
dfa4f9a
test: exercise getPackageAvailableVersions in roundtrip
edvilme Jul 31, 2026
fb1bd18
test: skip roundtrip for managers that cannot install the test package
edvilme Aug 2, 2026
01575b1
test: use flask (universal + transitive deps) for package-manager rou…
edvilme Aug 3, 2026
7e9c7a3
test: restrict package roundtrip to isolated environments
edvilme Aug 6, 2026
0f2ac6f
fix: preserve Poetry command working directory
edvilme Aug 6, 2026
e4c7f10
fix: reject pip management for Python 2
edvilme Aug 6, 2026
39f8559
test: restore package command parser coverage
edvilme Aug 6, 2026
29c2d41
fix: address remaining package manager review feedback
edvilme Aug 6, 2026
0a8c3ad
test: use disposable package manager environments
edvilme Aug 7, 2026
d59560d
test: remove package manager tests from refactor
edvilme Aug 12, 2026
9edd9ef
Merge branch 'main' into package-manager-command-adoption
edvilme Aug 17, 2026
29c5b74
Merge origin/main into package-manager-command-adoption
edvilme Aug 18, 2026
5053e1a
Merge branch 'main' into package-manager-command-adoption
edvilme Aug 18, 2026
4b4b292
Address package manager review feedback
edvilme Aug 18, 2026
d2c0993
Preserve Poetry package manager behavior
edvilme Aug 18, 2026
850bc29
Merge branch 'main' into package-manager-command-adoption
edvilme Aug 18, 2026
b2eff5a
Fix Conda package list error caching
edvilme Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions src/managers/builtin/pipListUtils.ts

This file was deleted.

255 changes: 149 additions & 106 deletions src/managers/builtin/pipPackageManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Pep440Version } from '@renovatebot/pep440';
import { compare, explain as parse, rcompare } from '@renovatebot/pep440';
import { compare, explain as parse } from '@renovatebot/pep440';
import {
CancellationError,
Disposable,
Expand All @@ -21,10 +21,26 @@ import {
PythonEnvironment,
PythonEnvironmentApi,
} from '../../api';
import { withProgress } from '../../common/window.apis';
import { CommandConstructorOptions } from '../base/commands/index';
import { updatePackagesAndNotify } from '../common/packageChanges';
import { runPython, runUV, shouldUseUv } from './helpers';
import { createPipOrUvCommand } from './commands/factory';
import {
PipAvailableVersionsCommand,
PipInstallCommand,
PipListCommand,
PipListDirectNamesCommand,
PipUninstallCommand,
PipVersionCommand,
UvAvailableVersionsCommand,
UvInstallCommand,
UvListCommand,
UvListDirectNamesCommand,
UvUninstallCommand,
UvVersionCommand,
} from './commands/index';
import { getWorkspacePackagesToInstall } from './pipUtils';
import { managePackages, normalizePackageName, refreshPipDirectPackageNames, refreshPipPackages } from './utils';
import { parsePackageSpecs } from './utils';
import { VenvManager } from './venvManager';

export class PipPackageManager implements PackageManager, Disposable {
Expand Down Expand Up @@ -65,43 +81,72 @@ export class PipPackageManager implements PackageManager, Disposable {
}
}

const manageOptions = {
...options,
install: toInstall,
uninstall: toUninstall,
};
await window.withProgress(
{
location: ProgressLocation.Notification,
title: 'Installing packages',
cancellable: true,
},
async (_progress, token) => {
try {
await managePackages(environment, manageOptions, this, token);
await updatePackagesAndNotify(
this,
environment,
this.packages.get(environment.envId.id),
(changes) => {
this._onDidChangePackages.fire({ environment, manager: this, changes });
},
);
} catch (e) {
if (e instanceof CancellationError) {
throw e;
}
this.log.error('Error managing packages', e);
setImmediate(async () => {
const result = await window.showErrorMessage('Error managing packages', 'View Output');
if (result === 'View Output') {
this.log.show();
}
});
throw e;
if (environment.version.startsWith('2.')) {
throw new Error('Python 2.* is not supported (deprecated)');
}

try {
const pythonExecutable = environment.execInfo?.run?.executable;
if (!pythonExecutable) {
throw new Error('Unable to determine Python executable path');
}

// Centralize command options for install/uninstall operations
const manageCommandOptions: CommandConstructorOptions = {
pythonExecutable,
log: this.log,
};

// Execute uninstall if needed
Comment thread
edvilme marked this conversation as resolved.
Outdated
if (toUninstall.length > 0) {
const uninstallCmd: PipUninstallCommand | UvUninstallCommand = await createPipOrUvCommand(
manageCommandOptions,
environment.environmentPath.fsPath,
PipUninstallCommand,
UvUninstallCommand,
);
const packages = parsePackageSpecs(toUninstall);
await withProgress(
Comment thread
edvilme marked this conversation as resolved.
Outdated
{ location: ProgressLocation.Notification, title: 'Installing packages', cancellable: true },
(_progress, token) => uninstallCmd.execute({ packages, cancellationToken: token }),
);
}

// Execute install if needed
if (toInstall.length > 0) {
const installCmd: PipInstallCommand | UvInstallCommand = await createPipOrUvCommand(
manageCommandOptions,
environment.environmentPath.fsPath,
PipInstallCommand,
UvInstallCommand,
);
const packages = parsePackageSpecs(toInstall);
await withProgress(
{ location: ProgressLocation.Notification, title: 'Installing packages', cancellable: true },
(_progress, token) =>
installCmd.execute({ packages, upgrade: options.upgrade, cancellationToken: token }),
);
}

await updatePackagesAndNotify(this, environment, this.packages.get(environment.envId.id), (changes) => {
this._onDidChangePackages.fire({ environment, manager: this, changes });
});
} catch (e) {
if (e instanceof CancellationError) {
// Cancellation is a normal control-flow exit; skip the user-facing error
// UI/logging, but rethrow so callers can distinguish cancel from failure
// (e.g. venv creation sets pkgInstallationCancelled by catching CancellationError).
throw e;
}
this.log.error('Error managing packages', e);
setImmediate(async () => {
const result = await window.showErrorMessage('Error managing packages', 'View Output');
if (result === 'View Output') {
this.log.show();
}
},
);
});
throw e;
}
}

async refresh(environment: PythonEnvironment): Promise<Package[] | undefined> {
Expand All @@ -125,7 +170,20 @@ export class PipPackageManager implements PackageManager, Disposable {

async getPackages(environment: PythonEnvironment, options?: GetPackagesOptions): Promise<Package[] | undefined> {
if (options?.skipCache || !this.packages.has(environment.envId.id)) {
const data = await refreshPipPackages(environment, this.log);
const pythonExecutable = environment.execInfo?.run?.executable;
if (!pythonExecutable) {
return undefined;
}
const listCmd: PipListCommand | UvListCommand = await createPipOrUvCommand(
{
pythonExecutable,
log: this.log,
},
environment.environmentPath.fsPath,
PipListCommand,
UvListCommand,
);
const data = await listCmd.execute();
const packages = (data ?? []).map((pkg) => this.api.createPackageItem(pkg, environment, this));
this.packages.set(environment.envId.id, packages);
return packages;
Expand All @@ -135,22 +193,17 @@ export class PipPackageManager implements PackageManager, Disposable {

async getVersion(environment: PythonEnvironment): Promise<Pep440Version | undefined> {
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 ? (parse(match[1]) ?? undefined) : undefined;
const pythonExecutable = environment.execInfo?.run?.executable;
if (!pythonExecutable) {
return undefined;
}
const result = await runPython(
environment.execInfo?.run?.executable ?? 'python',
['-m', 'pip', '--version'],
undefined,
this.log,
const versionCmd: PipVersionCommand | UvVersionCommand = await createPipOrUvCommand(
{ pythonExecutable, log: this.log },
environment.environmentPath.fsPath,
PipVersionCommand,
UvVersionCommand,
);
// "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 await versionCmd.execute();
} catch {
return undefined;
}
Expand All @@ -161,39 +214,42 @@ export class PipPackageManager implements PackageManager, Disposable {
packageName: string,
): Promise<Pep440Version[] | undefined> {
try {
const python = environment.execInfo?.run?.executable;
if (!python) {
const pythonExecutable = environment.execInfo?.run?.executable;
if (!pythonExecutable) {
return undefined;
}

const baseVersion = parse(environment.version)?.base_version;
// Normalize versions like '3.13.1.final.0' (Python's sys.version_info format) to '3.13.1'
// before parsing, since pep440 only accepts valid PEP 440 version strings.
const versionMatch = (environment.version ?? '').match(/^(\d+(?:\.\d+)*)/);
const normalizedVersion = versionMatch?.[1] ?? '';
const baseVersion = parse(normalizedVersion)?.base_version;
if (!baseVersion) {
return undefined;
}
// uv - Run pip via `uv tool run pip`
const useUv = await shouldUseUv(this.log, environment.environmentPath.fsPath);
if (useUv) {
const output = await runUV(
['tool', 'run', 'pip', 'index', 'versions', packageName, '--json', '--python-version', baseVersion],
undefined,
this.log,
);
return parsePipIndexVersionsJson(output);
}

// pip >= 21.2.0 - use `pip index versions <package> --json` to get available versions in a machine readable format.
const pipVersion = await this.getVersion(environment);
if (pipVersion && compare(pipVersion.public, '21.2.0') >= 0) {
const output = await runPython(
python,
['-m', 'pip', 'index', 'versions', packageName, '--json', '--python-version', baseVersion],
undefined,
this.log,
const availableVersionsCmd: PipAvailableVersionsCommand | UvAvailableVersionsCommand =
await createPipOrUvCommand(
{ pythonExecutable, log: this.log },
environment.environmentPath.fsPath,
PipAvailableVersionsCommand,
UvAvailableVersionsCommand,
);
return parsePipIndexVersionsJson(output);

// For pip < 21.2.0, check version first
if (availableVersionsCmd instanceof PipAvailableVersionsCommand) {
const pipVersion = await this.getVersion(environment);
if (!pipVersion || compare(pipVersion.public, '21.2.0') < 0) {
// pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists.
return undefined;
}
}

// pip <= 20.3.4 - version picking is undefined; no reliable machine-readable API exists.
const versions = await availableVersionsCmd.execute({
packageName,
pythonVersion: baseVersion,
});
return versions.sort((a, b) => compare(b.public, a.public));
Comment thread
edvilme marked this conversation as resolved.
} catch {
return undefined;
}
Expand All @@ -205,38 +261,25 @@ export class PipPackageManager implements PackageManager, Disposable {
}

/**
* Returns direct (non-transitive) package names using `pip list --not-required` or `uv pip tree --depth=0`.
* Returns direct (non-transitive) package names.
*
* pip uses `pip list --format=json --not-required`; uv uses `uv pip tree --depth=0`.
*
Comment thread
edvilme marked this conversation as resolved.
* Note: These commands return packages with no installed dependents (leaf packages), not packages
* the user explicitly installed. pip/uv do not track install intent.
* Note: These return top-level packages (no installed dependents / roots of the
* dependency tree), not necessarily packages the user explicitly installed. pip/uv
* do not track install intent.
*/
async getDirectPackageNames(environment: PythonEnvironment): Promise<Set<string> | undefined> {
const data = await refreshPipDirectPackageNames(environment, this.log);
return data ? new Set(data.map(normalizePackageName)) : undefined;
}
}

/**
* Parses JSON output from `pip index versions <package> --json`.
* Expected format: { "name": "...", "versions": ["1.2.3", "1.2.2", ...] }
*/
export function parsePipIndexVersionsJson(output: string): Pep440Version[] | 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 as string[])
.filter((v) => !!v.trim())
.map((v) => parse(v.trim()))
.filter((v): v is Pep440Version => v !== null)
.sort((a, b) => rcompare(a.public, b.public));
const pythonExecutable = environment.execInfo?.run?.executable;
if (!pythonExecutable) {
return undefined;
}
return undefined;
} catch {
return undefined;
const listDirectNamesCmd: PipListDirectNamesCommand | UvListDirectNamesCommand = await createPipOrUvCommand(
{ pythonExecutable, log: this.log },
environment.environmentPath.fsPath,
PipListDirectNamesCommand,
UvListDirectNamesCommand,
);
return listDirectNamesCmd.execute();
}
}
Loading
Loading