diff --git a/lib/modules/manager/pip-compile/extract.spec.ts b/lib/modules/manager/pip-compile/extract.spec.ts index 786fc48a1a9007cb57ba0b762ef516f9b9184ec0..9e9f55720f089a15cad17237729ff95985450ca0 100644 --- a/lib/modules/manager/pip-compile/extract.spec.ts +++ b/lib/modules/manager/pip-compile/extract.spec.ts @@ -159,6 +159,79 @@ describe('modules/manager/pip-compile/extract', () => { expect(packageFiles).toBeNull(); }); + it('return sorted package files', async () => { + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=4.txt 3.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('-r 2.txt\nfoo'); + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=2.txt 1.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('foo'); + + const lockFiles = ['4.txt', '2.txt']; + const packageFiles = await extractAllPackageFiles({}, lockFiles); + expect(packageFiles).toBeDefined(); + expect(packageFiles?.map((p) => p.packageFile)).toEqual(['1.in', '3.in']); + expect(packageFiles?.map((p) => p.lockFiles!.pop())).toEqual([ + '2.txt', + '4.txt', + ]); + }); + + it('return sorted package files with constraint in file', async () => { + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=4.txt 3.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('-c 2.txt\nfoo'); + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=2.txt 1.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('foo'); + + const lockFiles = ['4.txt', '2.txt']; + const packageFiles = await extractAllPackageFiles({}, lockFiles); + expect(packageFiles).toBeDefined(); + expect(packageFiles?.map((p) => p.packageFile)).toEqual(['1.in', '3.in']); + expect(packageFiles?.map((p) => p.lockFiles!.pop())).toEqual([ + '2.txt', + '4.txt', + ]); + }); + + it('return sorted package files with constraint in command', async () => { + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile( + 'pip-compile --constraint=2.txt --output-file=4.txt 3.in', + ['foo==1.0.1'], + ), + ); + fs.readLocalFile.mockResolvedValueOnce('foo'); + fs.readLocalFile.mockResolvedValueOnce( + getSimpleRequirementsFile('pip-compile --output-file=2.txt 1.in', [ + 'foo==1.0.1', + ]), + ); + fs.readLocalFile.mockResolvedValueOnce('foo'); + + const lockFiles = ['4.txt', '2.txt']; + const packageFiles = await extractAllPackageFiles({}, lockFiles); + expect(packageFiles).toBeDefined(); + expect(packageFiles?.map((p) => p.packageFile)).toEqual(['1.in', '3.in']); + expect(packageFiles?.map((p) => p.lockFiles!.pop())).toEqual([ + '2.txt', + '4.txt', + ]); + }); + it('adds lockedVersion to deps in package file', async () => { fs.readLocalFile.mockResolvedValueOnce( getSimpleRequirementsFile( diff --git a/lib/modules/manager/pip-compile/extract.ts b/lib/modules/manager/pip-compile/extract.ts index 72dc9c40c1b24e96e094497bba8871c975e69e46..40296061223521a157de19782ec68ed93cd90977 100644 --- a/lib/modules/manager/pip-compile/extract.ts +++ b/lib/modules/manager/pip-compile/extract.ts @@ -1,3 +1,4 @@ +import { Graph } from 'graph-data-structure'; import { logger } from '../../../logger'; import { readLocalFile } from '../../../util/fs'; import { normalizeDepName } from '../../datasource/pypi/common'; @@ -80,8 +81,6 @@ export async function extractAllPackageFiles( } lockFileArgs.set(fileMatch, compileArgs); for (const constraint in compileArgs.constraintsFiles) { - // TODO(not7cd): handle constraints - /* istanbul ignore next */ depsBetweenFiles.push({ sourceFile: constraint, outputFile: fileMatch, @@ -130,6 +129,24 @@ export async function extractAllPackageFiles( config, ); if (packageFileContent) { + if (packageFileContent.managerData?.requirementsFiles) { + for (const file of packageFileContent.managerData.requirementsFiles) { + depsBetweenFiles.push({ + sourceFile: file, + outputFile: packageFile, + type: 'requirement', + }); + } + } + if (packageFileContent.managerData?.constraintsFiles) { + for (const file of packageFileContent.managerData.constraintsFiles) { + depsBetweenFiles.push({ + sourceFile: file, + outputFile: packageFile, + type: 'requirement', + }); + } + } for (const dep of packageFileContent.deps) { const lockedVersion = lockedDeps?.find( (lockedDep) => @@ -158,13 +175,38 @@ export async function extractAllPackageFiles( } } } - // TODO(not7cd): sort by requirement layering (-r -c within .in files) if (packageFiles.size === 0) { return null; } + const result: PackageFile[] = []; + const graph: ReturnType<typeof Graph> = Graph(); + depsBetweenFiles.forEach(({ sourceFile, outputFile }) => { + graph.addEdge(sourceFile, outputFile); + }); + const sorted = graph.topologicalSort(); + for (const file of sorted) { + if (packageFiles.has(file)) { + const packageFile = packageFiles.get(file)!; + const sortedLockFiles = []; + // TODO(not7cd): this needs better test case + for (const lockFile of packageFile.lockFiles!) { + if (sorted.includes(lockFile)) { + sortedLockFiles.push(lockFile); + } + } + packageFile.lockFiles = sortedLockFiles; + result.push(packageFile); + } + } + // istanbul ignore if: should never happen + if (result.length !== packageFiles.size) { + throw new Error( + 'pip-compile: topological sort failed to include all package files', + ); + } logger.debug( 'pip-compile: dependency graph:\n' + generateMermaidGraph(depsBetweenFiles, lockFileArgs), ); - return Array.from(packageFiles.values()); + return result; }