开发 “recipe” 扩展¶
本教程的目的是说明角色, 指令和域.完成后, 我们将能够使用此扩展来描述配方并从我们的文档中的其他地方引用该配方.
注解
本教程基于首次发布在 opensource.com 上的指南, 并在此处提供了原作者的许可.
概述¶
我们希望扩展程序将以下内容添加到Sphinx:
一个
recipe
directive, 包含一些描述配方步骤的内容, 以及一个contains:
选项, 突出显示配方的主要成分.一个
ref
role, 它提供了对配方本身的交叉引用.一个
recipe
domain, 它允许我们将上述角色和域以及索引之类的东西联系在一起.
为此, 我们需要将以下元素添加到Sphinx:
一个名为
recipe
的新指令允许我们参考配料和配方的新指数
一个名为
recipe
的新域, 它将包含recipe
指令和ref
角色
前提条件¶
我们需要与以下相同的设置 之前的扩展.这一次, 我们将扩展名为 recipe.py
的文件中.
以下是您可能获得的文件夹结构的示例:
└── source
├── _ext
│ └── recipe.py
├── conf.py
└── index.rst
编写扩展¶
打开 recipe.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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | from collections import defaultdict
from docutils.parsers.rst import directives
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain
from sphinx.domains import Index
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
class RecipeDirective(ObjectDescription):
"""A custom directive that describes a recipe."""
has_content = True
required_arguments = 1
option_spec = {
'contains': directives.unchanged_required,
}
def handle_signature(self, sig, signode):
signode += addnodes.desc_name(text=sig)
return sig
def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append('recipe' + '-' + sig)
if 'noindex' not in self.options:
ingredients = [
x.strip() for x in self.options.get('contains').split(',')]
recipes = self.env.get_domain('recipe')
recipes.add_recipe(sig, ingredients)
class IngredientIndex(Index):
"""A custom index that creates an ingredient matrix."""
name = 'ingredient'
localname = 'Ingredient Index'
shortname = 'Ingredient'
def generate(self, docnames=None):
content = defaultdict(list)
recipes = {name: (dispname, typ, docname, anchor)
for name, dispname, typ, docname, anchor, _
in self.domain.get_objects()}
recipe_ingredients = self.domain.data['recipe_ingredients']
ingredient_recipes = defaultdict(list)
# flip from recipe_ingredients to ingredient_recipes
for recipe_name, ingredients in recipe_ingredients.items():
for ingredient in ingredients:
ingredient_recipes[ingredient].append(recipe_name)
# convert the mapping of ingredient to recipes to produce the expected
# output, shown below, using the ingredient name as a key to group
#
# name, subtype, docname, anchor, extra, qualifier, description
for ingredient, recipe_names in ingredient_recipes.items():
for recipe_name in recipe_names:
dispname, typ, docname, anchor = recipes[recipe_name]
content[ingredient].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeIndex(Index):
"""A custom index that creates an recipe matrix."""
name = 'recipe'
localname = 'Recipe Index'
shortname = 'Recipe'
def generate(self, docnames=None):
content = defaultdict(list)
# sort the list of recipes in alphabetical order
recipes = self.domain.get_objects()
recipes = sorted(recipes, key=lambda recipe: recipe[0])
# generate the expected output, shown below, from the above using the
# first letter of the recipe as a key to group thing
#
# name, subtype, docname, anchor, extra, qualifier, description
for name, dispname, typ, docname, anchor, _ in recipes:
content[dispname[0].lower()].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeDomain(Domain):
name = 'recipe'
label = 'Recipe Sample'
roles = {
'ref': XRefRole()
}
directives = {
'recipe': RecipeDirective,
}
indices = {
RecipeIndex,
IngredientIndex
}
initial_data = {
'recipes': [], # object list
'recipe_ingredients': {}, # name -> object
}
def get_full_qualified_name(self, node):
return '{}.{}'.format('recipe', node.arguments[0])
def get_objects(self):
for obj in self.data['recipes']:
yield(obj)
def resolve_xref(self, env, fromdocname, builder, typ, target, node,
contnode):
match = [(docname, anchor)
for name, sig, typ, docname, anchor, prio
in self.get_objects() if sig == target]
if len(match) > 0:
todocname = match[0][0]
targ = match[0][1]
return make_refnode(builder, fromdocname, todocname, targ,
contnode, targ)
else:
print('Awww, found nothing')
return None
def add_recipe(self, signature, ingredients):
"""Add a new recipe to the domain."""
name = '{}.{}'.format('recipe', signature)
anchor = 'recipe-{}'.format(signature)
self.data['recipe_ingredients'][name] = ingredients
# name, dispname, type, docname, anchor, priority
self.data['recipes'].append(
(name, signature, 'Recipe', self.env.docname, anchor, 0))
def setup(app):
app.add_domain(RecipeDomain)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
|
让我们一步一步地看看这个扩展的每一部分, 以解释发生了什么.
指令类
首先要检查的是 RecipeDirective
指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | required_arguments = 1
option_spec = {
'contains': directives.unchanged_required,
}
def handle_signature(self, sig, signode):
signode += addnodes.desc_name(text=sig)
return sig
def add_target_and_index(self, name_cls, sig, signode):
signode['ids'].append('recipe' + '-' + sig)
if 'noindex' not in self.options:
ingredients = [
x.strip() for x in self.options.get('contains').split(',')]
recipes = self.env.get_domain('recipe')
recipes.add_recipe(sig, ingredients)
class IngredientIndex(Index):
"""A custom index that creates an ingredient matrix."""
|
与 开发一个 “Hello world” 扩展 和 开发 “TODO” 扩展 不同, 这个指令不是派生自 docutils.parsers.rst.Directive
, 并没有定义 run
方法.相反, 它派生自 sphinx.directives.ObjectDescription
并定义 handle_signature
和 add_target_and_index
方法.这是因为 ObjectDescription
是一个特殊用途的指令, 用于描述类, 函数或者在我们的例子中的配方.更具体地说, handle_signature
实现解析指令的签名, 并将对象的名称和类型传递给它的超类, 而 add_taget_and_index
添加一个目标(链接到)和该节点的索引条目.
我们还看到这个指令定义了 has_content
, required_arguments
和 option_spec
.与 previous tutorial 中添加的 TodoDirective
指令不同, 此指令除了嵌套的reStructuredText之外还采用单个参数, 配方名称和选项 contains
.身体.
索引类
待处理
添加指数的简要概述
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 | localname = 'Ingredient Index'
shortname = 'Ingredient'
def generate(self, docnames=None):
content = defaultdict(list)
recipes = {name: (dispname, typ, docname, anchor)
for name, dispname, typ, docname, anchor, _
in self.domain.get_objects()}
recipe_ingredients = self.domain.data['recipe_ingredients']
ingredient_recipes = defaultdict(list)
# flip from recipe_ingredients to ingredient_recipes
for recipe_name, ingredients in recipe_ingredients.items():
for ingredient in ingredients:
ingredient_recipes[ingredient].append(recipe_name)
# convert the mapping of ingredient to recipes to produce the expected
# output, shown below, using the ingredient name as a key to group
#
# name, subtype, docname, anchor, extra, qualifier, description
for ingredient, recipe_names in ingredient_recipes.items():
for recipe_name in recipe_names:
dispname, typ, docname, anchor = recipes[recipe_name]
content[ingredient].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeIndex(Index):
"""A custom index that creates an recipe matrix."""
name = 'recipe'
localname = 'Recipe Index'
shortname = 'Recipe'
def generate(self, docnames=None):
content = defaultdict(list)
# sort the list of recipes in alphabetical order
recipes = self.domain.get_objects()
recipes = sorted(recipes, key=lambda recipe: recipe[0])
# generate the expected output, shown below, from the above using the
# first letter of the recipe as a key to group thing
#
# name, subtype, docname, anchor, extra, qualifier, description
for name, dispname, typ, docname, anchor, _ in recipes:
content[dispname[0].lower()].append(
(dispname, 0, docname, anchor, docname, '', typ))
# convert the dict to the sorted list of tuples expected
content = sorted(content.items())
return content, True
class RecipeDomain(Domain):
|
IngredientIndex
和 RecipeIndex
都来自 Index
.它们实现自定义逻辑以生成定义索引的值元组.请注意, RecipeIndex
是一个只有一个条目的简单索引.扩展它以覆盖更多对象类型还不是代码的一部分.
两个索引都使用方法 Index.generate()
来完成他们的工作.此方法组合来自我们域的信息, 对其进行排序, 并将其返回到Sphinx将接受的列表结构中.这可能看起来很复杂, 但实际上它只是一个元组列表, 如 ('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup', ...)
.有关此API的更多信息, 请参阅 domain API guide.
The domain
Sphinx域是一个专门的容器, 它将角色, 指令和索引连接在一起.让我们来看看我们在这里创建的域名.
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 | roles = {
'ref': XRefRole()
}
directives = {
'recipe': RecipeDirective,
}
indices = {
RecipeIndex,
IngredientIndex
}
initial_data = {
'recipes': [], # object list
'recipe_ingredients': {}, # name -> object
}
def get_full_qualified_name(self, node):
return '{}.{}'.format('recipe', node.arguments[0])
def get_objects(self):
for obj in self.data['recipes']:
yield(obj)
def resolve_xref(self, env, fromdocname, builder, typ, target, node,
contnode):
match = [(docname, anchor)
for name, sig, typ, docname, anchor, prio
in self.get_objects() if sig == target]
if len(match) > 0:
todocname = match[0][0]
targ = match[0][1]
return make_refnode(builder, fromdocname, todocname, targ,
contnode, targ)
else:
print('Awww, found nothing')
return None
def add_recipe(self, signature, ingredients):
"""Add a new recipe to the domain."""
name = '{}.{}'.format('recipe', signature)
anchor = 'recipe-{}'.format(signature)
self.data['recipe_ingredients'][name] = ingredients
# name, dispname, type, docname, anchor, priority
self.data['recipes'].append(
(name, signature, 'Recipe', self.env.docname, anchor, 0))
def setup(app):
app.add_domain(RecipeDomain)
|
关于这个 recipe
域和域通常有一些有趣的事情需要注意.首先, 我们实际上通过 directives
, roles
和 indices
属性注册我们的指令, 角色和索引, 而不是稍后通过 setup
中的调用.我们还可以注意到, 我们实际上并没有定义自定义角色, 而是重用 sphinx.roles.XRefRole
角色并定义 sphinx.domains.Domain.resolve_xref
方法.这个方法有两个参数, typ
和 target
, 它们引用交叉引用类型及其目标名称.我们将使用 target
从我们域名的 recipes
中解析目的地, 因为我们目前只有一种类型的节点.
继续, 我们可以看到我们已经定义了 initial_data
. initial_data
中定义的值将被复制到 env.domaindata [domain_name]
作为域的初始数据, 域实例可以通过 self.data
访问它.我们看到我们在 initial_data
中定义了两个项:recipes
和 recipe2ingredient
.它们包含定义的所有对象(即所有配方)的列表以及将规范成分名称映射到对象列表的哈希.我们命名对象的方式在我们的扩展中很常见, 并且在 get_full_qualified_name
方法中定义.对于创建的每个对象, 规范名称是 recipe.<recipename>
, 其中 <recipename>
是文档编写者给对象的名称(配方).这使扩展可以使用共享相同名称的不同对象类型.拥有规范名称和对象的中心位置是一个巨大的优势.我们的索引和交叉引用代码都使用此功能.
setup
函数
总是, setup
函数是一个要求, 用于将扩展的各个部分挂钩到Sphinx.让我们来看看这个扩展的 setup
函数.
1 2 3 4 | 'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
|
这看起来与我们以前看到的有点不同.没有调用 add_directive()
甚至 add_role()
.相反, 我们只需要调用 add_domain()
然后进行一些初始化 standard domain.这是因为我们已经将指令, 角色和索引注册为指令本身的一部分.
使用¶
您现在可以在整个项目中使用扩展程序.例如:
Joe's Recipes
=============
Below are a collection of my favourite recipes. I highly recommend the
:recipe:ref:`TomatoSoup` recipe in particular!
.. toctree::
tomato-soup
The recipe contains `tomato` and `cilantro`.
.. recipe:recipe:: TomatoSoup
:contains: tomato cilantro salt pepper
This recipe is a tasty tomato soup, combine all ingredients
and cook.
需要注意的重要事项是使用 :recipe:ref:
角色交叉引用其他地方实际定义的配方(使用 :recipe:recipe:
指令.