扩展构建过程

本教程的目标是创建一个比 使用角色和指令扩展语法 .而该指南只是涵盖了编写自定义 roledirective ,本指南涵盖了Sphinx构建过程的更复杂的扩展;添加多个指令,以及自定义节点,附加配置值和自定义事件处理程序。

为此,我们将涵盖 todo 扩展程序添加了在文档中包含待办事项条目并在中心位置收集这些条目的功能。这类似于 sphinx.ext.todo 扩展与Sphinx一起分发。

概述

备注

要了解此扩展的设计,请参阅 重要对象Build phases .

我们希望扩展将以下内容添加到Sphinx:

  • A todo 指令,包含一些标记有“TODO”的内容,并且只有在设置了新的配置值时才会在输出中显示。默认情况下,Todo条目不应出现在输出中。

  • A todolist 该指令创建整个文档中所有待办事项的列表。

为此,我们需要将以下元素添加到Sphinx:

  • 新指令,称为 todotodolist .

  • 新的文档树节点来表示这些指令,通常也称为 todotodolist . 如果新指令只产生一些可由现有节点表示的内容,我们就不需要新节点。

  • 新的配置值 todo_include_todos (配置值名称应以扩展名开头,以保持唯一性),用于控制todo条目是否进入输出。

  • 新事件处理程序:一个用于 doctree-resolved 事件,替换todo和todolist节点,一个用于 env-merge-info 合并来自并行生成的中间结果,一个用于 env-purge-doc (the原因将在后面讨论)。

先决条件

如同 使用角色和指令扩展语法 ,我们不会通过PyPI分发这个插件,因此我们再次需要一个Sphinx项目来调用这个插件。您可以使用现有项目或使用创建新项目 sphinx-quickstart .

我们假设您正在使用单独的来源 (source )并建造 (build )文件夹。您的扩展名文件可以位于项目的任何文件夹中。在我们的例子中,让我们执行以下操作:

  1. 创建一个 _ext 文件夹 source

  2. _ext 文件夹被称为 todo.py

以下是您可能获得的文件夹结构的示例:

└── source
    ├── _ext
    │   └── todo.py
    ├── _static
    ├── conf.py
    ├── somefolder
    ├── index.rst
    ├── somefile.rst
    └── someotherfile.rst

编写扩展

开放 todo.py 并将以下代码粘贴在其中,所有这些我们将很快详细解释:

  1from docutils import nodes
  2from docutils.parsers.rst import Directive
  3
  4from sphinx.application import Sphinx
  5from sphinx.locale import _
  6from sphinx.util.docutils import SphinxDirective
  7from sphinx.util.typing import ExtensionMetadata
  8
  9
 10class todo(nodes.Admonition, nodes.Element):
 11    pass
 12
 13
 14class todolist(nodes.General, nodes.Element):
 15    pass
 16
 17
 18def visit_todo_node(self, node):
 19    self.visit_admonition(node)
 20
 21
 22def depart_todo_node(self, node):
 23    self.depart_admonition(node)
 24
 25
 26class TodolistDirective(Directive):
 27    def run(self):
 28        return [todolist('')]
 29
 30
 31class TodoDirective(SphinxDirective):
 32    # this enables content in the directive
 33    has_content = True
 34
 35    def run(self):
 36        targetid = 'todo-%d' % self.env.new_serialno('todo')
 37        targetnode = nodes.target('', '', ids=[targetid])
 38
 39        todo_node = todo('\n'.join(self.content))
 40        todo_node += nodes.title(_('Todo'), _('Todo'))
 41        todo_node += self.parse_content_to_nodes()
 42
 43        if not hasattr(self.env, 'todo_all_todos'):
 44            self.env.todo_all_todos = []
 45
 46        self.env.todo_all_todos.append({
 47            'docname': self.env.docname,
 48            'lineno': self.lineno,
 49            'todo': todo_node.deepcopy(),
 50            'target': targetnode,
 51        })
 52
 53        return [targetnode, todo_node]
 54
 55
 56def purge_todos(app, env, docname):
 57    if not hasattr(env, 'todo_all_todos'):
 58        return
 59
 60    env.todo_all_todos = [
 61        todo for todo in env.todo_all_todos if todo['docname'] != docname
 62    ]
 63
 64
 65def merge_todos(app, env, docnames, other):
 66    if not hasattr(env, 'todo_all_todos'):
 67        env.todo_all_todos = []
 68    if hasattr(other, 'todo_all_todos'):
 69        env.todo_all_todos.extend(other.todo_all_todos)
 70
 71
 72def process_todo_nodes(app, doctree, fromdocname):
 73    if not app.config.todo_include_todos:
 74        for node in doctree.findall(todo):
 75            node.parent.remove(node)
 76
 77    # Replace all todolist nodes with a list of the collected todos.
 78    # Augment each todo with a backlink to the original location.
 79    env = app.env
 80
 81    if not hasattr(env, 'todo_all_todos'):
 82        env.todo_all_todos = []
 83
 84    for node in doctree.findall(todolist):
 85        if not app.config.todo_include_todos:
 86            node.replace_self([])
 87            continue
 88
 89        content = []
 90
 91        for todo_info in env.todo_all_todos:
 92            para = nodes.paragraph()
 93            filename = env.doc2path(todo_info['docname'], base=None)
 94            description = _(
 95                '(The original entry is located in %s, line %d and can be found '
 96            ) % (filename, todo_info['lineno'])
 97            para += nodes.Text(description)
 98
 99            # Create a reference
100            newnode = nodes.reference('', '')
101            innernode = nodes.emphasis(_('here'), _('here'))
102            newnode['refdocname'] = todo_info['docname']
103            newnode['refuri'] = app.builder.get_relative_uri(
104                fromdocname, todo_info['docname']
105            )
106            newnode['refuri'] += '#' + todo_info['target']['refid']
107            newnode.append(innernode)
108            para += newnode
109            para += nodes.Text('.)')
110
111            # Insert into the todolist
112            content.extend((
113                todo_info['todo'],
114                para,
115            ))
116
117        node.replace_self(content)
118
119
120def setup(app: Sphinx) -> ExtensionMetadata:
121    app.add_config_value('todo_include_todos', False, 'html')
122
123    app.add_node(todolist)
124    app.add_node(
125        todo,
126        html=(visit_todo_node, depart_todo_node),
127        latex=(visit_todo_node, depart_todo_node),
128        text=(visit_todo_node, depart_todo_node),
129    )
130
131    app.add_directive('todo', TodoDirective)
132    app.add_directive('todolist', TodolistDirective)
133    app.connect('doctree-resolved', process_todo_nodes)
134    app.connect('env-purge-doc', purge_todos)
135    app.connect('env-merge-info', merge_todos)
136
137    return {
138        'version': '0.1',
139        'env_version': 1,
140        'parallel_read_safe': True,
141        'parallel_write_safe': True,
142    }

这是比中详细介绍的扩展广泛得多 使用角色和指令扩展语法 但是,我们将逐步查看每个部分,以解释发生了什么。

节点类

让我们从节点类开始:

 1
 2
 3class todo(nodes.Admonition, nodes.Element):
 4    pass
 5
 6
 7class todolist(nodes.General, nodes.Element):
 8    pass
 9
10
11def visit_todo_node(self, node):
12    self.visit_admonition(node)
13
14

除了继承中定义的标准docutils类之外,节点类通常不需要做任何事情 docutils.nodes . todo 继承自 Admonition 因为它应该像纸条或警告一样处理, todolist 只是一个“一般”节点。

备注

许多扩展不必创建自己的节点类,并且可以与已提供的节点一起正常工作 docutilsSphinx .

注意

重要的是要知道,虽然您可以在不离开您的情况下扩展Sphinx conf.py ,如果您在那里声明继承的节点,您会遇到一个不明显的 PickleError .因此,如果出现问题,请确保将继承的节点放入单独的Python模块中。

有关更多详细信息,请参阅:

指令类

指令类是通常从 docutils.parsers.rst.Directive .中也详细介绍了指令接口 docutils documentation ;重要的是类应该具有配置允许标记的属性,以及 run 返回节点列表的方法。

首先看 TodolistDirective 指令:

1
2
3class TodolistDirective(Directive):
4    def run(self):

这非常简单,创建并返回我们的 todolist 节点类。 的 TodolistDirective 指令本身既没有需要处理的内容,也没有需要处理的参数。这就把我们带到了 TodoDirective 指令:

 1
 2class TodoDirective(SphinxDirective):
 3    # this enables content in the directive
 4    has_content = True
 5
 6    def run(self):
 7        targetid = 'todo-%d' % self.env.new_serialno('todo')
 8        targetnode = nodes.target('', '', ids=[targetid])
 9
10        todo_node = todo('\n'.join(self.content))
11        todo_node += nodes.title(_('Todo'), _('Todo'))
12        todo_node += self.parse_content_to_nodes()
13
14        if not hasattr(self.env, 'todo_all_todos'):
15            self.env.todo_all_todos = []
16
17        self.env.todo_all_todos.append({
18            'docname': self.env.docname,
19            'lineno': self.lineno,
20            'todo': todo_node.deepcopy(),
21            'target': targetnode,
22        })
23
24        return [targetnode, todo_node]

这里涵盖了几个重要的事情。首先,如您所见,我们现在正在对 SphinxDirective 助手类而不是通常的 Directive 课这使我们可以访问 build environment instance 使用 self.env 财产如果没有这个,我们就不得不使用相当复杂的 self.state.document.settings.env .然后,充当链接目标(来自 TodolistDirective ), TodoDirective 指令除了返回目标节点 todo node. 目标ID(在HTML中,这将是锚名称)是通过使用生成的 env.new_serialno 它在每次调用时返回一个新的唯一整数,因此会产生唯一的目标名称。目标节点在没有任何文本(前两个参数)的情况下被实例化。

创建警告节点时,使用 self.state.nested_parse . 第一个参数给出内容主体,第二个参数给出内容偏移量。 第三个参数给出了解析结果的父节点,在我们的例子中, todo node.随后, todo 将节点添加到环境中。 需要这样才能在作者放置 todolist 指令。 对于这种情况,环境属性 todo_all_todos 被使用(同样,名称应该是唯一的,所以它的开头是扩展名)。 当创建新环境时,它不存在,因此指令必须检查并创建它(如有必要)。 有关todo条目位置的各种信息与节点的副本一起存储。

在最后一行中,返回了应该放入doctree中的节点:目标节点和警告节点。

指令返回的节点结构如下::

+--------------------+
| target node        |
+--------------------+
+--------------------+
| todo node          |
+--------------------+
  \__+--------------------+
     | admonition title   |
     +--------------------+
     | paragraph          |
     +--------------------+
     | ...                |
     +--------------------+

事件处理程序

事件处理程序是Sphinx最强大的功能之一,它提供了一种挂钩到文档过程的任何部分的方法。Sphinx本身提供了许多事件,详细信息见 the API guide ,我们将在这里使用它们的一个子集。

让我们看看上面示例中使用的事件处理程序。 首先,那个 env-purge-doc 活动:

1def purge_todos(app, env, docname):
2    if not hasattr(env, 'todo_all_todos'):
3        return
4
5    env.todo_all_todos = [
6        todo for todo in env.todo_all_todos if todo['docname'] != docname

由于我们将源文件中的信息存储在环境中,并且是持久的,因此当源文件更改时,它可能会过时。 因此,在读取每个源文件之前,环境的记录将被清除,并且 env-purge-doc 活动为扩展提供了同样的机会。在这里,我们从 todo_all_todos 名单 如果文档中剩余有todo,则会在解析期间再次添加它们。

下一个处理程序,用于 env-merge-info 事件,在并行构建期间使用。与并行构建期间一样,所有线程都有自己的线程 env ,有多个 todo_all_todos 需要合并的列表:

1
2def merge_todos(app, env, docnames, other):
3    if not hasattr(env, 'todo_all_todos'):
4        env.todo_all_todos = []
5    if hasattr(other, 'todo_all_todos'):

另一个处理程序属于 doctree-resolved 活动:

 1
 2def process_todo_nodes(app, doctree, fromdocname):
 3    if not app.config.todo_include_todos:
 4        for node in doctree.findall(todo):
 5            node.parent.remove(node)
 6
 7    # Replace all todolist nodes with a list of the collected todos.
 8    # Augment each todo with a backlink to the original location.
 9    env = app.env
10
11    if not hasattr(env, 'todo_all_todos'):
12        env.todo_all_todos = []
13
14    for node in doctree.findall(todolist):
15        if not app.config.todo_include_todos:
16            node.replace_self([])
17            continue
18
19        content = []
20
21        for todo_info in env.todo_all_todos:
22            para = nodes.paragraph()
23            filename = env.doc2path(todo_info['docname'], base=None)
24            description = _(
25                '(The original entry is located in %s, line %d and can be found '
26            ) % (filename, todo_info['lineno'])
27            para += nodes.Text(description)
28
29            # Create a reference
30            newnode = nodes.reference('', '')
31            innernode = nodes.emphasis(_('here'), _('here'))
32            newnode['refdocname'] = todo_info['docname']
33            newnode['refuri'] = app.builder.get_relative_uri(
34                fromdocname, todo_info['docname']
35            )
36            newnode['refuri'] += '#' + todo_info['target']['refid']
37            newnode.append(innernode)
38            para += newnode
39            para += nodes.Text('.)')
40
41            # Insert into the todolist
42            content.extend((
43                todo_info['todo'],

doctree-resolved 事件在结束时发出 phase 3 (resolving) 并允许进行自定义解析。我们为此事件编写的处理程序涉及更多。如果 todo_include_todos 配置值(我们将很快描述)为假,全部 todotodolist 从文档中删除节点。如果没有, todo 节点只是保持它们的位置和状态。 todolist 节点被todo条目的列表所取代,并带有指向它们来自的位置的反向链接。 列表项由 todo 动态创建的条目和docutils节点:每个条目有一个段落,包含给出位置的文本,以及带有反向引用的链接(包含斜线节点的引用节点)。引用URI由以下人员构建 sphinx.builders.Builder.get_relative_uri() 它根据使用的构建器创建合适的URL,并附加todo节点(目标)的ID作为锚点名称。

setup 功能

如所指出 previouslysetup 函数是必需的,用于将指令插入Sphinx。然而,我们也使用它来连接扩展的其他部分。让我们看看我们 setup 功能:

 1
 2        node.replace_self(content)
 3
 4
 5def setup(app: Sphinx) -> ExtensionMetadata:
 6    app.add_config_value('todo_include_todos', False, 'html')
 7
 8    app.add_node(todolist)
 9    app.add_node(
10        todo,
11        html=(visit_todo_node, depart_todo_node),
12        latex=(visit_todo_node, depart_todo_node),
13        text=(visit_todo_node, depart_todo_node),
14    )
15
16    app.add_directive('todo', TodoDirective)
17    app.add_directive('todolist', TodolistDirective)
18    app.connect('doctree-resolved', process_todo_nodes)
19    app.connect('env-purge-doc', purge_todos)
20    app.connect('env-merge-info', merge_todos)
21
22    return {
23        'version': '0.1',
24        'env_version': 1,
25        'parallel_read_safe': True,
26        'parallel_write_safe': True,
27    }

该函数中的调用引用了我们之前添加的类和函数。各个呼叫的功能如下:

  • add_config_value() 让Sphinx知道它应该识别新的 config value todo_include_todos ,其默认值应为 False (this还告诉Sphinx它是布尔值)。

    如果第三个论点是 'html' ,如果配置值更改了其值,则将完全重建HTML文档。 对于影响读取(构建)的配置值来说,这是需要的 phase 1 (reading) ).

  • add_node() 添加新 node class 到构建系统。 它还可以为每个支持的输出格式指定访问者函数。 当新节点停留到 phase 4 (writing) .以来 todolist 节点始终被替换 phase 3 (resolving) ,它不需要任何。

  • add_directive() 添加新 directive ,按姓名和阶级给出。

  • 最后, connect() 增加了一个 event handler 到名称由第一个参数给出的事件。 事件处理程序函数是用多个参数调用的,这些参数与事件一起记录。

这样,我们的扩展就完成了。

使用扩展

与之前一样,我们需要通过在 conf.py 文件.这里需要两个步骤:

  1. 添加 _ext 目录到 Python path 使用 sys.path.append .这应该放在文件的顶部。

  2. 更新或创建 extensions 列表并将扩展文件名添加到列表中

此外,我们不妨设置 todo_include_todos 配置值。如上所述,这默认为 False 但我们可以明确地设置它。

例如:

import sys
from pathlib import Path

sys.path.append(str(Path('_ext').resolve()))

extensions = ['todo']

todo_include_todos = False

现在,您可以在整个项目中使用该扩展包。例如:

index.rst
Hello, world
============

.. toctree::
   somefile.rst
   someotherfile.rst

Hello world. Below is the list of TODOs.

.. todolist::
somefile.rst
foo
===

Some intro text here...

.. todo:: Fix this
someotherfile.rst
bar
===

Some more text here...

.. todo:: Fix that

因为我们已经配置了 todo_include_todosFalse ,我们实际上不会看到为 todotodolist 指令。然而,如果我们将其切换为true,我们将看到之前描述的输出。

进一步阅读

有关更多信息,请参阅 docutils 文件和 SphinxAPI .

如果您希望跨多个项目或与其他人共享您的扩展,请查看 第三方扩展 科.