使用 Sphinx 为项目自动生成 API 文档

对于一个优秀的开源项目,高质量的文档与代码同样重要,糟糕的文档会将大部分潜在用户拒之门外。

从流行的开源项目中,我们可以总结出一些关于文档的最佳实践:

  • 文档和代码库在一个仓库中维护以做到同步更新(个别体量超大的项目例外)
  • 使用纯文本格式编写文档,如 markdownreStructText,然后通过生成工具将其转换成易于阅读且样式丰富的静态网页,进一步可通过 CI 将静态网页托管到可公开访问的站点
  • 在文档中直接引用实际的代码,并提供 API 文档

API 文档通常位于文档中的 API ReferenceReferences 章节,它能节省用户查阅源码的时间,帮助用户快速理解项目的内部组成。对于作为库(而非应用)的项目而言,提供组织良好的 API 文档尤为重要,如 FlaskClick 等,本文就将介绍如何使用 Sphinx 为项目自动生成 API 文档。

示例项目所使用的的完整源码已推送到 GitHub 的 demo 仓库中。

为什么是 Sphinx

有许多开源工具可以用来将纯文本转换为静态网页,选择 Sphinx 的理由有以下几点:

  • 经典老牌,使用相当广泛,Python 的官方文档也是用它所生成
  • 功能全面,支持各种主题和插件,和 Python 具有语言上的亲和性
  • 能够很方便地根据代码的文档字符串自动生成 API 文档

Sphinx 的默认文本格式是 reStructuredText(简称 rst),它是一种极其强大的标记语言,配合 Sphinx 可以实现很多 markdown 所不具备的解析、引用特性,缺点是规则和指令较为复杂。这原本并不算缺点,但随着 markdown 的流行,人们似乎无法再接受任何比 markdown 更复杂一点的标记语言了。

不过这并不是问题,我们也可以借助 MyST 解析器实现 Sphinx 和 markdown 的完全兼容,接下来的示例中我们将混合使用 mdrst 两种格式,当然你也可以只使用任意一种格式。

新建文档项目

安装 Sphinx

示例将是一个 Python 语言项目,因此最直接的方式当然是通过 pip 安装:

1
pip install sphinx

对于其他语言及平台, Sphinx 也提供了丰富的安装途径,详见 Installing Sphinx

设置文档源目录

我们将在同一个项目中维护代码和文档,因此首先需要在项目根目录新建一个 docs 文件夹(也可以使用其他名称),用来存放所有和文档有关的文件,我们将使用该文件夹作为 Sphinx 工作的源目录(source directory)。

初始化项目

切换到 docs 目录,执行以下命令在该目录初始化一个新的 Sphinx 文档项目:

1
sphinx-quickstart

接下来根据命令行提示完成初始化项目的各项配置,如填写项目名称和作者,对于不理解的选项回车使用默认选项即可。

初始化后的 docs 目录内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
docs
├── Makefile
├── _build
├── _static
├── _templates
├── conf.py
├── index.rst
└── make.bat

3 directories, 4 files

其中最重要的是 conf.py 文件,它是一个 Sphinx 将会运行的 Python 模块,保存着刚刚通过命令行输入的配置,还可以编辑文件来修改配置或添加插件,并通过该文件运行需要的 Python 代码。

Makefilemake.bat 提供了一些快捷命令,在 docs 目录执行 make html 即可通过源文件生成静态网页。

index.rst 是文档源文件的首页,文档里有一些默认的样板内容,通常我们将其作为访问其他页面的入口目录。

此时已经可以运行 make html 生成静态网页用于预览,生成的 HTML 页面保存在 _build/html 目录。使用浏览器打开后效果如下:

如果对默认的样式不满意,还可以在 Sphinx Themes Gallery 找到大量可以轻松替换的文档主题。

基本使用

初识 reStructuredText

接下来我们借助 index.rst 简单认识一下 reStructuredText 格式。

  1. 标题

    1
    2
    
    Welcome to sphinx-demo's documentation!
    =======================================
    

    在文本的下一行连续输入同一标点符号,即可将文本标记为标题,另外还有很违反直觉的两点要求:

    • 符号的长度需要超出文本
    • 无法显式地指定标题级别,如果解析到另一种符号标记的标题,将根据出现顺序指定为新的标题级别,如
    1
    2
    3
    4
    5
    
    Secondary Title
    ----------------
    
    Third Title
    ~~~~~~~~~~~~
    

    相比 markdown 直接使用符号的个数指示标题级别,这种规则并不算直观。

  2. 目录树

    1
    2
    3
    4
    5
    6
    
    .. toctree::
       :maxdepth: 2
       :caption: Contents:
    
       install
       reference
    

    形如 .. {directive}:: 这样的文本在 rst 中被称为指令,可以实现各种特殊效果,比如 .. toctree:: 将在当前位置插入一颗目录树。在指令的下一行可以通过 :{option}: value 指定选项,如上示例中指定了该目录树的最大深度和标题。

    再往下就是目录树的条目了,Sphinx 将根据条目名称如 install 作为相对路径在源目录即 docs 中寻找名为 install.rstinstall.md 的文件,并生成指向该文件的链接。

  3. 引用

    1
    2
    3
    4
    5
    6
    
    Indices and tables
    ==================
    
    * :ref:`genindex`
    * :ref:`modindex`
    * :ref:`search`
    

    这部分内容包含了 3 种格式:

    • 首先是我们上面介绍过的标题。

    • 然后是和 markdown 类似地通过 * 渲染列表项。

    • 最后是 :ref:`label_name` ,生成一条指向 label_name 标签的交叉引用,这时需要通过 .. _label_name: 指令提前创建一个全局唯一的标签,如为一个标题创建标签:

      1
      2
      3
      4
      
      .. _label_name:
      
      Section to cross-reference
      -------------------------- 
      

我们仅简单介绍几种在 index.rst 中出现过的格式,实际上 reStructuredText 能实现的功能远不限于此。

添加 markdown 支持

为 Sphinx 添加 markdown 支持非常简单。

首先安装 MyST 插件:

1
pip install myst-parser

接着修改 conf.py 文件,找到 extensions = [] 所在的一行,向该列表中添加 "myst_parser"

1
extensions = ["myst_parser"]

之后源目录中所有的 .md 文件就会像 .rst 一样被 Sphinx 正常解析。

添加新的文档内容

  1. 向首页添加新的文本段落

    在已有的标题和指令之外直接插入文本即可,如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    Welcome to sphinx-demo's documentation!
    =======================================
    
    .. toctree::
       :maxdepth: 2
       :caption: Contents:
    
    
    Indices and tables
    ==================
    
    * :ref:`genindex`
    * :ref:`modindex`
    * :ref:`search`
    
    新内容
    ======
    
    这是一个新的段落
    

    生成 HTML 页面如下:

  2. 添加新的 .rst 文件

    docs 目录中创建一个新的 install.rst ,在文件中写入标题,然后在首页文件中添加相应的目录条目或者引用,本例中我们添加到目录树:

    1
    2
    3
    4
    5
    
    .. toctree::   
       :maxdepth: 2   
       :caption: Contents: 
    
       install
    

    添加新文件时需要注意两点:

    • 新文件中需要至少有一个标题,否则不会生成相应的链接,且链接默认会使用该标题作为展示文本。
    • 在引用中添加子目录前缀即可在源目录中通过子目录管理文件。
  3. 添加新的 .md 文件

    我们尝试在 docs 中创建一个子目录 references ,然后在该目录中添加 api_reference.md 文件,接着在 index.rst 文件中添加以下内容引用新添加的文件:

    1
    2
    3
    4
    
    Appendix
    =========
    
    :doc:`references/api_reference`
    

    实质上与添加 .rst 文件完全相同,因此新的文档中也需要包含标题才能生成链接。

最终通过 make html 生成的 HTML 页面效果如下:

生成 API 文档

以上介绍的内容,已经足够为项目编写一般的文本内容文档,最后来看如何通过代码自动生成 API 文档。

以单文件模块作为示例,结果同样适用于多文件的包和模块,假设我们的代码位于根目录下的 main.py 文件(即 main 模块)中,内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Human:
    """Foo class"""

    def __init__(self, gender, name):
        """Make a virtual human.

        :param sex: gender of human.
        :param name: name of human.
        """
        self.gender = gender
        self.name = name

    def speak(self, words):
        """speak some words.

        :param words: words to speak.
        :return: None
        """
        print(words)

    def get_intro(self):
        """get man's introduction.

        :return: self introduction string.
        """
        return f'Name: {self.name};Gender: {self.gender}'

启用扩展

Sphinx 通过 autodoc 扩展导入源代码并解析其文档字符串转换成文档。此外还可通过 viewcode 扩展直接从文档访问 API 对应的源代码页面。

首先编辑 conf.py 文件以启用扩展,在 extensions 配置项中加入两个扩展项:

1
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']

为了获取文档字符串,Sphinx 需要能够导入文档所在的代码模块。也就是说,我们的源码模块必须位于 Sphinx 的导入层级结构中,这有许多种实现途径,本例选择在 conf.py 中将代码模块所在的目录(即 conf.py 文件所在的上一级目录)加入到 sys.path 中,在 conf.py 中加入以下代码:

1
2
3
4
import os
import sys

sys.path.insert(0, os.path.abspath('..')

需要注意被 Sphinx 导入时模块中的代码会被执行一次,因此我们应该隔离模块中的执行逻辑以避免产生副作用,如将其包裹在 if __name__ == '__main__' 代码块中。

导入文档

最后,在 references/api_reference.md 文档中通过 automodule 指令导入 main 模块的文档:

1
2
3
4
5
6
# API Reference

​```{eval-rst}
.. automodule:: main    
    :members:
​```

注意:

  • 除了 automodule ,还可使用 autoclassautofunction 等指令为其他类型的 Python 对象自动生成文档。

  • 为了在 markdown 格式中使用 autodoc ,需要使用 eval-rst 指令对导入文档的指令做一层封装,详见 Use sphinx.ext.autodoc in Markdown files,它等价于 .rst 文件的如下写法:

    .. automodule:: main
    	:members:
    

references/api_reference.md 生成的页面效果如下:

通过页面右侧的 [source] 链接还能够直接从文档页面跳转到相应的源代码:

文档字符串风格

上面的示例代码中,我们使用 reStructuredText 风格编写文档字符串,如果你更喜欢 NumPyGoogle 风格的文档字符串,可以启用 napoleon 扩展与之兼容。napoleon 是一个预处理器,在 autodoc 处理文档字符串之前将其转换为正确的 reStructuredText 文本。

启用方法同样是修改 conf.py 文件的 extensions 配置项:

1
2
3
4
# conf.py

# Add napoleon to the extensions list
extensions = ['sphinx.ext.napoleon']

启用后 autodoc 将能够同时支持 reStructuredText、NumPy 及 Google 三种不同风格的文档字符串。

其他途径

除了 Sphinx ,使用 MkDocs 配合 mkdocstrings 扩展包也能实现自动从源码生成文档。相比之下 MkDocs 配置更加简单,原生支持 markdown 且可以运行预览用的开发服务器,目前在 Python 项目中也非常流行。

参考链接

updatedupdated2022-08-032022-08-03