开发 “TODO” 扩展¶
本教程的目标是创建一个比在 开发一个 “Hello world” 扩展 中创建的更全面的扩展.虽然该指南仅涉及编写自定义 directive,但本指南添加了多个指令,以及自定义节点,其他配置值和自定义事件处理程序.为此,我们将介绍一个 todo
扩展,它增加了在文档中包含todo条目的功能,并在中心位置收集这些条目.这类似于与Sphinx一起发布的 sphinxext.todo
扩展.
概述¶
我们希望扩展程序将以下内容添加到Sphinx:
一个
todo
指令,包含一些用 “TODO” 标记的内容,如果设置了新的配置值,则只显示在输出中. Todo条目默认不应该在输出中.一个
todolist
指令,用于创建整个文档中所有todo条目的列表.
为此,我们需要将以下元素添加到Sphinx:
新的指令,称为
todo
和todolist
.用于表示这些指令的新文档树节点,通常也称为
todo
和todolist
.如果新指令仅产生现有节点可表示的某些内容,则我们不需要新节点.一个新的配置值
todo_include_todos
(配置值名称应以扩展名开头,以保持唯一),控制todo条目是否使其进入输出.新事件处理程序: 一个用于
doctree-resolved
事件,用于替换todo和todolist节点,另一个用于env-purge-doc
(其原因将在后面介绍).
先决条件¶
与 开发一个 “Hello world” 扩展 一样,我们不会通过PyPI分发这个插件,所以我们需要一个Sphinx项目来调用它.您可以使用 sphinx-quickstart 来使用现有项目或创建新项目.
我们假设您使用单独的源(source
)和build(build
)文件夹.您的扩展文件可以位于项目的任何文件夹中.在我们的例子中,让我们做以下事情:
在
source
中创建一个_ext
文件夹在
_ext
文件夹中创建一个名为todo.py
的新Python文件
以下是您可能获得的文件夹结构的示例:
└── source
├── _ext
│ └── todo.py
├── _static
├── conf.py
├── somefolder
├── index.rst
├── somefile.rst
└── someotherfile.rst
编写扩展¶
打开 todo.py
并在其中粘贴以下代码,所有这些我们将在稍后详细解释:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx.locale import _
from sphinx.util.docutils import SphinxDirective
class todo(nodes.Admonition, nodes.Element):
pass
class todolist(nodes.General, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
class TodolistDirective(Directive):
def run(self):
return [todolist('')]
class TodoDirective(SphinxDirective):
# this enables content in the directive
has_content = True
def run(self):
targetid = 'todo-%d' % self.env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(self.env, 'todo_all_todos'):
self.env.todo_all_todos = []
self.env.todo_all_todos.append({
'docname': self.env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
def purge_todos(app, env, docname):
if not hasattr(env, 'todo_all_todos'):
return
env.todo_all_todos = [todo for todo in env.todo_all_todos
if todo['docname'] != docname]
def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
# Replace all todolist nodes with a list of the collected todos.
# Augment each todo with a backlink to the original location.
env = app.builder.env
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
for todo_info in env.todo_all_todos:
para = nodes.paragraph()
filename = env.doc2path(todo_info['docname'], base=None)
description = (
_('(The original entry is located in %s, line %d and can be found ') %
(filename, todo_info['lineno']))
para += nodes.Text(description, description)
# Create a reference
newnode = nodes.reference('', '')
innernode = nodes.emphasis(_('here'), _('here'))
newnode['refdocname'] = todo_info['docname']
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, todo_info['docname'])
newnode['refuri'] += '#' + todo_info['target']['refid']
newnode.append(innernode)
para += newnode
para += nodes.Text('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
node.replace_self(content)
def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
app.add_node(todolist)
app.add_node(todo,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
|
这是比以下详细介绍的更广泛的扩展 开发一个 “Hello world” 扩展,但是,我们将逐步查看每个部分以解释正在发生的事情.
节点类
让我们从节点类开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class todo(nodes.Admonition, nodes.Element):
pass
class todolist(nodes.General, nodes.Element):
pass
def visit_todo_node(self, node):
self.visit_admonition(node)
def depart_todo_node(self, node):
self.depart_admonition(node)
|
除了继承自 docutils.nodes
中定义的标准docutils类之外,节点类通常不需要做任何事情. todo
继承自 Admonition
,因为它应该像笔记或警告一样处理, todolist
只是一个“普通”节点.
注解
许多扩展不必创建自己的节点类,并且可以使用 docutils 和 Sphinx.
指令类
指令类是通常从“派生”派生的类 docutils.parsers.rst.Directive
.指令接口也在 docutils documentation 中详细介绍;重要的是该类应该具有配置允许标记的属性,并且 run
方法,返回节点列表.
首先看一下 TodolistDirective
指令:
1 2 3 4 | class TodolistDirective(Directive):
def run(self):
return [todolist('')]
|
它非常简单,创建并返回我们的 todolist
节点类的实例. TodolistDirective
指令本身既没有内容也没有需要处理的参数.这就把我们带到了 TodoDirective
指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class TodoDirective(SphinxDirective):
# this enables content in the directive
has_content = True
def run(self):
targetid = 'todo-%d' % self.env.new_serialno('todo')
targetnode = nodes.target('', '', ids=[targetid])
todo_node = todo('\n'.join(self.content))
todo_node += nodes.title(_('Todo'), _('Todo'))
self.state.nested_parse(self.content, self.content_offset, todo_node)
if not hasattr(self.env, 'todo_all_todos'):
self.env.todo_all_todos = []
self.env.todo_all_todos.append({
'docname': self.env.docname,
'lineno': self.lineno,
'todo': todo_node.deepcopy(),
'target': targetnode,
})
return [targetnode, todo_node]
|
这里介绍了几个重要的事情.首先,正如您所看到的,我们现在继承子类 SphinxDirective
助手类而不是通常的 Directive
类.这使我们可以使用 self.env
属性访问 build environment instance.没有这个,我们必须使用相当复杂的 self.state.document.settings.env
.然后,作为一个链接目标(来自 TodolistDirective
), TodoDirective
指令除了 todo
节点外还需要返回一个目标节点.目标ID(在HTML中,这将是锚名称)是使用 env.new_serialno
生成的,它在每次调用时返回一个新的唯一整数,因此会导致唯一的目标名称.目标节点实例化而没有任何文本(前两个参数).
在创建admonition节点时,使用 self.state.nested_parse
解析指令的内容主体.第一个参数给出内容主体,第二个参数给出内容偏移量.第三个参数给出了解析结果的父节点,在我们的例子中是 todo
节点.在此之后, todo
节点被添加到环境中.这需要能够在整个文档中创建所有todo条目的列表,在作者放置 todolist
指令的地方.在这种情况下,使用环境属性 todo_all_todos
(同样,名称应该是唯一的,因此它以扩展名称为前缀).创建新环境时不存在,因此必要时必须检查并创建指令.关于todo条目位置的各种信息与节点的副本一起存储.
在最后一行中,将返回应放入doctree的节点:目标节点和admonition节点.
指令返回的节点结构如下所示:
+--------------------+
| target node |
+--------------------+
+--------------------+
| todo node |
+--------------------+
\__+--------------------+
| admonition title |
+--------------------+
| paragraph |
+--------------------+
| ... |
+--------------------+
事件处理程序
事件处理程序是Sphinx最强大的功能之一,提供了一种挂钩文档过程任何部分的方法. Sphinx本身提供了许多事件,详见 API guide,我们将在这里使用它们的子集.
让我们看看上面例子中使用的事件处理程序.首先,一个用于 env-purge-doc
事件:
1 2 3 4 5 6 | def purge_todos(app, env, docname):
if not hasattr(env, 'todo_all_todos'):
return
env.todo_all_todos = [todo for todo in env.todo_all_todos
if todo['docname'] != docname]
|
由于我们将源文件中的信息存储在持久的环境中,因此当源文件发生更改时,它可能会过时.因此,在读取每个源文件之前,清除环境的记录,并且 env-purge-doc
事件为扩展提供了执行相同操作的机会.在这里,我们清除所有todos,其docname与 todo_all_todos
列表中的给定匹配.如果文档中还有待办事项,则在解析期间将再次添加它们.
另一个处理程序属于 doctree-resolved
事件:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | def process_todo_nodes(app, doctree, fromdocname):
if not app.config.todo_include_todos:
for node in doctree.traverse(todo):
node.parent.remove(node)
# Replace all todolist nodes with a list of the collected todos.
# Augment each todo with a backlink to the original location.
env = app.builder.env
for node in doctree.traverse(todolist):
if not app.config.todo_include_todos:
node.replace_self([])
continue
content = []
for todo_info in env.todo_all_todos:
para = nodes.paragraph()
filename = env.doc2path(todo_info['docname'], base=None)
description = (
_('(The original entry is located in %s, line %d and can be found ') %
(filename, todo_info['lineno']))
para += nodes.Text(description, description)
# Create a reference
newnode = nodes.reference('', '')
innernode = nodes.emphasis(_('here'), _('here'))
newnode['refdocname'] = todo_info['docname']
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, todo_info['docname'])
newnode['refuri'] += '#' + todo_info['target']['refid']
newnode.append(innernode)
para += newnode
para += nodes.Text('.)', '.)')
# Insert into the todolist
content.append(todo_info['todo'])
content.append(para)
node.replace_self(content)
|
doctree-resolved
事件在以下结尾发出 阶段3(解析) 并允许自定义解析.我们为此事件编写的处理程序涉及更多.如果 todo_include_todos
配置值(我们将在稍后描述)为false,则从文档中删除所有 todo
和 todolist
节点.如果没有, todo
节点就会停留在哪里以及它们如何. todolist
节点被todo条目列表替换,并带有到它们来自的位置的反向链接.列表项由 todo
条目中的节点和动态创建的docutils节点组成:每个条目的段落,包含给出位置的文本,以及包含斜体节点的链接(包含斜体节点的参考节点)反向引用.引用URI由以下构建 sphinx.builders.Builder.get_relative_uri()
, 它根据使用的构建器创建合适的URI,并附加待办事项节点(目标)的ID作为锚名称.
setup
函数
如上所述 之前的, setup
函数是一个要求,用于将指令插入Sphinx.但是,我们也使用它来连接扩展的其他部分.让我们看看我们的 setup
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def setup(app):
app.add_config_value('todo_include_todos', False, 'html')
app.add_node(todolist)
app.add_node(todo,
html=(visit_todo_node, depart_todo_node),
latex=(visit_todo_node, depart_todo_node),
text=(visit_todo_node, depart_todo_node))
app.add_directive('todo', TodoDirective)
app.add_directive('todolist', TodolistDirective)
app.connect('doctree-resolved', process_todo_nodes)
app.connect('env-purge-doc', purge_todos)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
|
此函数中的调用是指我们之前添加的类和函数.个人电话的作用如下:
add_config_value()
让Sphinx知道它应该识别新的 config 值todo_include_todos
,其默认值应该是False
(这也告诉Sphinx它是一个布尔值) .如果第三个参数是
'html'
,如果配置值改变了它的值,HTML文档将完全重建.这对于影响读取的配置值是必需的(build:ref:phase 1(reading)<build-phases>).add_node()
为构建系统添加了一个新的 node class.它还可以为每种支持的输出格式指定访问者功能.当新节点停留时,需要这些访问者函数 阶段4(写入).由于todolist
节点总是被替换为 phase 3(resolving),它不需要任何节点.add_directive()
添加一个新的*指令*,由name和class给出.最后,:meth:~Sphinx.connect 将 事件处理程序 添加到名称由第一个参数给出的事件中.使用事件记录的几个参数调用事件处理函数.
有了这个,我们的扩展就完成了.
使用扩展¶
和以前一样,我们需要通过在我们的 conf.py
文件中声明它来启用扩展.这里有两个步骤:
使用
sys.path.append
将_ext
目录添加到 Python path 中.这应该放在文件的顶部.更新或创建
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,我们将看到前面描述的输出.