Skip to content

Commit

Permalink
feat(manager): add PEP 723
Browse files Browse the repository at this point in the history
  • Loading branch information
mkniewallner committed Sep 7, 2024
1 parent e064c5e commit 5bd88c7
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 0 deletions.
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",
# ]
# ///
`,
'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
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')
.map((line) => line.substring(line.startsWith('# ') ? 2 : 1))
.join('\n');

const { val: deps, err } = Result.parse(parsedToml, Pep723Schema).unwrap();

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));

export const Pep723Schema = Toml.pipe(
z
.object({
dependencies: z
.array(Pep723Dep)
.transform((deps) => deps.filter((dep) => !!dep))
.optional(),
})
.transform(({ dependencies }) => dependencies ?? []),
);

0 comments on commit 5bd88c7

Please sign in to comment.