Skip to content

Commit

Permalink
feat(pip-compile): support uv pip compile
Browse files Browse the repository at this point in the history
  • Loading branch information
maxbrunet committed Jan 30, 2025
1 parent 545bf31 commit e586242
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --generate-hashes --output-file=requirements.txt --universal requirements.in
attrs==21.2.0 \
--hash=sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1 \
--hash=sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb
# via -r requirements.in
setuptools==59.2.0 \
--hash=sha256:157d21de9d055ab9e8ea3186d91e7f4f865e11f42deafa952d90842671fc2576 \
--hash=sha256:4adde3d1e1c89bde1c643c64d89cdd94cbfd8c75252ee459d4500bccb9c7d05d
# via -r requirements.in
13 changes: 13 additions & 0 deletions lib/modules/manager/pip-compile/artifacts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,19 @@ describe('modules/manager/pip-compile/artifacts', () => {
);
});

it('returns extracted arguments for uv ', () => {
expect(
constructPipCompileCmd(
extractHeaderCommand(
Fixtures.get('requirementsWithUv.txt'),
'subdir/requirements.txt',
),
),
).toBe(
'uv pip compile --generate-hashes --output-file=requirements.txt --universal requirements.in',
);
});

it('returns --no-emit-index-url when credentials are found in PIP_INDEX_URL', () => {
process.env.PIP_INDEX_URL = 'https://user:[email protected]/pypi/simple';
expect(
Expand Down
4 changes: 3 additions & 1 deletion lib/modules/manager/pip-compile/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function constructPipCompileCmd(
compileArgs: PipCompileArgs,
upgradePackages: Upgrade[] = [],
): string {
if (compileArgs.isCustomCommand) {
if (compileArgs.commandType === 'custom') {
throw new Error(
'Detected custom command, header modified or set by CUSTOM_COMPILE_COMMAND',
);
Expand All @@ -68,6 +68,7 @@ export function constructPipCompileCmd(
}
// safeguard against index url leak if not explicitly set by an option
if (
compileArgs.commandType === 'pip-compile' &&
!compileArgs.noEmitIndexUrl &&
!compileArgs.emitIndexUrl &&
haveCredentialsInPipEnvironmentVariables()
Expand Down Expand Up @@ -115,6 +116,7 @@ export async function updateArtifacts({
}
const compileArgs = extractHeaderCommand(existingOutput, outputFileName);
const pythonVersion = extractPythonVersion(
compileArgs.commandType,
existingOutput,
outputFileName,
);
Expand Down
31 changes: 26 additions & 5 deletions lib/modules/manager/pip-compile/common.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mockDeep } from 'jest-mock-extended';
import { hostRules } from '../../../../test/util';
import { logger } from '../../../logger';
import {
allowedPipOptions,
allowedOptions,
extractHeaderCommand,
extractPythonVersion,
getRegistryCredVarsFromPackageFiles,
Expand Down Expand Up @@ -137,8 +137,8 @@ describe('modules/manager/pip-compile/common', () => {
).toEqual(exampleSourceFiles);
});

it.each(allowedPipOptions)(
'returned sourceFiles must not contain options',
it.each(allowedOptions['pip-compile'])(
'returned sourceFiles must not contain options (pip-compile)',
(argument: string) => {
const sourceFiles = extractHeaderCommand(
getCommandInHeader(`pip-compile ${argument}=reqs.txt reqs.in`),
Expand All @@ -149,13 +149,25 @@ describe('modules/manager/pip-compile/common', () => {
},
);

it.each(allowedOptions['uv'])(
'returned sourceFiles must not contain options (uv)',
(argument: string) => {
const sourceFiles = extractHeaderCommand(
getCommandInHeader(`uv pip compile ${argument}=reqs.txt reqs.in`),
'reqs.txt',
).sourceFiles;
expect(sourceFiles).not.toContainEqual(argument);
expect(sourceFiles).toEqual(['reqs.in']);
},
);

it('detects custom command', () => {
expect(
extractHeaderCommand(
getCommandInHeader(`./pip-compile-wrapper reqs.in`),
'reqs.txt',
),
).toHaveProperty('isCustomCommand', true);
).toHaveProperty('commandType', 'custom');
});

it.each([
Expand All @@ -176,14 +188,23 @@ describe('modules/manager/pip-compile/common', () => {
it('extracts Python version from valid header', () => {
expect(
extractPythonVersion(
'pip-compile',
getCommandInHeader('pip-compile reqs.in'),
'reqs.txt',
),
).toBe('3.11');
});

it('returns undefined if version cannot be extracted', () => {
expect(extractPythonVersion('', 'reqs.txt')).toBeUndefined();
expect(
extractPythonVersion('pip-compile', '', 'reqs.txt'),
).toBeUndefined();
});

it('returns undefined if the command type is uv', () => {
expect(
extractPythonVersion('uv', '', 'reqs.txt'),
).toBeUndefined();
});
});

Expand Down
67 changes: 51 additions & 16 deletions lib/modules/manager/pip-compile/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ensureLocalPath } from '../../../util/fs/util';
import * as hostRules from '../../../util/host-rules';
import { regEx } from '../../../util/regex';
import type { PackageFileContent, UpdateArtifactsConfig } from '../types';
import type { PipCompileArgs, SupportedManagers } from './types';
import type { CommandType, PipCompileArgs, SupportedManagers } from './types';

export function getPythonVersionConstraint(
config: UpdateArtifactsConfig,
Expand Down Expand Up @@ -83,24 +83,45 @@ export const constraintLineRegex = regEx(
export const disallowedPipOptions = [
'--no-header', // header is required by this manager
];
export const optionsWithArguments = [
const commonOptionsWithArguments = [
'--output-file',
'--extra',
'--extra-index-url',
];
const pipOptionsWithArguments = [
'--resolver',
'--constraint',
...commonOptionsWithArguments,
];
export const allowedPipOptions = [
const uvOptionsWithArguments = ['--constraints', ...commonOptionsWithArguments];
export const optionsWithArguments = [
...pipOptionsWithArguments,
...uvOptionsWithArguments,
];
const allowedCommonOptions = [
'-v',
'--all-extras',
'--allow-unsafe',
'--generate-hashes',
'--no-emit-index-url',
'--emit-index-url',
'--strip-extras',
'--index-url',
...optionsWithArguments,
];
export const allowedOptions: Record<CommandType, string[]> = {
'pip-compile': [
'--all-extras',
'--allow-unsafe',
'--generate-hashes',
'--no-emit-index-url',
'--strip-extras',
...allowedCommonOptions,
...pipOptionsWithArguments,
],
uv: [
'--no-strip-extras',
'--universal',
...allowedCommonOptions,
...uvOptionsWithArguments,
],
custom: [],
};

// TODO(not7cd): test on all correct headers, even with CUSTOM_COMPILE_COMMAND
export function extractHeaderCommand(
Expand All @@ -118,31 +139,41 @@ export function extractHeaderCommand(
);
const command = compileCommand.groups.command;
const argv = [command];
const isCustomCommand = command !== 'pip-compile';
let commandType: CommandType;
if (command === 'pip-compile') {
commandType = 'pip-compile';
} else if (command === 'uv') {
commandType = 'uv';
} else {
commandType = 'custom';
}
if (compileCommand.groups.arguments) {
argv.push(...split(compileCommand.groups.arguments));
}
logger.debug(
{ fileName, argv, isCustomCommand },
{ fileName, argv, commandType },
`pip-compile: extracted command from header`,
);

const result: PipCompileArgs = {
argv,
command,
isCustomCommand,
commandType,
outputFile: '',
sourceFiles: [],
};
for (const arg of argv.slice(1)) {
if (commandType === 'uv' && ['pip', 'compile'].includes(arg)) {
continue;
}
// TODO(not7cd): check for "--option -- argument" case
if (!arg.startsWith('-')) {
result.sourceFiles.push(arg);
continue;
}
throwForDisallowedOption(arg);
throwForNoEqualSignInOptionWithArgument(arg);
throwForUnknownOption(arg);
throwForUnknownOption(commandType, arg);

if (arg.includes('=')) {
const [option, value] = arg.split('=');
Expand All @@ -153,7 +184,7 @@ export function extractHeaderCommand(
result.extraIndexUrl = result.extraIndexUrl ?? [];
result.extraIndexUrl.push(value);
// TODO: add to secrets? next PR
} else if (option === '--constraint') {
} else if (['--constraint', '--constraints'].includes(option)) {
result.constraintsFiles = result.constraintsFiles ?? [];
result.constraintsFiles.push(value);
} else if (option === '--output-file') {
Expand Down Expand Up @@ -210,9 +241,13 @@ const pythonVersionRegex = regEx(
);

export function extractPythonVersion(
commandType: CommandType,
content: string,
fileName: string,
): string | undefined {
if (commandType === 'uv') {
return;
}
const match = pythonVersionRegex.exec(content);
if (match?.groups === undefined) {
logger.warn(
Expand Down Expand Up @@ -244,14 +279,14 @@ function throwForNoEqualSignInOptionWithArgument(arg: string): void {
);
}
}
function throwForUnknownOption(arg: string): void {
function throwForUnknownOption(commandType: CommandType, arg: string): void {
if (arg.includes('=')) {
const [option] = arg.split('=');
if (allowedPipOptions.includes(option)) {
if (allowedOptions[commandType].includes(option)) {
return;
}
}
if (allowedPipOptions.includes(arg)) {
if (allowedOptions[commandType].includes(arg)) {
return;
}
throw new Error(`Option ${arg} not supported (yet)`);
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/manager/pip-compile/readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Due to limited functionality, the `pip-compile` manager should be considered in an "alpha" stage, which means it's not ready for production use for the majority of end users.
We welcome feedback and bug reports!

The `uv pip compile` command is also supported through this manager in the same fashion as `pip-compile`.

The current implementation has some limitations.
Read the full document before you start using the `pip-compile` manager.

Expand Down
4 changes: 3 additions & 1 deletion lib/modules/manager/pip-compile/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ export type SupportedManagers =
| 'setup-cfg'
| 'pep621';

export type CommandType = 'pip-compile' | 'uv' | 'custom';

export interface PipCompileArgs {
argv: string[]; // all arguments as a list
command: string;
isCustomCommand: boolean;
commandType: CommandType;
constraintsFiles?: string[];
extra?: string[];
allExtras?: boolean;
Expand Down

0 comments on commit e586242

Please sign in to comment.