扩展构建过程¶
本教程的目标是创建一个比 扩展语法与角色和指令 中创建的更全面的扩展.虽然该指南仅涵盖了编写自定义 role 和 directive ,但本指南涵盖了 Sphinx 构建过程的更复杂扩展;添加多个指令,以及自定义节点、额外的配置值和自定义事件处理程序.
为此,我们将介绍一个 todo
扩展,该扩展增加了在文档中包含待办事项条目的功能,并将这些条目集中收集.这类似于与 Sphinx 一同分发的 sphinx.ext.todo
扩展.
概述¶
我们希望该扩展向Sphinx添加以下内容:
一个
todo
指令,包含一些标记为 “TODO” 的内容,仅在设置了新的配置值时在输出中显示.默认情况下,待办事项条目不应出现在输出中.一个
todolist
指令,用于创建文档中所有待办事项条目的列表.
为此,我们需要向 Sphinx 添加以下元素:
新的指令,称为
todo
和todolist
.新的文档树节点用于表示这些指令,通常也称为
todo
和todolist
.如果新的指令仅产生一些可以由现有节点表示的内容,我们就不需要新的节点.一个新的配置值
todo_include_todos
(配置值名称应以扩展名开头,以保持唯一性),用于控制待办事项是否包含在输出中.新的事件处理程序:一个用于
doctree-resolved
事件,以替换待办事项和待办事项列表节点;一个用于env-merge-info
以合并来自并行构建的中间结果;以及一个用于env-purge-doc
(原因将在后面讨论).
前提条件¶
与 扩展语法与角色和指令 一样,我们不会通过 PyPI 发布此插件,因此我们再次需要一个 Sphinx 项目来调用它.您可以使用现有项目或者使用 sphinx-quickstart 创建一个新项目.
我们假设您使用的是单独的源文件 (source
) 和构建文件 (build
) 文件夹.您的扩展文件可以位于项目的任何文件夹中.在我们的案例中,让我们进行以下操作:
在
source
中创建一个_ext
文件夹在
_ext
文件夹中创建一个新的 Python 文件,命名为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.builder.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.nodes
中定义的标准 docutils 类. todo
继承自 Admonition
,因为它应该像注释或警告一样处理,而 todolist
只是一个”通用”节点.
注意
重要的是要知道,虽然您可以在不离开您的 conf.py
的情况下扩展 Sphinx,但如果您在此声明一个继承节点,您将遇到一个不明显的 PickleError
.因此,如果发生错误,请确保将继承节点放入单独的 Python 模块中.
有关更多详细信息,请参阅:
指令类
一个指令类是通常从 docutils.parsers.rst.Directive
派生的类.指令接口在 docutils 文档 中也有详细的介绍;重要的是,该类应该具有配置允许的标记的属性,以及一个返回节点列表的 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
类.这使我们能够使用 self.env
属性访问 构建环境实例 .如果没有这个,我们将不得不使用相当复杂的 self.state.document.settings.env
.然后,为了作为链接目标(来自 TodolistDirective
), TodoDirective
指令需要返回一个目标节点,除了 todo
节点之外.目标 ID(在HTML中,这将是锚点名称)是通过使用 env.new_serialno
生成的,每次调用都会返回一个新的唯一整数,因此导致唯一的目标名称.目标节点是在没有任何文本的情况下实例化的(前两个参数).
在创建警告节点时,指令的内容主体使用 self.state.nested_parse
进行解析.第一个参数是内容主体,第二个参数是内容偏移量.第三个参数是解析结果的父节点,在我们的案例中是 todo
节点.接下来, todo
节点被添加到环境中.这是为了能够在文档的所有 todolist
指令放置位置创建所有待办事项条目的列表.对于这种情况,环境属性 todo_all_todos
被使用(同样,名称应当唯一,因此它以扩展名作为前缀).当创建新的环境时,它并不存在,因此指令必须检查并在必要时创建它.有关待办事项条目位置的各种信息与节点的副本一起存储.
在最后一行,应该放入文档树中的节点被返回:目标节点和警告节点.
该指令返回的节点结构如下:
+--------------------+
| target node |
+--------------------+
+--------------------+
| todo node |
+--------------------+
\__+--------------------+
| admonition title |
+--------------------+
| paragraph |
+--------------------+
| ... |
+--------------------+
事件处理程序
事件处理器是 Sphinx 最强大的功能之一,它提供了一种方法来钩住文档处理的任何部分.Sphinx 本身提供了许多事件,详细信息请参见 API 指南 ,我们将在此处使用其中的一个子集.
让我们来看一下上面示例中使用的事件处理程序.首先是 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
由于我们将源文件中的信息存储在持久的环境中,因此当源文件发生更改时,这些信息可能会过时.因此,在读取每个源文件之前,环境中记录的相关信息会被清除,:event:env-purge-doc 事件为扩展提供了同样的机会.在这里,我们从 todo_all_todos
列表中清除所有与给定文档名称匹配的待办事项.如果文档中还有待办事项,解析过程中将再次添加它们.
下一个处理器用于 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.builder.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'],
在 第 3 阶段 (解析) 结束时,会触发 doctree-resolved
事件,允许进行自定义解析.我们为此事件编写的处理程序稍显复杂.如果 todo_include_todos
配置值(我们将很快描述)为 false,则所有 todo
和 todolist
节点将从文档中删除.如果不是, todo
节点将保持原样. todolist
节点将被替换为待办事项条目的列表,完整地链接到它们来源的位置.这些列表项由 todo
条目的节点和动态创建的 docutils 节点组成:每个条目一个段落,其中包含提供位置的文本,以及一个带有反向引用的链接(包含斜体节点的引用节点).引用 URI 是通过 sphinx.builders.Builder.get_relative_uri()
构建的,该方法根据所使用的构建器创建适当的 URI,并将待办事项节点(目标)的 ID 作为锚名称附加.
The setup
function
如前所述 previously , setup
函数是一个必需项,用于将指令插件接入 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 valuetodo_include_todos
,其默认值应为False
(这也告诉 Sphinx 这是一种布尔值).如果第三个参数是
'html'
,那么当配置值改变时,HTML 文档将完全重建.这是针对影响读取的配置值所需的 (构建 阶段 1 (读取) ).add_node()
将一个新的 节点类 添加到构建系统中.它还可以为每个支持的输出格式指定访问者函数.当新节点一直存在到 第 4 阶段 (写入) 时,这些访问者函数是必要的.由于todolist
节点在 第 3 阶段 (解析) 中总是被替换,因此它不需要任何访问者函数.add_directive()
根据名称和类添加一个新的 指令.最后,:meth:~Sphinx.connect 将一个 事件处理器 添加到由第一个参数给出的事件.事件处理器函数会接收多个参数,这些参数在事件中有文档说明.
通过这些,我们的扩展就完成了.
使用扩展¶
如前所述,我们需要通过在 conf.py
文件中声明来启用扩展.这需要两个步骤:
将
_ext
目录添加到 Python path ,使用sys.path.append
.这应该放在文件的顶部.更新或创建
extensions
列表,并将扩展文件名添加到列表中
此外,我们可能希望设置 todo_include_todos
配置值.正如上面提到的,这默认设置为 False
,但我们可以明确地设置它.
例如:
import os
import sys
sys.path.append(os.path.abspath("./_ext"))
extensions = ['todo']
todo_include_todos = False
您现在可以在整个项目中使用该扩展.例如:
Hello, world
============
.. toctree::
somefile.rst
someotherfile.rst
Hello world. Below is the list of TODOs.
.. todolist::
foo
===
Some intro text here...
.. todo:: Fix this
bar
===
Some more text here...
.. todo:: Fix that
因为我们将 todo_include_todos
配置为 False
,所以我们实际上不会看到 todo
和 todolist
指令的任何渲染结果.然而,如果我们将其切换为 true,我们将会看到之前描述的输出.
进一步阅读¶
有关更多信息,参阅 docutils 文档和 Sphinx API .
如果您希望在多个项目之间或与其他人分享您的扩展,请查看 第三方扩展 部分.