mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-13 15:36:25 +00:00
refactor(poetry): Use transforms for dependencies parsing (#23965)
This commit is contained in:
parent
f45cccd537
commit
95d3a1db88
6 changed files with 168 additions and 178 deletions
|
@ -377,30 +377,19 @@ exports[`modules/manager/poetry/extract extractPackageFile() extracts multiple d
|
||||||
"datasource": "pypi",
|
"datasource": "pypi",
|
||||||
"depName": "dep3",
|
"depName": "dep3",
|
||||||
"depType": "dependencies",
|
"depType": "dependencies",
|
||||||
"managerData": {
|
|
||||||
"nestedVersion": true,
|
|
||||||
},
|
|
||||||
"skipReason": "path-dependency",
|
"skipReason": "path-dependency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"currentValue": "",
|
|
||||||
"datasource": "pypi",
|
"datasource": "pypi",
|
||||||
"depName": "dep4",
|
"depName": "dep4",
|
||||||
"depType": "dependencies",
|
"depType": "dependencies",
|
||||||
"managerData": {
|
|
||||||
"nestedVersion": false,
|
|
||||||
},
|
|
||||||
"skipReason": "path-dependency",
|
"skipReason": "path-dependency",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"currentValue": "",
|
|
||||||
"datasource": "pypi",
|
"datasource": "pypi",
|
||||||
"depName": "dep5",
|
"depName": "dep5",
|
||||||
"depType": "dependencies",
|
"depType": "dependencies",
|
||||||
"managerData": {
|
"skipReason": "unspecified-version",
|
||||||
"nestedVersion": false,
|
|
||||||
},
|
|
||||||
"versioning": "pep440",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"currentValue": "^0.8.3",
|
"currentValue": "^0.8.3",
|
||||||
|
@ -504,7 +493,7 @@ exports[`modules/manager/poetry/extract extractPackageFile() extracts multiple d
|
||||||
"nestedVersion": false,
|
"nestedVersion": false,
|
||||||
},
|
},
|
||||||
"packageName": "dev-dep2",
|
"packageName": "dev-dep2",
|
||||||
"skipReason": "unspecified-version",
|
"skipReason": "invalid-version",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"currentValue": "^0.8.3",
|
"currentValue": "^0.8.3",
|
||||||
|
@ -554,13 +543,9 @@ exports[`modules/manager/poetry/extract extractPackageFile() handles multiple co
|
||||||
{
|
{
|
||||||
"deps": [
|
"deps": [
|
||||||
{
|
{
|
||||||
"currentValue": "",
|
|
||||||
"datasource": "pypi",
|
"datasource": "pypi",
|
||||||
"depName": "foo",
|
"depName": "foo",
|
||||||
"depType": "dependencies",
|
"depType": "dependencies",
|
||||||
"managerData": {
|
|
||||||
"nestedVersion": false,
|
|
||||||
},
|
|
||||||
"skipReason": "multiple-constraint-dep",
|
"skipReason": "multiple-constraint-dep",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -226,7 +226,6 @@ describe('modules/manager/poetry/extract', () => {
|
||||||
`;
|
`;
|
||||||
const res = (await extractPackageFile(content, filename))!.deps;
|
const res = (await extractPackageFile(content, filename))!.deps;
|
||||||
expect(res[0].depName).toBe('flask');
|
expect(res[0].depName).toBe('flask');
|
||||||
expect(res[0].currentValue).toBeEmptyString();
|
|
||||||
expect(res[0].skipReason).toBe('git-dependency');
|
expect(res[0].skipReason).toBe('git-dependency');
|
||||||
expect(res).toHaveLength(2);
|
expect(res).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
@ -264,7 +263,6 @@ describe('modules/manager/poetry/extract', () => {
|
||||||
`;
|
`;
|
||||||
const res = (await extractPackageFile(content, filename))!.deps;
|
const res = (await extractPackageFile(content, filename))!.deps;
|
||||||
expect(res[0].depName).toBe('flask');
|
expect(res[0].depName).toBe('flask');
|
||||||
expect(res[0].currentValue).toBe('');
|
|
||||||
expect(res[0].skipReason).toBe('path-dependency');
|
expect(res[0].skipReason).toBe('path-dependency');
|
||||||
expect(res).toHaveLength(2);
|
expect(res).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,27 +1,21 @@
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import { logger } from '../../../logger';
|
import { logger } from '../../../logger';
|
||||||
import type { SkipReason } from '../../../types';
|
import { filterMap } from '../../../util/filter-map';
|
||||||
import {
|
import {
|
||||||
getSiblingFileName,
|
getSiblingFileName,
|
||||||
localPathExists,
|
localPathExists,
|
||||||
readLocalFile,
|
readLocalFile,
|
||||||
} from '../../../util/fs';
|
} from '../../../util/fs';
|
||||||
import { parseGitUrl } from '../../../util/git/url';
|
|
||||||
import { regEx } from '../../../util/regex';
|
|
||||||
import { Result } from '../../../util/result';
|
import { Result } from '../../../util/result';
|
||||||
import { GithubTagsDatasource } from '../../datasource/github-tags';
|
|
||||||
import { PypiDatasource } from '../../datasource/pypi';
|
|
||||||
import * as pep440Versioning from '../../versioning/pep440';
|
|
||||||
import * as poetryVersioning from '../../versioning/poetry';
|
|
||||||
import type { PackageDependency, PackageFileContent } from '../types';
|
import type { PackageDependency, PackageFileContent } from '../types';
|
||||||
import {
|
import {
|
||||||
Lockfile,
|
Lockfile,
|
||||||
type PoetryDependencyRecord,
|
type PoetryDependencyRecord,
|
||||||
type PoetryGroupRecord,
|
type PoetryGroupRecord,
|
||||||
type PoetrySchema,
|
type PoetrySchema,
|
||||||
|
PoetrySchemaToml,
|
||||||
type PoetrySectionSchema,
|
type PoetrySectionSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
import { parsePoetry } from './utils';
|
|
||||||
|
|
||||||
function extractFromDependenciesSection(
|
function extractFromDependenciesSection(
|
||||||
parsedFile: PoetrySchema,
|
parsedFile: PoetrySchema,
|
||||||
|
@ -66,84 +60,20 @@ function extractFromSection(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const deps: PackageDependency[] = [];
|
return filterMap(Object.values(sectionContent), (dep) => {
|
||||||
|
if (dep.depName === 'python' || dep.depName === 'source') {
|
||||||
for (const depName of Object.keys(sectionContent)) {
|
return null;
|
||||||
if (depName === 'python' || depName === 'source') {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pep503NormalizeRegex = regEx(/[-_.]+/g);
|
dep.depType = depType;
|
||||||
let packageName = depName.toLowerCase().replace(pep503NormalizeRegex, '-');
|
|
||||||
let skipReason: SkipReason | null = null;
|
const packageName = dep.packageName ?? dep.depName;
|
||||||
let currentValue = sectionContent[depName];
|
if (packageName && packageName in poetryLockfile) {
|
||||||
let nestedVersion = false;
|
dep.lockedVersion = poetryLockfile[packageName];
|
||||||
let datasource = PypiDatasource.id;
|
|
||||||
let lockedVersion: string | null = null;
|
|
||||||
if (packageName in poetryLockfile) {
|
|
||||||
lockedVersion = poetryLockfile[packageName];
|
|
||||||
}
|
}
|
||||||
if (!is.string(currentValue)) {
|
|
||||||
if (is.array(currentValue)) {
|
return dep;
|
||||||
currentValue = '';
|
});
|
||||||
skipReason = 'multiple-constraint-dep';
|
|
||||||
} else {
|
|
||||||
const version = currentValue.version;
|
|
||||||
const path = currentValue.path;
|
|
||||||
const git = currentValue.git;
|
|
||||||
if (version) {
|
|
||||||
currentValue = version;
|
|
||||||
nestedVersion = true;
|
|
||||||
if (!!path || git) {
|
|
||||||
skipReason = path ? 'path-dependency' : 'git-dependency';
|
|
||||||
}
|
|
||||||
} else if (path) {
|
|
||||||
currentValue = '';
|
|
||||||
skipReason = 'path-dependency';
|
|
||||||
} else if (git) {
|
|
||||||
if (currentValue.tag) {
|
|
||||||
currentValue = currentValue.tag;
|
|
||||||
datasource = GithubTagsDatasource.id;
|
|
||||||
const githubPackageName = extractGithubPackageName(git);
|
|
||||||
if (githubPackageName) {
|
|
||||||
packageName = githubPackageName;
|
|
||||||
} else {
|
|
||||||
skipReason = 'git-dependency';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentValue = '';
|
|
||||||
skipReason = 'git-dependency';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentValue = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const dep: PackageDependency = {
|
|
||||||
depName,
|
|
||||||
depType,
|
|
||||||
currentValue,
|
|
||||||
managerData: { nestedVersion },
|
|
||||||
datasource,
|
|
||||||
};
|
|
||||||
if (lockedVersion) {
|
|
||||||
dep.lockedVersion = lockedVersion;
|
|
||||||
}
|
|
||||||
if (depName !== packageName) {
|
|
||||||
dep.packageName = packageName;
|
|
||||||
}
|
|
||||||
if (skipReason) {
|
|
||||||
dep.skipReason = skipReason;
|
|
||||||
} else if (pep440Versioning.isValid(currentValue)) {
|
|
||||||
dep.versioning = pep440Versioning.id;
|
|
||||||
} else if (poetryVersioning.isValid(currentValue)) {
|
|
||||||
dep.versioning = poetryVersioning.id;
|
|
||||||
} else {
|
|
||||||
dep.skipReason = 'unspecified-version';
|
|
||||||
}
|
|
||||||
deps.push(dep);
|
|
||||||
}
|
|
||||||
return deps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRegistries(pyprojectfile: PoetrySchema): string[] | undefined {
|
function extractRegistries(pyprojectfile: PoetrySchema): string[] | undefined {
|
||||||
|
@ -169,9 +99,12 @@ export async function extractPackageFile(
|
||||||
packageFile: string
|
packageFile: string
|
||||||
): Promise<PackageFileContent | null> {
|
): Promise<PackageFileContent | null> {
|
||||||
logger.trace(`poetry.extractPackageFile(${packageFile})`);
|
logger.trace(`poetry.extractPackageFile(${packageFile})`);
|
||||||
const pyprojectfile = parsePoetry(packageFile, content);
|
const { val: pyprojectfile, err } = Result.parse(
|
||||||
if (!pyprojectfile?.tool?.poetry) {
|
PoetrySchemaToml,
|
||||||
logger.debug({ packageFile }, `contains no poetry section`);
|
content
|
||||||
|
).unwrap();
|
||||||
|
if (err) {
|
||||||
|
logger.debug({ packageFile, err }, `Poetry: error parsing pyproject.toml`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,9 +142,10 @@ export async function extractPackageFile(
|
||||||
|
|
||||||
const extractedConstraints: Record<string, any> = {};
|
const extractedConstraints: Record<string, any> = {};
|
||||||
|
|
||||||
if (is.nonEmptyString(pyprojectfile?.tool?.poetry?.dependencies?.python)) {
|
const pythonVersion =
|
||||||
extractedConstraints.python =
|
pyprojectfile?.tool?.poetry?.dependencies?.python?.currentValue;
|
||||||
pyprojectfile?.tool?.poetry?.dependencies?.python;
|
if (is.nonEmptyString(pythonVersion)) {
|
||||||
|
extractedConstraints.python = pythonVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res: PackageFileContent = {
|
const res: PackageFileContent = {
|
||||||
|
@ -233,11 +167,3 @@ export async function extractPackageFile(
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractGithubPackageName(url: string): string | null {
|
|
||||||
const parsedUrl = parseGitUrl(url);
|
|
||||||
if (parsedUrl.source !== 'github.com') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `${parsedUrl.owner}/${parsedUrl.name}`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +1,152 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { parseGitUrl } from '../../../util/git/url';
|
||||||
|
import { regEx } from '../../../util/regex';
|
||||||
import { LooseArray, LooseRecord, Toml } from '../../../util/schema-utils';
|
import { LooseArray, LooseRecord, Toml } from '../../../util/schema-utils';
|
||||||
|
import { GitRefsDatasource } from '../../datasource/git-refs';
|
||||||
|
import { GithubTagsDatasource } from '../../datasource/github-tags';
|
||||||
|
import { PypiDatasource } from '../../datasource/pypi';
|
||||||
|
import * as pep440Versioning from '../../versioning/pep440';
|
||||||
|
import * as poetryVersioning from '../../versioning/poetry';
|
||||||
|
import type { PackageDependency } from '../types';
|
||||||
|
|
||||||
const PoetryDependencySchema = z.object({
|
const PoetryPathDependency = z
|
||||||
path: z.string().optional(),
|
.object({
|
||||||
git: z.string().optional(),
|
path: z.string(),
|
||||||
tag: z.string().optional(),
|
version: z.string().optional().catch(undefined),
|
||||||
version: z.string().optional(),
|
})
|
||||||
});
|
.transform(({ version }): PackageDependency => {
|
||||||
|
const dep: PackageDependency = {
|
||||||
|
datasource: PypiDatasource.id,
|
||||||
|
skipReason: 'path-dependency',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
dep.currentValue = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dep;
|
||||||
|
});
|
||||||
|
|
||||||
|
const PoetryGitDependency = z
|
||||||
|
.object({
|
||||||
|
git: z.string(),
|
||||||
|
tag: z.string().optional().catch(undefined),
|
||||||
|
version: z.string().optional().catch(undefined),
|
||||||
|
})
|
||||||
|
.transform(({ git, tag, version }): PackageDependency => {
|
||||||
|
if (!tag) {
|
||||||
|
const res: PackageDependency = {
|
||||||
|
datasource: GitRefsDatasource.id,
|
||||||
|
packageName: git,
|
||||||
|
skipReason: 'git-dependency',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
res.currentValue = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedUrl = parseGitUrl(git);
|
||||||
|
if (parsedUrl.source !== 'github.com') {
|
||||||
|
return {
|
||||||
|
datasource: GitRefsDatasource.id,
|
||||||
|
currentValue: tag,
|
||||||
|
packageName: git,
|
||||||
|
skipReason: 'git-dependency',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { owner, name } = parsedUrl;
|
||||||
|
const repo = `${owner}/${name}`;
|
||||||
|
return {
|
||||||
|
datasource: GithubTagsDatasource.id,
|
||||||
|
currentValue: tag,
|
||||||
|
packageName: repo,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const PoetryPypiDependency = z.union([
|
||||||
|
z
|
||||||
|
.object({ version: z.string().optional() })
|
||||||
|
.transform(({ version: currentValue }): PackageDependency => {
|
||||||
|
if (!currentValue) {
|
||||||
|
return { datasource: PypiDatasource.id };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
datasource: PypiDatasource.id,
|
||||||
|
managerData: { nestedVersion: true },
|
||||||
|
currentValue,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
z.string().transform(
|
||||||
|
(version): PackageDependency => ({
|
||||||
|
datasource: PypiDatasource.id,
|
||||||
|
currentValue: version,
|
||||||
|
managerData: { nestedVersion: false },
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PoetryDependencySchema = z.union([
|
||||||
|
PoetryPathDependency,
|
||||||
|
PoetryGitDependency,
|
||||||
|
PoetryPypiDependency,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PoetryArraySchema = z.array(z.unknown()).transform(
|
||||||
|
(): PackageDependency => ({
|
||||||
|
datasource: PypiDatasource.id,
|
||||||
|
skipReason: 'multiple-constraint-dep',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const PoetryValue = z.union([PoetryDependencySchema, PoetryArraySchema]);
|
||||||
|
type PoetryValue = z.infer<typeof PoetryValue>;
|
||||||
|
|
||||||
export const PoetryDependencyRecord = LooseRecord(
|
export const PoetryDependencyRecord = LooseRecord(
|
||||||
z.string(),
|
z.string(),
|
||||||
z.union([PoetryDependencySchema, z.array(PoetryDependencySchema), z.string()])
|
PoetryValue.transform((dep) => {
|
||||||
);
|
if (dep.skipReason) {
|
||||||
|
return dep;
|
||||||
|
}
|
||||||
|
|
||||||
|
// istanbul ignore if: normaly should not happen
|
||||||
|
if (!dep.currentValue) {
|
||||||
|
dep.skipReason = 'unspecified-version';
|
||||||
|
return dep;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pep440Versioning.isValid(dep.currentValue)) {
|
||||||
|
dep.versioning = pep440Versioning.id;
|
||||||
|
return dep;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poetryVersioning.isValid(dep.currentValue)) {
|
||||||
|
dep.versioning = poetryVersioning.id;
|
||||||
|
return dep;
|
||||||
|
}
|
||||||
|
|
||||||
|
dep.skipReason = 'invalid-version';
|
||||||
|
return dep;
|
||||||
|
})
|
||||||
|
).transform((record) => {
|
||||||
|
for (const [depName, dep] of Object.entries(record)) {
|
||||||
|
dep.depName = depName;
|
||||||
|
if (!dep.packageName) {
|
||||||
|
const pep503NormalizeRegex = regEx(/[-_.]+/g);
|
||||||
|
const packageName = depName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(pep503NormalizeRegex, '-');
|
||||||
|
if (depName !== packageName) {
|
||||||
|
dep.packageName = packageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return record;
|
||||||
|
});
|
||||||
|
|
||||||
export type PoetryDependencyRecord = z.infer<typeof PoetryDependencyRecord>;
|
export type PoetryDependencyRecord = z.infer<typeof PoetryDependencyRecord>;
|
||||||
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { codeBlock } from 'common-tags';
|
|
||||||
import { parsePoetry } from './utils';
|
|
||||||
|
|
||||||
describe('modules/manager/poetry/utils', () => {
|
|
||||||
const fileName = 'fileName';
|
|
||||||
|
|
||||||
describe('parsePoetry', () => {
|
|
||||||
it('load and parse successfully', () => {
|
|
||||||
const fileContent = codeBlock`
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
dep1 = "1.0.0"
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
dep2 = "1.0.1"
|
|
||||||
`;
|
|
||||||
const actual = parsePoetry(fileName, fileContent);
|
|
||||||
expect(actual).toMatchObject({
|
|
||||||
tool: {
|
|
||||||
poetry: {
|
|
||||||
dependencies: { dep1: '1.0.0' },
|
|
||||||
group: { dev: { dependencies: { dep2: '1.0.1' } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invalid toml', () => {
|
|
||||||
const actual = parsePoetry(fileName, 'clearly_invalid');
|
|
||||||
expect(actual).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invalid schema', () => {
|
|
||||||
const fileContent = codeBlock`
|
|
||||||
[tool.poetry.dependencies]:
|
|
||||||
dep1 = 1
|
|
||||||
`;
|
|
||||||
const actual = parsePoetry(fileName, fileContent);
|
|
||||||
expect(actual).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { logger } from '../../../logger';
|
|
||||||
import { type PoetrySchema, PoetrySchemaToml } from './schema';
|
|
||||||
|
|
||||||
export function parsePoetry(
|
|
||||||
fileName: string,
|
|
||||||
fileContent: string
|
|
||||||
): PoetrySchema | null {
|
|
||||||
const res = PoetrySchemaToml.safeParse(fileContent);
|
|
||||||
if (res.success) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
logger.debug({ err: res.error, fileName }, 'Error parsing poetry lockfile.');
|
|
||||||
return null;
|
|
||||||
}
|
|
Loading…
Reference in a new issue