feat(manager): add inline script metadata (PEP 723) support (#31266)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Mathieu Kniewallner 2024-09-30 14:33:07 +02:00 committed by GitHub
parent 959e493c32
commit dcaf51c9f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 197 additions and 0 deletions

View file

@ -71,6 +71,7 @@ import * as nvm from './nvm';
import * as ocb from './ocb';
import * as osgi from './osgi';
import * as pep621 from './pep621';
import * as pep723 from './pep723';
import * as pipCompile from './pip-compile';
import * as pip_requirements from './pip_requirements';
import * as pip_setup from './pip_setup';
@ -174,6 +175,7 @@ api.set('nvm', nvm);
api.set('ocb', ocb);
api.set('osgi', osgi);
api.set('pep621', pep621);
api.set('pep723', pep723);
api.set('pip-compile', pipCompile);
api.set('pip_requirements', pip_requirements);
api.set('pip_setup', pip_setup);

View file

@ -0,0 +1,113 @@
import { codeBlock } from 'common-tags';
import { extractPackageFile } from '.';
describe('modules/manager/pep723/extract', () => {
describe('extractPackageFile()', () => {
it('should extract dependencies', () => {
const res = extractPackageFile(
codeBlock`
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests==2.32.3",
# "rich>=13.8.0",
# ]
# ///
`,
'foo.py',
);
expect(res).toEqual({
deps: [
{
currentValue: '==2.32.3',
currentVersion: '2.32.3',
datasource: 'pypi',
depName: 'requests',
depType: 'project.dependencies',
packageName: 'requests',
},
{
currentValue: '>=13.8.0',
datasource: 'pypi',
depName: 'rich',
depType: 'project.dependencies',
packageName: 'rich',
},
],
extractedConstraints: { python: '>=3.11' },
});
});
it('should skip invalid dependencies', () => {
const res = extractPackageFile(
codeBlock`
# /// script
# requires-python = "==3.11"
# dependencies = [
# "requests==2.32.3",
# "==1.2.3",
# ]
# ///
`,
'foo.py',
);
expect(res).toEqual({
deps: [
{
currentValue: '==2.32.3',
currentVersion: '2.32.3',
datasource: 'pypi',
depName: 'requests',
depType: 'project.dependencies',
packageName: 'requests',
},
],
extractedConstraints: { python: '==3.11' },
});
});
it('should return null on missing dependencies', () => {
const res = extractPackageFile(
codeBlock`
# /// script
# requires-python = ">=3.11"
# ///
`,
'foo.py',
);
expect(res).toBeNull();
});
it('should return null on invalid TOML', () => {
const res = extractPackageFile(
codeBlock`
# /// script
# requires-python
# dependencies = [
# "requests==2.32.3",
# "rich>=13.8.0",
# ]
# ///
`,
'foo.py',
);
expect(res).toBeNull();
});
it('should return null if there is no PEP 723 metadata', () => {
const res = extractPackageFile(
codeBlock`
if True:
print("requires-python>=3.11")
`,
'foo.py',
);
expect(res).toBeNull();
});
});
});

View file

@ -0,0 +1,40 @@
import { logger } from '../../../logger';
import { newlineRegex, regEx } from '../../../util/regex';
import type { PackageFileContent } from '../types';
import { Pep723Schema } from './schema';
// Adapted regex from the Python reference implementation: https://packaging.python.org/en/latest/specifications/inline-script-metadata/#reference-implementation
const regex = regEx(
/^# \/\/\/ (?<type>[a-zA-Z0-9-]+)$\s(?<content>(^#(| .*)$\s)+)^# \/\/\/$/,
'm',
);
export function extractPackageFile(
content: string,
packageFile: string,
): PackageFileContent | null {
const match = regex.exec(content);
const matchedContent = match?.groups?.content;
if (!matchedContent) {
return null;
}
// Adapted code from the Python reference implementation: https://packaging.python.org/en/latest/specifications/inline-script-metadata/#reference-implementation
const parsedToml = matchedContent
.split(newlineRegex)
.map((line) => line.substring(line.startsWith('# ') ? 2 : 1))
.join('\n');
const { data: res, error } = Pep723Schema.safeParse(parsedToml);
if (error) {
logger.debug(
{ packageFile, error },
`Error parsing PEP 723 inline script metadata`,
);
return null;
}
return res.deps.length ? res : null;
}

View file

@ -0,0 +1,12 @@
import type { Category } from '../../../constants';
import { PypiDatasource } from '../../datasource/pypi';
export { extractPackageFile } from './extract';
export const supportedDatasources = [PypiDatasource.id];
export const categories: Category[] = ['python'];
export const defaultConfig = {
// Since any Python file can embed PEP 723 metadata, make the feature opt-in, to avoid parsing all Python files.
fileMatch: [],
};

View file

@ -0,0 +1 @@
This manager supports updating dependencies inside Python files that use [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/), also known as PEP 723.

View file

@ -0,0 +1,29 @@
import is from '@sindresorhus/is';
import { z } from 'zod';
import { Toml } from '../../../util/schema-utils';
import { depTypes, pep508ToPackageDependency } from '../pep621/utils';
import type { PackageFileContent } from '../types';
const Pep723Dep = z
.string()
.transform((dep) => pep508ToPackageDependency(depTypes.dependencies, dep));
export const Pep723Schema = Toml.pipe(
z
.object({
'requires-python': z.string().optional(),
dependencies: z
.array(Pep723Dep)
.transform((deps) => deps.filter((dep) => !!dep))
.optional(),
})
.transform(({ 'requires-python': requiresPython, dependencies }) => {
const res: PackageFileContent = { deps: dependencies ?? [] };
if (is.nonEmptyString(requiresPython)) {
res.extractedConstraints = { python: requiresPython };
}
return res;
}),
);