Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/managers/builtin/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export async function runPython(
proc.stderr?.on('data', (data) => {
const s = data.toString('utf-8');
builder += s;
log?.append(`python: ${s}`);
log?.append(s);
});
proc.on('close', () => {
resolve(builder);
Expand Down
34 changes: 0 additions & 34 deletions src/managers/builtin/pipListUtils.ts

This file was deleted.

242 changes: 138 additions & 104 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,66 @@ 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;
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
if (toUninstall.length > 0) {
const uninstallCmd: PipUninstallCommand | UvUninstallCommand = await createPipOrUvCommand(
manageCommandOptions,
environment.environmentPath.fsPath,
PipUninstallCommand,
UvUninstallCommand,
);
const packages = parsePackageSpecs(toUninstall);
await withProgress(
{ 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; do not surface an error.
return;
}
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 +164,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 +187,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 +208,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));
} catch {
return undefined;
}
Expand All @@ -205,38 +255,22 @@ 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 using `pip list --not-required` or `uv pip list --not-required`.
*
* Note: These commands return packages with no installed dependents (leaf packages), not 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