Renovate 的 Poetry 版本解析模块:PEP 440 与 SemVer 混合版本的转换与比较原理
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
本篇技术文章围绕 Renovate 仓库中的 Poetry 版本解析模块(lib/modules/versioning/poetry/)展开,系统讲解其设计动机(PEP 440 与 SemVer 的混用问题)、版本/范围双向转换的实现原理、完整 API 的行为细节与边界限制,并结合源码与测试用例说明getNewValue等关键函数如何生成pyproject.toml中依赖约束的新值。读完后,你将理解 Renovate 如何在“按 Poetry 规范写入”与“按 npm/SemVer 规则计算”之间架起桥梁,并能准确判断该模块对预发布版本、通配符约束、!=排除等写法的支持边界。
背景:为什么 Poetry 需要独立的版本解析模块
Poetry 的依赖约束(写在pyproject.toml的[tool.poetry.dependencies]中)与 Python 包索引(PyPI)上实际发布的版本号,分属两套不完全一致的体系:
- Poetry 约束:在
major.minor.patch层面兼容 SemVer 风格的语义(如^1.2、~1.2.3、>=1.2,<2.0),但对预发布(pre)、后置发布(post)、开发版(dev)的表达采用 PEP 440 的形式; - PEP 440:Python 官方定义的版本号规范,如
1.0a1、1.0rc1、1.0.post1、1.0.dev1,与 SemVer 的-alpha.1、-rc.1等写法不同。
模块自带的说明文档 lib/modules/versioning/poetry/readme.md 对此总结为:“Poetry versioning is a little like a mix of PEP440 and SemVer”(Poetry 的版本规则近似于 PEP 440 与 SemVer 的混合体),并指出当前实现基于 npm 版本解析,通过“按 Poetry 的模式解析 → 交给 npm 实现计算 → 反向还原归一化”来让 Renovate 能把pyproject.toml中 SemVer 风格的版本与 PyPI 上的 PEP 440 表示进行有意义的比较。“这些表示在 major.minor.patch 正式版本上等价,但在 pre-、post- 和 dev 版本上并不相同。”
Renovate 同时提供 pep440 版本解析模块(lib/modules/versioning/pep440/),用于纯 PEP 440 语境的比较;而 Poetry 模块的定位是服务于 Poetry 生态下pyproject.toml的解析与约束改写。
整体架构:以 npm 版本解析为计算引擎
从 lib/modules/versioning/poetry/index.ts 的源码结构看,模块声明了基本属性:
export const id = 'poetry'; export const displayName = 'Poetry'; export const supportsRanges = true; export const supportedRangeStrategies: RangeStrategy[] = [ 'bump', 'widen', 'replace', ];即:模块标识为poetry(Renovate 配置中versioning: "poetry"即指向它),支持范围(range)语义,且支持bump、widen、replace三种rangeStrategy。
核心设计是“翻译—计算—反翻译”三段式,翻译函数全部集中在 lib/modules/versioning/poetry/transform.ts:
| 函数 | 方向 | 用途 |
|---|---|---|
poetry2semver | Poetry 版本 → SemVer 字符串 | 把单个版本号(如1.9.01b01)转成标准 SemVer(如1.9.1-beta.1),供 npm 实现比较 |
poetry2npm | Poetry 范围 → npm 范围 | 把>=1.0,<2.0这类逗号分隔的约束翻译成 npm 可解析的空格分隔形式 |
semver2poetry | SemVer → Poetry 风格 | 反向还原版本号内的标记(如-rc.1还原为rc1) |
npm2poetry | npm 范围 → Poetry 范围 | 把空格分隔的 npm 范围还原为逗号分隔的 Poetry 写法 |
以matches(version, range)为例,调用链是:
function matches(version: string, range: string): boolean { const semverVersion = poetry2semver(version); return !!( isVersion(version) && semverVersion && npm.matches(semverVersion, poetry2npm(range)) ); }(见 lib/modules/versioning/poetry/index.ts 第 89–96 行)也就是说:先校验并转换版本号,再转换范围,最终委托给 npm 版本解析实现(lib/modules/versioning/npm/index.ts)完成匹配计算。getSatisfyingVersion、minSatisfyingVersion也是同样的模式:把候选版本列表批量poetry2semver、把范围poetry2npm,调用 npm 实现后,再把结果semver2poetry还原成 Poetry 风格输出。
一个值得注意的分工:isGreaterThan(版本间大小比较)与sortVersions(排序)并未走 npm 通道,而是直接委托给 pep440 模块(index.ts 第 55–57 行、第 241–243 行)。从源码结构看,这是有意为之——PyPI 返回的版本是 PEP 440 表示,直接用它比较能避免转换过程中丢失 PEP 440 的排序语义(例如 post 与 local 版本的相对顺序)。
版本识别:VERSION_PATTERN 正则
版本是否“可识别”由 lib/modules/versioning/poetry/patterns.ts 中的VERSION_PATTERN决定。该正则的注释写明:它是poetry.core.version.Version用于解析“SemVer(pre/post/dev 子集)与 PEP 440 并集”的等价模式。其结构按 PEP 440 的段序组织:
- 可选
v前缀:v1.2.3合法; - epoch:
(?<epoch>[0-9]+)!形式,如1!2.0.0; - release 段:
[0-9]+(?:\.[0-9]+){0,2},即 1 到 3 段数字(1、1.9、1.9.4均可); - pre 段:字母标记为
a|b|c|rc|alpha|beta|pre|preview,可带数字,如1.9b0、1.9.pre; - post 段:两种形式——
-N(如1.9-0)或字母post|rev|r(如1.9.0-post、1.9.0rev3); - dev 段:
dev标记,如1.9.0dev0; - local 段:
+后的本地标识,如1.0+abc.5。
测试用例(lib/modules/versioning/poetry/index.spec.ts 中isVersion组)验证了边界:17.04.01合法(前导零会被解析时去掉),17.b4.0非法(release 段必须是数字),0.98.5.1非法(超过 3 段)。
同一文件还定义了RANGE_COMPARATOR_PATTERN,匹配范围中的比较运算符(^、~、=、>、<、||等),用于把范围字符串按运算符切分。
版本双向转换:poetry2semver 与 semver2poetry
poetry2semver(transform.ts 第 43–82 行)做了四件事:
- 解析与归一化:通过
parseLetterTag把 Poetry 的标记拼写归一化为 npm/SemVer 习惯——alpha→a、beta→b、c/pre/preview→rc、r/rev→post,缺省的数字默认补0; - 补齐 release 段:默认
padRelease = true时把1、1.9补成1.0.0、1.9.0(范围转换时传false,不补零); - 去前导零:release 各段与 pre/post/dev 数字中的前导零被剥离(
1.9.01b01→1.9.1-beta.1); - 用
semver.valid兜底校验:转不出合法 SemVer 就返回null。
文档中特别注明的一个设计取舍:epoch 段被静默丢弃,因为 SemVer 没有对应概念。这意味着1!2.0.0与2.0.0在该模块看来等价,属于已知的表达力损失。
反向的semver2poetry做对称的还原(第 85–101 行),把 SemVer 的 pre 标记拼写映射回 Poetry 风格:a→alpha、b→beta、c→rc、dev→alpha。测试用例equals("1.9b0", "1.9.0-beta.0") === true与getSatisfyingVersion(['0.8.0a2','0.8.0a7'], '^0.8.0-alpha.0') === '0.8.0-alpha.2'(输出被还原回 alpha 风格)都印证了这一往返转换的一致性。
范围转换:poetry2npm 与 npm2poetry
Poetry 与 npm 的范围写法差异主要有两点:AND 的分隔符(Poetry 用逗号,npm 用空格)与无运算符版本的含义(Poetry 中裸版本号表示“精确匹配”,不像 Cargo 隐式补^——源码注释明确说明poetry2npm“doesn't add a^”)。
poetry2npm(transform.ts 第 110–132 行)的流程:逗号变空格 → 按RANGE_COMPARATOR_PATTERN切分 → 每段poetry2semver(chunk, false)转换 → 拼接并把===替换为=。它还带一个throwOnUnsupported开关:当范围含!=排除(如>=2.6, !=3.0.*, <4)时抛出异常,因为这类模式在 Poetry/npm 之间难以可靠翻译。isValid正是利用这一点返回false(index.ts 第 68–82 行),测试用例isValid('>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4') === false验证了这一边界。
npm2poetry(第 141–163 行)做反向拼接:先把范围内嵌的版本逐段semver2poetry还原拼写,再把运算符与其后的版本粘合(避免^ 1.0被空格拆开),最终以逗号连接,||保留为 Poetry 支持的“或”写法。源码注释指出该函数“largely copied from cargo versioning code”,因为两者都使用逗号作 AND 分隔符。
完整 API 行为一览
api对象(index.ts 第 249–267 行)导出的全部能力及其实现路径如下:
| API | 实现方式 |
|---|---|
equals/getMajor/getMinor/getPatch | poetry2semver后委托 npm 实现 |
isVersion/isCompatible | 直接匹配VERSION_PATTERN |
isGreaterThan/sortVersions | 委托 pep440 模块 |
isValid | poetry2npm(input, true)后委托npm.isValid,不支持的输入记录 debug 日志并返回false |
matches/isLessThanRange | 版本与范围双向转换后委托 npm 实现 |
getSatisfyingVersion/minSatisfyingVersion | 批量转换 → npm 计算 →semver2poetry还原 |
isStable | 转换后委托npm.isStable(测试确认1.9.4-beta、1.9.4a0均判为不稳定) |
isSingleVersion | =1.2.3(可带空格)或裸版本号视为单版本;1.*不算 |
subset | 范围poetry2npm后委托npm.subset,测试确认^1.1.0 || ^2.0.0是^1.0.0 || ^2.0.0的子集 |
getNewValue | 见下节 |
getNewValue:生成依赖约束新值的核心逻辑
getNewValue是 Renovate 更新 PR 时计算pyproject.toml新约束值的关键函数(index.ts 第 161–239 行),其处理顺序为:
- replace 策略下的兼容短路:若新版本的 SemVer 形式满足当前约束(
npm.matches),则原样返回currentValue——不满足才改写; ^/~短写补全:handleShort根据当前约束的段数决定补写到哪一级。^1.0.0+ 新 major 生成^2.0.0;而^1+2.1.7(bump 策略)生成^2.1.7。测试用例getNewValue('^1.0.0', 'replace', '1.0.0', '2.0.7') === '^2.0.0'与getNewValue('^1', 'bump', '1.0.0', '2.1.7') === '^2.1.7'分别覆盖了这两种路径;- 完整版号校验:若
newVersion不是三段完整版本(如含 pre 标记或只有两段),则放弃计算直接返回原值(6.b0.0这类非法号会触发该分支,测试getNewValue('5.0','bump','5.0.0','6.b0.0') === '5.0'验证了这一点); - 委托 npm 计算:
poetry2npm(currentValue)+poetry2semver(newVersion)后调用npm.getNewValue,再把结果npm2poetry还原,全程 try/catch 保护,失败则回退原值。
测试用例(index.spec.ts 第 207–267 行)展示了典型行为,可归纳为几类:
// 精确版本 bump:=1.0.0 前缀与空格被规范化 getNewValue('= 1.0.0', 'bump', '1.0.0', '1.1.0') === '=1.1.0' // 通配符范围 getNewValue('1.0.*', 'replace', '1.0.0', '1.1.0') === '1.1.*' getNewValue('1.*', 'replace', '1.0.0', '2.1.0') === '2.*' // 比较运算符 getNewValue('<1.3.4', 'replace', '1.2.3', '1.5.0') === '<1.5.1' getNewValue('<= 1.3.4', 'replace', '1.2.3', '1.5.0') === '<= 1.5.0' // widen 策略:追加新的 major 分支而非替换 getNewValue('^2.2', 'widen', '2.2.0', '3.0.0') === '^2.2 || ^3.0.0' getNewValue('^2.2 || ^3.0.0', 'widen', '3.0.0', '4.0.0') === '^2.2 || ^3.0.0 || ^4.0.0' // 预发布版本拼写还原 getNewValue('^1', 'bump', '1.0.0', '1.0.7rc.1') === '^1.0.7-rc.1' getNewValue('^0.8.0-alpha.0', 'bump', '0.8.0-alpha.0', '0.8.0a1') === '^0.8.0-alpha.1'注意最后一条:输入的新版本0.8.0a1(PEP 440 风格)输出的约束写作-alpha.1,说明npm2poetry的还原映射把 SemVer 的a标记映射回了alpha拼写。
模块在 Renovate 中的消费方式
该版本解析模块通过模块注册机制(lib/modules/versioning/api.ts 自动聚合各子模块导出)对外暴露,Renovate 配置中以versioning: "poetry"选用。仓库内的直接消费方包括:
- Poetry 包管理器:lib/modules/manager/poetry/schema.ts 导入
poetryVersioning,用于解析pyproject.toml中 Poetry 依赖的版本约束(该 manager 的 zod schema 同时处理 path/git 依赖、PEP 508 风格约束等); - 版本查找过滤器:lib/workers/repository/process/lookup/filter.ts 第 150 行附近对
config.versioning === poetryVersioning.id做特判,在过滤候选版本时按 Poetry 规则处理。
从源码结构看,manager 层与 lookup 流程均把“判断版本关系”的决策下沉到该模块,保证了 Python/Poetry 生态的版本语义与 Renovate 通用流程解耦。
已知边界与实现取舍
基于源码与测试用例可以确认的边界:
!=排除约束不受支持:isValid对含!=的范围返回false,Renovate 会视为无法解析该约束(见 transform.ts 第 123–130 行);- epoch 段被丢弃:
poetry2semver注释明确说明,SemVer 无 epoch 等价物; - 非完整版本号的 newVersion 不计算新值:直接回退原值并记录 debug 日志;
- 裸版本号在范围中表示精确匹配,不隐式补
^(与 Cargo 版本文法不同); - pre/post/dev 的语义依赖转换:文档明确指出这些版本在 PEP 440 与 SemVer 表示中不完全等价,模块通过统一的归一化拼写表(
parseLetterTag/semver2poetry)保证两端行为一致,但诸如 local 版本(+abc.5)的比较最终依赖 pep440 通道。
参考文件
- 模块说明文档:lib/modules/versioning/poetry/readme.md
- API 实现:lib/modules/versioning/poetry/index.ts
- 版本/范围正则:lib/modules/versioning/poetry/patterns.ts
- 双向转换实现:lib/modules/versioning/poetry/transform.ts
- 完整测试用例:lib/modules/versioning/poetry/index.spec.ts
- 相关模块:pep440 版本解析、npm 版本解析、poetry 包管理器
- 版本模块注册入口:lib/modules/versioning/index.ts
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考