SciPy 文档贡献指南:从本地 Sphinx 渲染到写作规范与 CI 校验全解析

原创2026-09-21 16:40:101,731 阅读
文章标签:科学计算

SciPy 文档贡献指南:从本地 Sphinx 渲染到写作规范与 CI 校验全解析

本文面向希望向 SciPy 仓库提交文档修改(修复 docstring 错误、补充示例、改进措辞)的开发者,系统讲解 SciPy 官方文档贡献规范:如何判断哪些缺陷值得优先修复、如何用 spin docs 在本地渲染文档、如何在 PR 中通过 CircleCI 云端预览,以及 docstring 写作必须遵守的 Sphinx 标记与随机数示例约定。读完本文,你将掌握一套可复现的文档修改—校验—提交完整工作流,并理解其背后的构建配置与源码实现细节。

文档缺陷的优先级:先修什么,后修什么

SciPy 社区对文档缺陷的处理是有明确优先级排序的,这直接决定了你的贡献被合并的速度与概率。

最高优先级:技术性错误与结构性问题

技术性错误(technical inaccuracies) 是最高优先级的修复对象,例如:

  • docstring 中遗漏了某个参数说明;
  • 对函数、参数或方法的描述有误;
  • 数学公式、返回值类型或异常行为说明不准确。

这类错误“易于确认、易于落实”(easy to confirm and put in place),维护者可以快速验证修复的正确性并合入。

结构性问题(structural defects) 同样享受高优先级,最典型的代表就是失效链接(broken links)。由于 SciPy 文档体量庞大(docstring 数量以万计),链接指向的模块、函数或段落一旦被重命名或移动,很容易产生死链,这类修复同样是确定性强的机械性工作。

如果你熟悉 Git 工作流,可以直接按 编辑工作流 的说明提交一个包含修复的 pull request(PR);否则,请通过打开 issue 的方式上报问题。

中低优先级:错别字与措辞调整

错别字(typos and misspellings) 欢迎上报,但优先级较低,维护者不一定能及时处理。它们同样可以走 PR 或 issue 两条通道。

措辞(wording) 类修改则需要格外谨慎,它被归为“需要判断力”的改动,审查门槛更高。文档原文明确定义的“不可取”措辞修改包括:

  • 试图“修正”所有牛津逗号(Oxford comma)的使用或缺失;
  • 试图“修正”处于演变中的常见用法,例如 "comprised of" 是否规范。

最容易通过审查的修改,是那些原文明确无误地出错的地方;而需要微妙编辑判断的改动,最好避免。需要注意的是,这不包括“为了让说明更清晰而重写令人困惑的表述”这类常规文档维护工作——后者仍然是欢迎的。

批量提交与 CI 跳过约定

文档贡献还有一个重要的实操约定:

尽量把小而琐碎的改动(如错别字)积累起来一次性提交,而不是一个改动发一个 PR。

同时,纯文档修改应该使用正确的提交信息来跳过 CI 检查。具体的 skip 标记定义在 continuous_integration.rst 中,例如:

  • [skip circle]:跳过 CircleCI(文档渲染检查就在 CircleCI 上);
  • [skip ci]:跳过所有 CI 任务。

这可以避免为一行错别字修复付出完整的 CI 排队成本。

C/Fortran 扩展模块的 docstring 定位技巧

SciPy 有大量函数/对象定义在 C 或 Fortran 扩展模块中,它们的 docstring 与实现代码是分离的(例如 scipy/special/_specfun.pyx、scipy/special/_basic.py 这类包装层与底层 Fortran 的关系)。当你想要查找某个函数的 docstring 时,不要假设它就在同名 .py 文件里,而应使用 grep 或类似工具在整个仓库中搜索该函数的文档字符串。

本地渲染文档:用 spin docs 构建 HTML

SciPy 的 docstring 通过 Sphinx 与 PyData Sphinx theme 渲染为 HTML。编写 docstring 的规范由 NumPy 文档规范(numpydoc 约定)定义;而本文档所解决的问题是:如何验证你的 docstring 修改确实能被正确渲染。

提示:官方还提供了一段视频教程《Rendering SciPy Documentation with Sphinx》作为补充材料。

前置条件:可用的 SciPy 构建

在渲染文档之前,你必须已经拥有一套可正常工作的 SciPy 源码构建(即从源码编译安装的 SciPy)。构建方法参见文档的 building 相关章节。如果本地的 SciPy 是损坏的或指向了错误的安装位置,后续构建出的文档将与仓库当前代码不一致(详见下文“版本不匹配”的排查)。

三步构建流程

  1. 确保 SciPy 构建成功(参见 构建说明)。

  2. 在仓库根目录运行:

    spin docs
    

    首次构建会花费较长时间(需要生成 autosummary、运行 doctest、执行示例代码等),但后续构建通常会显著加快(Sphinx 的增量构建机制只重编变更部分)。

  3. 打开输出目录 doc/build/html/,从 index.html 开始浏览,或者直接跳转到你关心的具体页面。

常见陷阱:增量构建不生效与版本错位

原文档专门用 note 强调了两个高频坑:

坑一:某些文档改动在重建时不被生效。 如果修改了 source/reference/generated 下的自动生成文件,或某些缓存类内容,普通的 spin docs 重建可能不会体现改动。此时需要从零构建,两种方式任选其一:

  • 手动删除 scipy/doc/build 与 source/reference/generated 两个目录后重新构建;
  • 运行 spin docs clean,然后再次构建文档。

坑二:SciPy 版本不一致。 构建时如果遇到类似如下输出:

installed scipy 5fd20ec1aa != current repo git version '35fd20ec1a'

说明 Sphinx 进程 import 到的 SciPy 与当前仓库 git 提交不一致,你很可能在使用错误的 SciPy 安装。立即用下面命令确认 import 路径:

python -c "import scipy; print(scipy.__file__)"

确保 scipy.__file__ 指向你从源码构建的版本(而非 pip 安装的旧版本)。

交互式示例:jupyterlite-sphinx

SciPy 文档包含大量交互式示例,它们借助 jupyterlite-sphinx 扩展渲染成可在浏览器中直接运行的 JupyterLite 会话。这项能力在 conf.py 中配置:

  • myst_nb 与 jupyterlite_sphinx 均被注册在 extensions 列表(见 doc/source/conf.py);
  • nb_execution_mode = "cache" 表示 notebook 执行结果会缓存,避免重复运行;
  • jupyterlite_overrides = "overrides.json" 允许在运行时覆盖 JupyterLite 设置;
  • global_enable_try_examples = True 为所有示例启用 "Try it in your browser!" 按钮。

如果你想为某个 notebook 启用交互式渲染,可参考 adding_notebooks.rst;交互式文档的整体设计见交互文档相关章节。

云端检查:PR 中的 CircleCI 文档预览

如果你提交了 PR,不必只依赖本地构建——SciPy 会在 CI 上自动渲染文档并生成可预览的产物。

云端检查的完整步骤:

  1. 登录 GitHub;
  2. 使用 GitHub 账号登录 CircleCI(首次需要授权);
  3. 回到 PR 页面,在底部找到 "Show all Checks"(显示所有检查项);
  4. 找到 "Check the rendered docs here!" 这一项,点击旁边的 "Details"(详情),即可在 CircleCI 提供的 URL 上查看本次 PR 对应的完整渲染文档。

这意味着每一位贡献者都可以在合并前确认自己的 docstring 改动在 HTML 中呈现正确,不必依赖本地环境。

文档写作规范:docstring 必须遵守的硬性约定

以下规范摘自文档的 "Documentation Guidelines" 部分,是 SciPy docstring 评审时的检查要点,也代表了 numpydoc 风格在实际大型科学计算项目中的最佳实践。

用 "must" 而非 "should" 表达强制约束

当描述输入参数的强制条件时,must 优于 should——对多数英语使用者而言,must 传达的约束强度更高("I must have oxygen to live" vs "I should exercise more")。

正确写法:

Parameters
----------
x : float
    `x` must be nonnegative.

错误写法:

Parameters
----------
x : float
    `x` should be nonnegative.

versionadded 标记的放置位置

versionadded 标记用于标注 API 引入的版本,其位置有两条明确规则:

  • 新函数:versionadded 放在 docstring 的 Notes 部分,而不是开头描述中;
  • 已有函数新增参数:versionadded 放在 Parameters 部分该参数描述文字的末尾。

这样做的目的是让版本信息贴近它所描述的对象,避免污染函数的总体介绍。

References 中引用 Wikipedia 的格式

References 部分允许引用 Wikipedia 文章,但引用格式必须包含文章标题 + "Wikipedia"(相当于期刊名)+ URL:

正确写法:

.. [1] "Zeta Distribution", Wikipedia,
       https://en.wikipedia.org/wiki/Zeta_distribution

错误写法(只有裸 URL):

.. [1] https://en.wikipedia.org/wiki/Zeta_distribution

DOI 与 arXiv 专用标记

References 中强烈推荐使用 DOI,Sphinx 提供了专用标记 :doi::

.. [2] D. Fishkind, S. Adali, H. Patsolic, L. Meng, D. Singh, V. Lyzinski,
       C. Priebe, "Seeded graph matching", Pattern Recognit. 87 (2019):
       203-215, :doi:`10.1016/j.patcog.2018.09.014`

arXiv 文章同样有专用标记 :arxiv:。使用这些结构化标记能让引用在渲染后自动成为可点击链接,也便于机器解析。

项目符号列表的 Sphinx 缩进规则

项目符号列表(bulleted lists)的缩进错误非常常见,规则如下:

  • 前一行不要以 :: 结尾;
  • 列表项不要缩进;
  • 列表前后必须各留一个空行。

正确写法:

Some text that precedes this interesting list:

* The first item in the list.
* The second item in the list.
* You get the idea.

Some text that follows the list.

两种常见错误写法分别是“列表项被缩进”和“列表前后无空行”,都会导致 Sphinx 无法正确解析列表结构。

示例必须自包含(self-contained)

每个 "Examples" 部分(无论 docstring 还是普通文档)都必须自包含:

  • 所有 import 必须显式写出;
  • 用到的数据必须在示例内定义;
  • 代码应当“复制粘贴到一个全新的 Python 解释器中就能直接运行”。

正确写法:

>>> import numpy as np
>>> rng = np.random.default_rng()

错误写法(缺少 import):

>>> rng = np.random.default_rng()

同时,文档鼓励在代码块之间穿插解释文字,但每个代码块与解释文字之间必须用空行分隔:

Some initial text

>>> import numpy as np
>>> rng = np.random.default_rng()

This is some explanation

>>> rng.random(10)

示例与随机性:必须使用 np.random.default_rng

在 SciPy 的 CI 中,示例(doctest)会被真正执行,其输出会与文档提供的参考输出比对。这样做的目的是验证示例本身的正确性(例如 API 是否已变化),而不是把 doctest 当作底层实现单元测试。

因此,示例中若需要随机数生成器,必须使用 np.random.Generator,规范创建方式是 np.random.default_rng:

正确写法:

>>> import numpy as np
>>> rng = np.random.default_rng()
>>> sample = rng.random(10)

显式播种也是允许的(种子必须用 np.random.SeedSequence().entropy 生成,避免使用常见数字):

>>> import numpy as np
>>> rng = np.random.default_rng(102524723947864966825913730119128190984)
>>> sample = rng.random(10)

错误写法(使用旧的全局随机接口):

>>> import numpy as np
>>> sample = np.random.random(10)

渲染时的种子隐藏机制

这里有一个容易忽略的细节:无论是否显式播种,渲染后的文档都不会显示种子。当 doctest 被执行而你没有显式提供种子时,默认使用固定值 1638083107694713882823079058616272161。这一设计的意图是鼓励用户在代码中显式决策是否使用种子,而不是直接复制粘贴文档里的种子。

这个机制在源码中有完整实现,见 scipy/_lib/_util.py 的 _fixed_default_rng 上下文管理器——它在测试环境下将 np.random.default_rng 替换为固定种子的版本;而 scipy/_lib/_util.py 的 _rng_html_rewrite 装饰器则通过正则 np.random.default_rng\((0x<a href="https://link.gitcode.com/i/d170c785189dd447b522768197350e50" target="_blank">0-9A-F]+|\d+)\) 将渲染出的种子替换为空参数形式。该装饰器在 [doc/source/conf.py 中被导入并作用于 numpydoc 的示例渲染流程,这正是“文档里看不到种子”的底层原因。

由此产生的后果是:用户无法精确复现随机示例的输出,所以包含随机数据的示例不应依赖或引用基于随机数据的具体数值来做论证。

legacy 指令:标记弃用但保留的 API

如果一个函数、模块或 API 处于 legacy 模式(为了向后兼容而保留,但不推荐在新代码中使用),可以在文档中使用 .. legacy:: 指令。

无参数用法会生成默认的弃用提示。强烈建议附加自定义消息,例如推荐替代的新 API,该消息会追加在默认提示之后:

.. legacy::

   New code should use :mod:`scipy.fft`.

如果要标注的不是子模块,而是函数、方法或任意自定义对象,可以使用可选参数:

.. legacy:: function

该指令由 Sphinx 在构建时展开为带样式的提示框,帮助用户在 API 文档中一眼识别出“旧接口”。

构建配置速览:文档系统的幕后组件

理解了上述规范,再看支撑它们的构建配置 doc/source/conf.py:

配置项 值 作用
extensions numpydoc、sphinx.ext.autodoc、matplotlib.sphinxext.plot_directive、myst_nb、jupyterlite_sphinx、doi_role 等 决定 docstring 解析、图表渲染、notebook 执行与交互式示例的能力
plot_formats [('png', 96)] 示例绘图输出为 96 dpi 的 PNG
nb_execution_mode "cache" notebook 示例执行结果缓存,加速重复构建
global_enable_try_examples True 全局启用 "Try it in your browser!" 交互按钮
default_role "autolink" 文档中的裸 `text` 自动解析为交叉引用

同时,SciPy 自带的 scipyoptdoc 扩展和 doi_role(位于 doc/source/doi_role.py)分别负责 SciPy 特有的签名展示与 DOI 引用渲染。

总结:一份可执行的文档贡献清单

把以上内容浓缩为提交文档修改前的自检清单:

  1. 判断优先级:技术错误与死链 > 错别字 > 措辞调整;小改动尽量攒批提交,纯文档提交使用 [skip ci] 类标记;
  2. 修改 docstring:遵守 must/should、versionadded 位置、Wikipedia/DOI 引用格式、列表缩进、示例自包含等规范;C/Fortran 扩展的 docstring 要用 grep 全局搜索定位;
  3. 本地验证:确认有可用的 SciPy 源码构建后运行 spin docs,在 doc/build/html/ 检查渲染结果;遇到缓存问题执行 spin docs clean,遇到版本错位检查 scipy.__file__;
  4. 云端验证:提交 PR 后,在 GitHub 上通过 CircleCI 的 "Check the rendered docs here!" 查看云端渲染预览;
  5. 随机示例:一律使用 np.random.default_rng(),明确播种时用 SeedSequence().entropy 生成种子,并确保示例不依赖随机数据的精确数值。

遵循这套流程,你的文档贡献就能以最低的往返成本被维护者确认与合并。

登录后查看全文
scipy