From 058578f619f085781f0e0860a5e38e09801be838 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Mon, 2 Sep 2024 08:29:27 +0200 Subject: [PATCH] feat(manager/uv): extract packages from lockfile (#31137) Co-authored-by: Michael Kriese --- lib/modules/manager/pep621/extract.spec.ts | 80 +++++++++++++++++++++ lib/modules/manager/pep621/processors/uv.ts | 25 ++++++- lib/modules/manager/pep621/schema.ts | 15 ++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/pep621/extract.spec.ts b/lib/modules/manager/pep621/extract.spec.ts index d86e5d419ad7c1..3eea0c65c2b35f 100644 --- a/lib/modules/manager/pep621/extract.spec.ts +++ b/lib/modules/manager/pep621/extract.spec.ts @@ -411,5 +411,85 @@ describe('modules/manager/pep621/extract', () => { ], }); }); + + it('should resolve lockedVersions from uv.lock', async () => { + fs.readLocalFile.mockResolvedValue( + codeBlock` + version = 1 + requires-python = ">=3.11" + + [[package]] + name = "attrs" + version = "24.2.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, + ] + + [[package]] + name = "pep621-uv" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "attrs" }, + ] + + [package.metadata] + requires-dist = [{ name = "attrs", specifier = ">=24.1.0" }] + `, + ); + + const res = await extractPackageFile( + codeBlock` + [project] + name = "pep621-uv" + version = "0.1.0" + dependencies = ["attrs>=24.1.0"] + requires-python = ">=3.11" + `, + 'pyproject.toml', + ); + expect(res).toMatchObject({ + extractedConstraints: { python: '>=3.11' }, + deps: [ + { + packageName: 'attrs', + depName: 'attrs', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=24.1.0', + lockedVersion: '24.2.0', + }, + ], + }); + }); + + it('should resolve dependencies without locked versions on invalid uv.lock', async () => { + fs.readLocalFile.mockResolvedValue(codeBlock`invalid_toml`); + + const res = await extractPackageFile( + codeBlock` + [project] + name = "pep621-uv" + version = "0.1.0" + dependencies = ["attrs>=24.1.0"] + requires-python = ">=3.11" + `, + 'pyproject.toml', + ); + expect(res).toMatchObject({ + extractedConstraints: { python: '>=3.11' }, + deps: [ + { + packageName: 'attrs', + depName: 'attrs', + datasource: 'pypi', + depType: 'project.dependencies', + currentValue: '>=24.1.0', + }, + ], + }); + }); }); }); diff --git a/lib/modules/manager/pep621/processors/uv.ts b/lib/modules/manager/pep621/processors/uv.ts index f6529e96407c0a..f5b6cd5c7f713f 100644 --- a/lib/modules/manager/pep621/processors/uv.ts +++ b/lib/modules/manager/pep621/processors/uv.ts @@ -5,13 +5,14 @@ import { logger } from '../../../../logger'; import { exec } from '../../../../util/exec'; import type { ExecOptions, ToolConstraint } from '../../../../util/exec/types'; import { getSiblingFileName, readLocalFile } from '../../../../util/fs'; +import { Result } from '../../../../util/result'; import type { PackageDependency, UpdateArtifact, UpdateArtifactsResult, Upgrade, } from '../../types'; -import { type PyProject } from '../schema'; +import { type PyProject, UvLockfileSchema } from '../schema'; import { depTypes, parseDependencyList } from '../utils'; import type { PyProjectProcessor } from './types'; @@ -34,11 +35,31 @@ export class UvProcessor implements PyProjectProcessor { return deps; } - extractLockedVersions( + async extractLockedVersions( project: PyProject, deps: PackageDependency[], packageFile: string, ): Promise { + const lockFileName = getSiblingFileName(packageFile, 'uv.lock'); + const lockFileContent = await readLocalFile(lockFileName, 'utf8'); + if (lockFileContent) { + const { val: lockFileMapping, err } = Result.parse( + lockFileContent, + UvLockfileSchema, + ).unwrap(); + + if (err) { + logger.debug({ packageFile, err }, `Error parsing uv lock file`); + } else { + for (const dep of deps) { + const packageName = dep.packageName; + if (packageName && packageName in lockFileMapping) { + dep.lockedVersion = lockFileMapping[packageName]; + } + } + } + } + return Promise.resolve(deps); } diff --git a/lib/modules/manager/pep621/schema.ts b/lib/modules/manager/pep621/schema.ts index 07204d581670d7..f63791335fdd32 100644 --- a/lib/modules/manager/pep621/schema.ts +++ b/lib/modules/manager/pep621/schema.ts @@ -79,3 +79,18 @@ export const PdmLockfileSchema = Toml.pipe( ), ) .transform((lock) => ({ lock })); + +export const UvLockfileSchema = Toml.pipe( + z.object({ + package: LooseArray( + z.object({ + name: z.string(), + version: z.string(), + }), + ), + }), +).transform(({ package: pkg }) => + Object.fromEntries( + pkg.map(({ name, version }): [string, string] => [name, version]), + ), +);