renovate/lib/modules/versioning/poetry/transform.ts

164 lines
4.9 KiB
TypeScript

import semver from 'semver';
import { regEx } from '../../../util/regex';
import { RANGE_COMPARATOR_PATTERN, VERSION_PATTERN } from './patterns';
interface LetterTag {
letter: string;
number: string;
}
function parseLetterTag(letter?: string, number?: string): LetterTag | null {
if (letter !== undefined) {
// apply the same normalizations as poetry
const spellings: Record<string, string> = {
alpha: 'a',
beta: 'b',
c: 'rc',
pre: 'rc',
preview: 'rc',
r: 'post',
rev: 'post',
};
return {
letter: spellings[letter] || letter,
number: number ?? '0',
};
}
if (letter === undefined && number !== undefined) {
return { letter: 'post', number };
}
return null;
}
function notEmpty(s: string): boolean {
return s !== '';
}
/**
* Parse versions like poetry.core.masonry.version.Version does (union of SemVer
* and PEP440, with normalization of certain prerelease tags), and emit in SemVer
* format. NOTE: this silently discards the epoch field in PEP440 versions, as
* it has no equivalent in SemVer.
*/
export function poetry2semver(
poetry_version: string,
padRelease = true,
): string | null {
const matchGroups = VERSION_PATTERN.exec(poetry_version)?.groups;
if (!matchGroups) {
return null;
}
// trim leading zeros from valid numbers
const releaseParts = matchGroups.release
.split('.')
.map((segment) => parseInt(segment, 10));
while (padRelease && releaseParts.length < 3) {
releaseParts.push(0);
}
const pre = parseLetterTag(matchGroups.pre_l, matchGroups.pre_n);
const post = matchGroups.post_n1
? parseLetterTag(undefined, matchGroups.post_n1)
: parseLetterTag(matchGroups.post_l, matchGroups.post_n);
const dev = parseLetterTag(matchGroups.dev_l, matchGroups.dev_n);
const parts = [releaseParts.map((num) => num.toString()).join('.')];
if (pre !== null) {
// trim leading zeros from valid numbers
pre.number = pre.number.replace(regEx(/^0+(\d+)/), '$1');
parts.push(`-${pre.letter}.${pre.number}`);
}
if (post !== null) {
// trim leading zeros from valid numbers
post.number = post.number.replace(regEx(/^0+(\d+)/), '$1');
parts.push(`-${post.letter}.${post.number}`);
}
if (dev !== null) {
// trim leading zeros from valid numbers
dev.number = dev.number.replace(regEx(/^0+(\d+)/), '$1');
parts.push(`-${dev.letter}.${dev.number}`);
}
return semver.valid(parts.join(''));
}
/** Reverse normalizations applied by poetry2semver */
export function semver2poetry(version?: string): string | null {
if (!version) {
return null;
}
const s = semver.parse(version);
if (!s) {
return null;
}
const spellings: Record<string, string> = {
a: 'alpha',
b: 'beta',
c: 'rc',
dev: 'alpha',
};
s.prerelease = s.prerelease.map((letter) => spellings[letter] ?? letter);
return s.format();
}
/**
* Translate a poetry-style version range to npm format
*
* This function works like cargo2npm, but it doesn't
* add a '^', because poetry treats versions without operators as
* exact versions.
*/
export function poetry2npm(input: string, throwOnUnsupported = false): string {
// replace commas with spaces, then split at valid semver comparators
const chunks = input
.split(',')
.map((str) => str.trim())
.filter(notEmpty)
.join(' ')
.split(RANGE_COMPARATOR_PATTERN);
// do not pad versions with zeros in a range
const transformed = chunks
.map((chunk) => poetry2semver(chunk, false) ?? chunk)
.join('')
.replace(/===/, '=');
if (throwOnUnsupported) {
const isUnsupported = transformed
.split(regEx(/\s+/))
.some((part) => part.startsWith('!=')); // Patterns like `!=1.2.3` can't be easily translated between poetry/npm
if (isUnsupported) {
throw new Error('Unsupported by Poetry versioning implementation');
}
}
return transformed;
}
/**
* Translate an npm-style version range to poetry format
*
* NOTE: This function is largely copied from cargo versioning code.
* Poetry uses commas (like in cargo) instead of spaces (like in npm)
* for AND operation.
*/
export function npm2poetry(range: string): string {
// apply poetry-style normalizations to versions embedded in range string
// (i.e. anything that is not a range operator, potentially surrounded by whitespace)
const transformedRange = range
.split(RANGE_COMPARATOR_PATTERN)
.map((chunk) => semver2poetry(chunk) ?? chunk)
.join('');
// Note: this doesn't remove the ^
const res = transformedRange
.split(' ')
.map((str) => str.trim())
.filter(notEmpty);
const operators = ['^', '~', '=', '>', '<', '<=', '>='];
for (let i = 0; i < res.length - 1; i += 1) {
if (operators.includes(res[i])) {
const newValue = res[i] + ' ' + res[i + 1];
res.splice(i, 2, newValue);
}
}
return res.join(', ').replace(/\s*,?\s*\|\|\s*,?\s*/g, ' || ');
}