Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions lib/modules/manager/pep723/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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",
# ]
# ///
`,
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved
'foo.py',
);

expect(res).toMatchObject({
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',
},
],
});
});

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).toMatchObject({
deps: [
{
currentValue: '==2.32.3',
currentVersion: '2.32.3',
datasource: 'pypi',
depName: 'requests',
depType: 'project.dependencies',
packageName: 'requests',
},
],
});
});

it('should return an empty list on missing dependencies', () => {
const res = extractPackageFile(
codeBlock`
# /// script
# requires-python = ">=3.11"
# ///
`,
'foo.py',
);

expect(res).toMatchObject({ deps: [] });
});

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();
});
});
});
41 changes: 41 additions & 0 deletions lib/modules/manager/pep723/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { logger } from '../../../logger';
import { regEx } from '../../../util/regex';
import { Result } from '../../../util/result';
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
viceice marked this conversation as resolved.
Show resolved Hide resolved
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;

Check warning on line 21 in lib/modules/manager/pep723/extract.ts

View check run for this annotation

Codecov / codecov/patch

lib/modules/manager/pep723/extract.ts#L21

Added line #L21 was not covered by tests
}

// Adapted code from the Python reference implementation: https://packaging.python.org/en/latest/specifications/inline-script-metadata/#reference-implementation
const parsedToml = matchedContent
.split('\n')
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved
.map((line) => line.substring(line.startsWith('# ') ? 2 : 1))
.join('\n');

const { val: deps, err } = Result.parse(parsedToml, Pep723Schema).unwrap();
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved

if (err) {
logger.debug(
{ packageFile, err },
`Error parsing PEP 723 inline script metadata`,
);
return null;
}

return { deps };
}
12 changes: 12 additions & 0 deletions lib/modules/manager/pep723/index.ts
Original file line number Diff line number Diff line change
@@ -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: [],
};
1 change: 1 addition & 0 deletions lib/modules/manager/pep723/readme.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions lib/modules/manager/pep723/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from 'zod';
import { Toml } from '../../../util/schema-utils';
import { depTypes, pep508ToPackageDependency } from '../pep621/utils';

const Pep723Dep = z
.string()
.transform((dep) => pep508ToPackageDependency(depTypes.dependencies, dep));
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved

export const Pep723Schema = Toml.pipe(
z
.object({
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved
dependencies: z
.array(Pep723Dep)
.transform((deps) => deps.filter((dep) => !!dep))
.optional(),
})
.transform(({ dependencies }) => dependencies ?? []),
);
Loading