diff --git a/lib/modules/manager/pip-compile/__fixtures__/requirementsWithUV.txt b/lib/modules/manager/pip-compile/__fixtures__/requirementsWithUV.txt new file mode 100644 index 000000000000000..81270c095e36256 --- /dev/null +++ b/lib/modules/manager/pip-compile/__fixtures__/requirementsWithUV.txt @@ -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 diff --git a/lib/modules/manager/pip-compile/artifacts.spec.ts b/lib/modules/manager/pip-compile/artifacts.spec.ts index 6818e35d0550d69..97e5c1114cdb12d 100644 --- a/lib/modules/manager/pip-compile/artifacts.spec.ts +++ b/lib/modules/manager/pip-compile/artifacts.spec.ts @@ -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:pass@example.com/pypi/simple'; expect( diff --git a/lib/modules/manager/pip-compile/artifacts.ts b/lib/modules/manager/pip-compile/artifacts.ts index 5d8bafb46d4220c..3608808a6a869b4 100644 --- a/lib/modules/manager/pip-compile/artifacts.ts +++ b/lib/modules/manager/pip-compile/artifacts.ts @@ -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', ); @@ -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() @@ -115,6 +116,7 @@ export async function updateArtifacts({ } const compileArgs = extractHeaderCommand(existingOutput, outputFileName); const pythonVersion = extractPythonVersion( + compileArgs.commandType, existingOutput, outputFileName, ); diff --git a/lib/modules/manager/pip-compile/common.spec.ts b/lib/modules/manager/pip-compile/common.spec.ts index a465e9a9bccf5f0..ca069568d149e68 100644 --- a/lib/modules/manager/pip-compile/common.spec.ts +++ b/lib/modules/manager/pip-compile/common.spec.ts @@ -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, @@ -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`), @@ -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([ diff --git a/lib/modules/manager/pip-compile/common.ts b/lib/modules/manager/pip-compile/common.ts index 5a2126198900be7..dc40ab89fc3922e 100644 --- a/lib/modules/manager/pip-compile/common.ts +++ b/lib/modules/manager/pip-compile/common.ts @@ -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, @@ -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, +]; +const uvOptionsWithArguments = ['--constraints', ...commonOptionsWithArguments]; +export const optionsWithArguments = [ + ...pipOptionsWithArguments, + ...uvOptionsWithArguments, ]; -export const allowedPipOptions = [ +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 = { + '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( @@ -118,23 +139,33 @@ 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); @@ -142,7 +173,7 @@ export function extractHeaderCommand( } throwForDisallowedOption(arg); throwForNoEqualSignInOptionWithArgument(arg); - throwForUnknownOption(arg); + throwForUnknownOption(commandType, arg); if (arg.includes('=')) { const [option, value] = arg.split('='); @@ -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') { @@ -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( @@ -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)`); diff --git a/lib/modules/manager/pip-compile/readme.md b/lib/modules/manager/pip-compile/readme.md index cb846fb0a78d42a..1432bb4f4c470a1 100644 --- a/lib/modules/manager/pip-compile/readme.md +++ b/lib/modules/manager/pip-compile/readme.md @@ -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. diff --git a/lib/modules/manager/pip-compile/types.ts b/lib/modules/manager/pip-compile/types.ts index c496e01c886a7db..ad0b9dcaf6c29b0 100644 --- a/lib/modules/manager/pip-compile/types.ts +++ b/lib/modules/manager/pip-compile/types.ts @@ -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;