pytest 变更日志生成机制:changelog/_template.rst 模板与 towncrier 新闻片段工作流解析
2026/9/14 11:33:21 网站建设 项目流程

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,因此区段名为空字符串)。这段逻辑的含义是:

  1. 每轮循环先把underline初始化为-
  2. 若区段名非空,则输出区段名,并用等长数量的-作为 RST 标题下划线(RST 要求下划线长度与标题一致);
  3. 紧接着把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 fixesNew 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.rst
  • 456.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是渲染到变更日志中的章节标题:

directoryname(渲染标题)含义
breakingRemovals and backward incompatible breaking changes以破坏性方式移除公共功能
deprecationDeprecations (removal in next major release)声明未来的 API 移除与行为变更
featureNew features面向用户的新功能、新命令行选项
improvementImprovements in existing functionality既有功能的改进
bugfixBug fixes缺陷修复
vendorVendored libraries内置依赖的更新
docImproved documentation文档结构与构建的改进
packagingPackaging updates and notes for downstreams面向下游的打包与工具链说明
contribContributor-facing changes影响贡献者体验的变更(测试、文档构建、开发环境)
miscMiscellaneous internal changes难以归入上述类别的内部变更

配置中每个类别都显式设置了showcontent = true,正如 2.5 节所述,这意味着所有类别在渲染时都会输出完整的条目文本与 issue 链接。配置文件中的注释还说明,之所以逐个显式声明类型,是因为 towncrier 本身不允许只覆盖miscshowcontent值,为了清晰与灵活性,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-towncriersphinx-issues(后者提供:issue::pr:等 GitHub 相关角色)。

对贡献者而言,最直接的验证方式是运行tox -e docs构建文档,然后在doc/en/_build/html/changelog.html中预览自己的变更记录在最终发布说明里的样子(见 changelog/README.rst)。

七、贡献者视角:如何添加一条变更记录

结合 CONTRIBUTING.rst 的提交流程指引,贡献者需要变更日志时的完整步骤为:

  1. changelog/目录下新建文件,命名为<issueid>.<type>.rst,其中type取自十类之一(featureimprovementbugfixdocdeprecationbreakingvendorpackagingcontribmisc);
  2. 如果改动不影响 pytest 的文档化行为,可以跳过此步骤;
  3. 若改动修复了某个 issue,文件名直接使用该 issue 编号;若当时没有对应 issue,可在 PR 提交后改用 PR 编号;
  4. 不确定应选择哪种类别时,直接在 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询