diff --git a/lib/modules/manager/pep621/artifacts.spec.ts b/lib/modules/manager/pep621/artifacts.spec.ts index f48d42050a816d16003fa7bef9625c34af7be0d8..ebcc935879894b23ffed81adacca8b1e73521af9 100644 --- a/lib/modules/manager/pep621/artifacts.spec.ts +++ b/lib/modules/manager/pep621/artifacts.spec.ts @@ -1,3 +1,4 @@ +import { codeBlock } from 'common-tags'; import { join } from 'upath'; import { mockExecAll } from '../../../../test/exec-util'; import { fs, mockedFunction } from '../../../../test/util'; @@ -36,6 +37,25 @@ describe('modules/manager/pep621/artifacts', () => { expect(result).toBeNull(); }); + it('return artifact error if newPackageFile content is not valid', async () => { + const updatedDeps = [ + { + packageName: 'dep1', + }, + ]; + const result = await updateArtifacts({ + packageFileName: 'pyproject.toml', + newPackageFileContent: '--test string--', + config, + updatedDeps, + }); + expect(result).toEqual([ + { + artifactError: { stderr: 'Failed to parse new package file content' }, + }, + ]); + }); + it('return processor result', async () => { const execSnapshots = mockExecAll(); GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); @@ -44,7 +64,11 @@ describe('modules/manager/pep621/artifacts', () => { fs.readLocalFile.mockResolvedValueOnce('new test content'); // python getPkgReleases.mockResolvedValueOnce({ - releases: [{ version: '3.11.1' }, { version: '3.11.2' }], + releases: [ + { version: '3.7.1' }, + { version: '3.8.1' }, + { version: '3.11.2' }, + ], }); // pdm getPkgReleases.mockResolvedValueOnce({ @@ -54,7 +78,12 @@ describe('modules/manager/pep621/artifacts', () => { const updatedDeps = [{ packageName: 'dep1' }]; const result = await updateArtifacts({ packageFileName: 'pyproject.toml', - newPackageFileContent: '', + newPackageFileContent: codeBlock` +[project] +name = "pdm" +dynamic = ["version"] +requires-python = "<3.9" + `, config: {}, updatedDeps, }); @@ -90,7 +119,7 @@ describe('modules/manager/pep621/artifacts', () => { '-w "/tmp/github/some/repo" ' + 'containerbase/sidecar ' + 'bash -l -c "' + - 'install-tool python 3.11.2 ' + + 'install-tool python 3.8.1 ' + '&& ' + 'install-tool pdm v2.5.0 ' + '&& ' + diff --git a/lib/modules/manager/pep621/artifacts.ts b/lib/modules/manager/pep621/artifacts.ts index 21145c463c14e2abbfb921057ea22dc39c7f6313..2d913a805015fd643d9c222bc87506f9ecb939d4 100644 --- a/lib/modules/manager/pep621/artifacts.ts +++ b/lib/modules/manager/pep621/artifacts.ts @@ -2,6 +2,7 @@ import is from '@sindresorhus/is'; import { writeLocalFile } from '../../../util/fs'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; import { processors } from './processors'; +import { parsePyProject } from './utils'; export async function updateArtifacts( updateArtifact: UpdateArtifact @@ -10,10 +11,24 @@ export async function updateArtifacts( await writeLocalFile(packageFileName, newPackageFileContent); + const project = parsePyProject(packageFileName, newPackageFileContent); + if (is.nullOrUndefined(project)) { + return [ + { + artifactError: { + stderr: 'Failed to parse new package file content', + }, + }, + ]; + } + // process specific tool sets const result: UpdateArtifactsResult[] = []; for (const processor of processors) { - const artifactUpdates = await processor.updateArtifacts(updateArtifact); + const artifactUpdates = await processor.updateArtifacts( + updateArtifact, + project + ); if (is.array(artifactUpdates)) { result.push(...artifactUpdates); } diff --git a/lib/modules/manager/pep621/extract.spec.ts b/lib/modules/manager/pep621/extract.spec.ts index bc23878672d80cc39fa3df884f4ccfbb95180040..4c14791d1db938b51be0d18d6291496de9e07b3f 100644 --- a/lib/modules/manager/pep621/extract.spec.ts +++ b/lib/modules/manager/pep621/extract.spec.ts @@ -26,6 +26,11 @@ describe('modules/manager/pep621/extract', () => { it('should return dependencies for valid content', function () { const result = extractPackageFile(pdmPyProject, 'pyproject.toml'); + expect(result).toMatchObject({ + extractedConstraints: { + python: '>=3.7', + }, + }); const dependencies = result?.deps.filter( (dep) => dep.depType === 'project.dependencies' ); diff --git a/lib/modules/manager/pep621/extract.ts b/lib/modules/manager/pep621/extract.ts index f062fec0ba3c69eb33bacf7ef92ae448e0f61135..f0710934a01771ee935b7e31f2bea985d9e64ffd 100644 --- a/lib/modules/manager/pep621/extract.ts +++ b/lib/modules/manager/pep621/extract.ts @@ -1,4 +1,4 @@ -import toml from '@iarna/toml'; +import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import type { ExtractConfig, @@ -6,8 +6,11 @@ import type { PackageFileContent, } from '../types'; import { processors } from './processors'; -import { PyProject, PyProjectSchema } from './schema'; -import { parseDependencyGroupRecord, parseDependencyList } from './utils'; +import { + parseDependencyGroupRecord, + parseDependencyList, + parsePyProject, +} from './utils'; export function extractPackageFile( content: string, @@ -18,17 +21,14 @@ export function extractPackageFile( const deps: PackageDependency[] = []; - let def: PyProject; - try { - const jsonMap = toml.parse(content); - def = PyProjectSchema.parse(jsonMap); - } catch (err) { - logger.debug( - { packageFile, err }, - `Failed to parse and validate pyproject file` - ); + const def = parsePyProject(packageFile, content); + if (is.nullOrUndefined(def)) { return null; } + const pythonConstraint = def.project?.['requires-python']; + const constraints = is.nonEmptyString(pythonConstraint) + ? { extractedConstraints: { python: pythonConstraint } } + : {}; // pyProject standard definitions deps.push( @@ -47,5 +47,5 @@ export function extractPackageFile( processedDeps = processor.process(def, processedDeps); } - return processedDeps.length ? { deps: processedDeps } : null; + return processedDeps.length ? { ...constraints, deps: processedDeps } : null; } diff --git a/lib/modules/manager/pep621/processors/pdm.spec.ts b/lib/modules/manager/pep621/processors/pdm.spec.ts index c5b1523a35db19a38056c381423495435132ae81..a09f5082e40e235527a4febc553a034b304aa900 100644 --- a/lib/modules/manager/pep621/processors/pdm.spec.ts +++ b/lib/modules/manager/pep621/processors/pdm.spec.ts @@ -26,12 +26,15 @@ describe('modules/manager/pep621/processors/pdm', () => { it('return null if there is no lock file', async () => { fs.getSiblingFileName.mockReturnValueOnce('pdm.lock'); const updatedDeps = [{ packageName: 'dep1' }]; - const result = await processor.updateArtifacts({ - packageFileName: 'pyproject.toml', - newPackageFileContent: '', - config, - updatedDeps, - }); + const result = await processor.updateArtifacts( + { + packageFileName: 'pyproject.toml', + newPackageFileContent: '', + config, + updatedDeps, + }, + {} + ); expect(result).toBeNull(); }); @@ -51,12 +54,15 @@ describe('modules/manager/pep621/processors/pdm', () => { }); const updatedDeps = [{ packageName: 'dep1' }]; - const result = await processor.updateArtifacts({ - packageFileName: 'pyproject.toml', - newPackageFileContent: '', - config: {}, - updatedDeps, - }); + const result = await processor.updateArtifacts( + { + packageFileName: 'pyproject.toml', + newPackageFileContent: '', + config: {}, + updatedDeps, + }, + {} + ); expect(result).toBeNull(); expect(execSnapshots).toMatchObject([ { @@ -94,12 +100,15 @@ describe('modules/manager/pep621/processors/pdm', () => { }); const updatedDeps = [{ packageName: 'dep1' }]; - const result = await processor.updateArtifacts({ - packageFileName: 'pyproject.toml', - newPackageFileContent: '', - config: {}, - updatedDeps, - }); + const result = await processor.updateArtifacts( + { + packageFileName: 'pyproject.toml', + newPackageFileContent: '', + config: {}, + updatedDeps, + }, + {} + ); expect(result).toEqual([ { artifactError: { lockFile: 'pdm.lock', stderr: 'test error' } }, ]); @@ -122,12 +131,15 @@ describe('modules/manager/pep621/processors/pdm', () => { }); const updatedDeps = [{ packageName: 'dep1' }, { packageName: 'dep2' }]; - const result = await processor.updateArtifacts({ - packageFileName: 'pyproject.toml', - newPackageFileContent: '', - config: {}, - updatedDeps, - }); + const result = await processor.updateArtifacts( + { + packageFileName: 'pyproject.toml', + newPackageFileContent: '', + config: {}, + updatedDeps, + }, + {} + ); expect(result).toEqual([ { file: { @@ -159,14 +171,17 @@ describe('modules/manager/pep621/processors/pdm', () => { releases: [{ version: 'v2.6.1' }, { version: 'v2.5.0' }], }); - const result = await processor.updateArtifacts({ - packageFileName: 'folder/pyproject.toml', - newPackageFileContent: '', - config: { - updateType: 'lockFileMaintenance', + const result = await processor.updateArtifacts( + { + packageFileName: 'folder/pyproject.toml', + newPackageFileContent: '', + config: { + updateType: 'lockFileMaintenance', + }, + updatedDeps: [], }, - updatedDeps: [], - }); + {} + ); expect(result).toEqual([ { file: { diff --git a/lib/modules/manager/pep621/processors/pdm.ts b/lib/modules/manager/pep621/processors/pdm.ts index c82330e6d05e2d36ab268e48ecd84b441c0e2ae7..3730cd8b33e9bcbed307b020e08cd1162dde2e30 100644 --- a/lib/modules/manager/pep621/processors/pdm.ts +++ b/lib/modules/manager/pep621/processors/pdm.ts @@ -50,7 +50,8 @@ export class PdmProcessor implements PyProjectProcessor { } async updateArtifacts( - updateArtifact: UpdateArtifact + updateArtifact: UpdateArtifact, + project: PyProject ): Promise<UpdateArtifactsResult[] | null> { const { config, updatedDeps, packageFileName } = updateArtifact; @@ -64,9 +65,11 @@ export class PdmProcessor implements PyProjectProcessor { logger.debug('No pdm.lock found'); return null; } + const pythonConstraint: ToolConstraint = { toolName: 'python', - constraint: config.constraints?.python, + constraint: + config.constraints?.python ?? project.project?.['requires-python'], }; const pdmConstraint: ToolConstraint = { toolName: 'pdm', diff --git a/lib/modules/manager/pep621/processors/types.ts b/lib/modules/manager/pep621/processors/types.ts index 37798951b7e161b6e1975952222add0962f0a759..bfbf0357e9ef5e7dda8e857c0d6be76f0926f2b9 100644 --- a/lib/modules/manager/pep621/processors/types.ts +++ b/lib/modules/manager/pep621/processors/types.ts @@ -7,7 +7,8 @@ import type { PyProject } from '../schema'; export interface PyProjectProcessor { updateArtifacts( - updateArtifact: UpdateArtifact + updateArtifact: UpdateArtifact, + project: PyProject ): Promise<UpdateArtifactsResult[] | null>; /** diff --git a/lib/modules/manager/pep621/schema.ts b/lib/modules/manager/pep621/schema.ts index 04dcc6609ce98c672b6153266bf1fe138ecd2c6b..c4a7443ebbf8ede971a981aeb97cdbc83bd05bbb 100644 --- a/lib/modules/manager/pep621/schema.ts +++ b/lib/modules/manager/pep621/schema.ts @@ -10,6 +10,7 @@ const DependencyRecordSchema = z export const PyProjectSchema = z.object({ project: z .object({ + 'requires-python': z.string().optional(), dependencies: DependencyListSchema, 'optional-dependencies': DependencyRecordSchema, }) diff --git a/lib/modules/manager/pep621/utils.ts b/lib/modules/manager/pep621/utils.ts index 9c8149cfc7318f96bf019526d722c3e5e6d34a46..bc1ac4cce252a36bbcc6c66715b964dcff35ebb9 100644 --- a/lib/modules/manager/pep621/utils.ts +++ b/lib/modules/manager/pep621/utils.ts @@ -1,8 +1,10 @@ +import toml from '@iarna/toml'; import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { PypiDatasource } from '../../datasource/pypi'; import type { PackageDependency } from '../types'; +import { PyProject, PyProjectSchema } from './schema'; import type { Pep508ParseResult } from './types'; const pep508Regex = regEx( @@ -99,3 +101,19 @@ export function parseDependencyList( } return deps; } + +export function parsePyProject( + packageFile: string, + content: string +): PyProject | null { + try { + const jsonMap = toml.parse(content); + return PyProjectSchema.parse(jsonMap); + } catch (err) { + logger.debug( + { packageFile, err }, + `Failed to parse and validate pyproject file` + ); + return null; + } +}