pytest 变更日志生成机制:changelog/_template.rst 模板与 towncrier 新闻片段工作流解析
【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest
本文以 pytest 仓库中负责生成CHANGELOG的核心模板 changelog/_template.rst 为主线,系统讲解 pytest 如何通过 towncrier 将分散的「新闻片段(newsfragment)」聚合为一份面向用户的发布说明。读完本文,你将理解模板的每一段 Jinja2 逻辑、pyproject.toml 中 10 类变更类型的配置方式、新闻片段的命名与写作规范,以及该模板如何与 Sphinx 文档站点(含草稿预览)完成集成,从而能够在 pytest 生态中熟练编写与审阅变更记录。
一、模板在整个发布流程中的位置
changelog/_template.rst并非一份普通的 RST 文档,而是一份Jinja2 模板——它不直接展示给用户阅读,而是被 towncrier 调用,用于把changelog/目录下数百个零散的变更记录文件渲染成最终发布的CHANGELOG。
整条流水线可以概括为:
changelog/*.<type>.rst(新闻片段,如 14743.feature.rst) │ towncrier build(配置读取自 pyproject.toml 的 [tool.towncrier]) ▼ changelog/_template.rst(Jinja2 模板,负责结构编排与 RST 渲染) ▼ doc/en/changelog.rst(最终产物,随文档站点发布)这条链路的枢纽配置位于 pyproject.toml:
[tool.towncrier] package = "pytest" package_dir = "src" filename = "doc/en/changelog.rst" directory = "changelog/" title_format = "pytest {version} ({project_date})" template = "changelog/_template.rst"其中各字段的作用:
directory = "changelog/":新闻片段的存放目录,即仓库中的 changelog/;filename = "doc/en/changelog.rst":模板渲染结果的落盘位置,即 doc/en/changelog.rst;template = "changelog/_template.rst":显式指定本文的主角——渲染模板;title_format = "pytest {version} ({project_date})":生成版本标题,例如最终成品中可以看到pytest 9.1.1 (2026-06-19)这种标题行(见 doc/en/changelog.rst)。
doc/en/changelog.rst 开头也明确写着该文件由 towncrier 托管,贡献者不应手动编辑,只能通过新增新闻片段的方式来添加变更记录;而第 32 行的.. towncrier release notes start注释则标明了渲染内容插入的位置。
二、模板结构逐段拆解
_template.rst全文约 40 行,虽然精炼,却完整承载了版本分组、类别编排、条目排序、空版本兜底等全部逻辑。下面逐段解读。
2.1 外层:遍历 section 与 RST 标题层级
模板最外层的骨架:
{% for section in sections %} {% set underline = "-" %} {% if section %} {{section}} {{ underline * section|length }}{% set underline = "~" %} {% endif %}towncrier 会把变更数据组织成sections字典传给模板,每个 key 是一个版本区段名(pytest 当前配置未声明自定义 section,因此区段名为空字符串)。这段逻辑的含义是:
- 每轮循环先把
underline初始化为-; - 若区段名非空,则输出区段名,并用等长数量的
-作为 RST 标题下划线(RST 要求下划线长度与标题一致); - 紧接着把
underline改为~。
这里的underline变量会在同一轮循环的后续代码中被继续使用(见 2.4 的类别标题),从而实现RST 标题层级的区分:带名称的 section 用-作一级标题,其下的类别用~作二级标题。由于 pytest 当前配置下区段名为空字符串,{% if section %}不成立,因此实际产物里类别标题使用-下划线,这与 doc/en/changelog.rst 中Improved Documentation下方使用-的效果一致。
2.2 空版本兜底:No significant changes
{% if sections[section] %} ... {% else %} No significant changes. {% endif %}如果某个版本区段下没有任何类别条目,模板不会输出空标题,而是渲染出一句No significant changes.。这是模板内置的兜底文案,保证没有变更的版本也能生成结构完整的说明。
2.3 类别循环:只渲染存在条目的类别
{% for category, val in definitions.items() if category in sections[section] %}definitions来自pyproject.toml中[[tool.towncrier.type]]的声明。if category in sections[section]是一个过滤条件——只有在该区段中实际存在条目的类别才会被渲染。这意味着:
- 没有任何条目的类别不会出现在产物中;
- 类别的渲染顺序严格遵循
definitions的声明顺序(先breaking,最后misc),而非随机顺序。
2.4 类别标题
{{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }}每个类别输出两行:一行是pyproject.toml中为该类别配置的显示名称(例如Bug fixes、New features),一行是与其长度相同的下划线。下划线字符正是 2.1 节中维护的underline变量,从而保证整份文档的标题层级自洽。
2.5 条目渲染:showcontent 分支
这是模板的核心输出部分,根据类别是否配置showcontent分为两条分支。
分支一:showcontent = true(渲染条目文本与 issue 链接)
{% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category]|dictsort(by='value') %} {% set issue_joiner = joiner(', ') %} - {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }} {% endfor %}这段逻辑值得逐项拆解:
dictsort(by='value'):对同一类别下的条目按内容文本排序(Jinja2 的dictsort默认按键排序,此处显式指定按值排序);values是与同一段文本相关联的 issue 编号列表,values|sort对编号做数值排序;issue_joiner = joiner(', '):Jinja2 的joiner工具在首次调用时返回空字符串,之后返回,,用于把同一条文本关联的多个 issue 链接用逗号拼接;{{ value }}形如#14743,而{{ value[1:] }}则去掉#前缀得到纯编号,用于拼进 issue 链接;- RST 引用语法
`文本 <URL>`_生成一个指向对应 issue 的超链接; - 最终输出形如
- `#14743 <issue链接>`_: 变更内容的列表项。
分支二:showcontent = false(仅列出 issue 编号,不含文本)
{% else %} - {{ sections[section][category]['']|sort|join(', ') }} {% endif %}当类别未开启showcontent时,towncrier 把内容存放在 key 为''(空字符串)的槽位里,模板直接将其中的所有编号排序后用,连接,输出形如- #123, #456的一行,不附带任何描述文字。
2.6 类别级空判断
{% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %}这是模板中的第二处兜底:若某类别存在但条目数为零,同样渲染No significant changes.。在当前 pytest 配置中,由于 10 个类别全部声明了showcontent = true,2.5 节的分支二与这里的空判断属于模板自带的通用防御逻辑,在 pytest 的常规发布中较少触发。
三、模板的输入一:新闻片段(newsfragment)规范
模板渲染的原料是 changelog/ 目录下的新闻片段文件。目录入口文档 changelog/README.rst 给出了明确的规范:
命名规则:每个文件命名为<ISSUE>.<TYPE>.rst,其中<ISSUE>是 issue(或 PR)编号,<TYPE>是变更类别。例如:
123.feature.rst456.bugfix.rst
写作要求(changelog/README.rst):
- 使用完整的句子,采用过去时或现在时,并正确使用标点;
- 内容应面向pytest 用户,而不是只对开发者有意义的内部实现细节;
- 推荐的示例写法如:
Improved verbose diff output with sequences.、Terminal summary statistics now use multiple colors.; - towncrier 会完整保留多段落和各类 RST 格式(代码块、列表等),但对除
feature之外的类别,通常保持单段落更简洁。
仓库中现存的片段可以直观印证这套规范,例如:
- changelog/14716.breaking.rst:
-c选项对不存在路径现在会报 usage error; - changelog/14743.feature.rst:diff 输出中新增
+/-符号图例; - changelog/14724.improvement.rst:
--no-summary不再跳过pytest_terminal_summary钩子; - changelog/14834.doc.rst:文档新增第三方插件
pytest-skip-slow; - changelog/14758.misc.rst:内部 node id 改为结构化
NodeId数据类型。
此外,同一 issue 可以有多个片段,例如 changelog/10745.improvement.1.rst 与 changelog/10745.improvement.2.rst,它们最终会被合并到同一条变更记录中。
四、模板的输入二:pyproject.toml 中的类型定义
模板遍历的definitions完全由 pyproject.toml 中的[[tool.towncrier.type]]列表驱动。pytest 共声明了 10 类变更,每类的directory即新闻片段文件名的类型后缀,name是渲染到变更日志中的章节标题:
| directory | name(渲染标题) | 含义 |
|---|---|---|
breaking | Removals and backward incompatible breaking changes | 以破坏性方式移除公共功能 |
deprecation | Deprecations (removal in next major release) | 声明未来的 API 移除与行为变更 |
feature | New features | 面向用户的新功能、新命令行选项 |
improvement | Improvements in existing functionality | 既有功能的改进 |
bugfix | Bug fixes | 缺陷修复 |
vendor | Vendored libraries | 内置依赖的更新 |
doc | Improved documentation | 文档结构与构建的改进 |
packaging | Packaging updates and notes for downstreams | 面向下游的打包与工具链说明 |
contrib | Contributor-facing changes | 影响贡献者体验的变更(测试、文档构建、开发环境) |
misc | Miscellaneous internal changes | 难以归入上述类别的内部变更 |
配置中每个类别都显式设置了showcontent = true,正如 2.5 节所述,这意味着所有类别在渲染时都会输出完整的条目文本与 issue 链接。配置文件中的注释还说明,之所以逐个显式声明类型,是因为 towncrier 本身不允许只覆盖misc的showcontent值,为了清晰与灵活性,pytest 选择把全部类型一次性声明出来。
五、模板的输出:真实渲染效果
将上述模板与配置组合后,一段14743.feature.rst最终会在 doc/en/changelog.rst 中呈现为类似下面的格式(RST 引用语法,URL 由模板中的固定前缀与 issue 编号拼接而成):
New features ------------ - `#14743 <https://github.com/pytest-dev/pytest/issues/14743>`_: `Full diff:` now contains a legend explaining the `+` & `-` symbols in diff output.仓库历史中真实的渲染示例可以参见 doc/en/changelog.rst,其中#12493那条记录即描述了草稿预览集成重构为sphinxcontrib-towncrier扩展的经过,其行内格式与上述模板输出完全吻合:
- `#12493 <issue 链接>`_: The change log draft preview integration has been refactored to use a third party extension ``sphinxcontrib-towncrier``.由此可以看到模板渲染的完整链路:issue 编号 + 文本→ RST 引用链接 → 带章节标题的列表项 → 按版本聚合的完整变更日志。
六、与文档站点的集成:草稿预览
_template.rst的产出并不会等到正式发版才可见。pytest 通过 Sphinx 扩展sphinxcontrib-towncrier在文档构建期间提供变更日志草稿预览。
在 doc/en/changelog.rst 中可以看到:
To be included in v\ |release| (if present) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT]towncrier-draft-entries指令会在构建文档时,把尚未发布的新闻片段实时渲染并嵌入到「未发布」章节中。相关的扩展配置位于 doc/en/conf.py:
towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-version', 'sphinx-release' towncrier_draft_include_empty = True towncrier_draft_working_directory = PROJECT_ROOT_DIR towncrier_draft_config_path = "pyproject.toml" # relative to cwd这些选项分别控制草稿版本号的推断方式(此处为draft)、是否包含空区段、工作目录以及 towncrier 配置文件的路径。依赖方面,doc/en/requirements.txt 中声明了sphinxcontrib-towncrier与sphinx-issues(后者提供:issue:、:pr:等 GitHub 相关角色)。
对贡献者而言,最直接的验证方式是运行tox -e docs构建文档,然后在doc/en/_build/html/changelog.html中预览自己的变更记录在最终发布说明里的样子(见 changelog/README.rst)。
七、贡献者视角:如何添加一条变更记录
结合 CONTRIBUTING.rst 的提交流程指引,贡献者需要变更日志时的完整步骤为:
- 在
changelog/目录下新建文件,命名为<issueid>.<type>.rst,其中type取自十类之一(feature、improvement、bugfix、doc、deprecation、breaking、vendor、packaging、contrib、misc); - 如果改动不影响 pytest 的文档化行为,可以跳过此步骤;
- 若改动修复了某个 issue,文件名直接使用该 issue 编号;若当时没有对应 issue,可在 PR 提交后改用 PR 编号;
- 不确定应选择哪种类别时,直接在 PR 中询问维护者即可。
从模板与配置的关系来看,贡献者每新增一个文件,就相当于为sections数据源注入一条记录;towncrier 在发版时会自动完成排序、链接化与章节归并,最终呈现为 doc/en/changelog.rst 中一份结构统一、层级分明的发布说明。
小结
changelog/_template.rst以约 40 行 Jinja2 代码,完整定义了 pytest 变更日志的渲染规则:section 遍历与 RST 标题层级、类别过滤与排序、showcontent双分支条目渲染、issue 链接拼接,以及两处No significant changes.兜底。它的一端连接 pyproject.toml 中 10 类变更类型的声明,另一端通过 doc/en/changelog.rst 与 Sphinx 文档站点相连,并借助sphinxcontrib-towncrier提供实时草稿预览。理解这份模板,就等于掌握了 pytest 从「一行变更记录」到「一份可读性极高的发布说明」的全部生成机制。
【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考