开发“配方”扩展¶
本教程的目标是说明角色、指令和域。一旦完成,我们将能够使用这个扩展来描述一个配方,并从文档的其他地方引用该配方。
备注
本教程基于一个首次发布于 opensource.com 并得到原作者的许可。
概述¶
我们希望扩展名为sphinx添加以下内容:
A
recipe
directive ,包含一些描述配方步骤的内容,以及:contains:
选择突出配方的主要成分。A
ref
role ,它提供了对配方本身的交叉引用。A
recipe
domain 这使得我们可以将上面的角色和域以及索引之类的东西联系在一起。
为此,我们需要将以下元素添加到sphinx中:
一个新的指令
recipe
新的指标,让我们可以参考成分和食谱
一个名为
recipe
,其中将包含recipe
指令和ref
角色
先决条件¶
我们需要和 the previous extensions . 这次,我们将在一个名为 recipe.py
.
下面是您可能获得的文件夹结构示例:
└── source
├── _ext
│ └── recipe.py
├── conf.py
└── index.rst
正在写入扩展名¶
正常开放 recipe.py
并在其中粘贴以下代码,我们稍后将详细解释所有这些代码:
1from collections import defaultdict
2
3from docutils.parsers.rst import directives
4
5from sphinx import addnodes
6from sphinx.application import Sphinx
7from sphinx.directives import ObjectDescription
8from sphinx.domains import Domain, Index
9from sphinx.roles import XRefRole
10from sphinx.util.nodes import make_refnode
11from sphinx.util.typing import ExtensionMetadata
12
13
14class RecipeDirective(ObjectDescription):
15 """A custom directive that describes a recipe."""
16
17 has_content = True
18 required_arguments = 1
19 option_spec = {
20 'contains': directives.unchanged_required,
21 }
22
23 def handle_signature(self, sig, signode):
24 signode += addnodes.desc_name(text=sig)
25 return sig
26
27 def add_target_and_index(self, name_cls, sig, signode):
28 signode['ids'].append('recipe' + '-' + sig)
29 if 'contains' in self.options:
30 ingredients = [x.strip() for x in self.options.get('contains').split(',')]
31
32 recipes = self.env.get_domain('recipe')
33 recipes.add_recipe(sig, ingredients)
34
35
36class IngredientIndex(Index):
37 """A custom index that creates an ingredient matrix."""
38
39 name = 'ingredient'
40 localname = 'Ingredient Index'
41 shortname = 'Ingredient'
42
43 def generate(self, docnames=None):
44 content = defaultdict(list)
45
46 recipes = {
47 name: (dispname, typ, docname, anchor)
48 for name, dispname, typ, docname, anchor, _ in self.domain.get_objects()
49 }
50 recipe_ingredients = self.domain.data['recipe_ingredients']
51 ingredient_recipes = defaultdict(list)
52
53 # flip from recipe_ingredients to ingredient_recipes
54 for recipe_name, ingredients in recipe_ingredients.items():
55 for ingredient in ingredients:
56 ingredient_recipes[ingredient].append(recipe_name)
57
58 # convert the mapping of ingredient to recipes to produce the expected
59 # output, shown below, using the ingredient name as a key to group
60 #
61 # name, subtype, docname, anchor, extra, qualifier, description
62 for ingredient, recipe_names in ingredient_recipes.items():
63 for recipe_name in recipe_names:
64 dispname, typ, docname, anchor = recipes[recipe_name]
65 content[ingredient].append((dispname, 0, docname, anchor, docname, '', typ))
66
67 # convert the dict to the sorted list of tuples expected
68 content = sorted(content.items())
69
70 return content, True
71
72
73class RecipeIndex(Index):
74 """A custom index that creates an recipe matrix."""
75
76 name = 'recipe'
77 localname = 'Recipe Index'
78 shortname = 'Recipe'
79
80 def generate(self, docnames=None):
81 content = defaultdict(list)
82
83 # sort the list of recipes in alphabetical order
84 recipes = self.domain.get_objects()
85 recipes = sorted(recipes, key=lambda recipe: recipe[0])
86
87 # generate the expected output, shown below, from the above using the
88 # first letter of the recipe as a key to group thing
89 #
90 # name, subtype, docname, anchor, extra, qualifier, description
91 for _name, dispname, typ, docname, anchor, _priority in recipes:
92 content[dispname[0].lower()].append((
93 dispname,
94 0,
95 docname,
96 anchor,
97 docname,
98 '',
99 typ,
100 ))
101
102 # convert the dict to the sorted list of tuples expected
103 content = sorted(content.items())
104
105 return content, True
106
107
108class RecipeDomain(Domain):
109 name = 'recipe'
110 label = 'Recipe Sample'
111 roles = {
112 'ref': XRefRole(),
113 }
114 directives = {
115 'recipe': RecipeDirective,
116 }
117 indices = {
118 RecipeIndex,
119 IngredientIndex,
120 }
121 initial_data = {
122 'recipes': [], # object list
123 'recipe_ingredients': {}, # name -> object
124 }
125
126 def get_full_qualified_name(self, node):
127 return f'recipe.{node.arguments[0]}'
128
129 def get_objects(self):
130 yield from self.data['recipes']
131
132 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
133 match = [
134 (docname, anchor)
135 for name, sig, typ, docname, anchor, prio in self.get_objects()
136 if sig == target
137 ]
138
139 if len(match) > 0:
140 todocname = match[0][0]
141 targ = match[0][1]
142
143 return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
144 else:
145 print('Awww, found nothing')
146 return None
147
148 def add_recipe(self, signature, ingredients):
149 """Add a new recipe to the domain."""
150 name = f'recipe.{signature}'
151 anchor = f'recipe-{signature}'
152
153 self.data['recipe_ingredients'][name] = ingredients
154 # name, dispname, type, docname, anchor, priority
155 self.data['recipes'].append((name, signature, 'Recipe', self.env.docname, anchor, 0))
156
157
158def setup(app: Sphinx) -> ExtensionMetadata:
159 app.add_domain(RecipeDomain)
160
161 return {
162 'version': '0.1',
163 'parallel_read_safe': True,
164 'parallel_write_safe': True,
165 }
让我们一步一步地看看这个扩展的每一部分,来解释发生了什么。
指令类
首先要检查的是 RecipeDirective
指令:
1class RecipeDirective(ObjectDescription):
2 """A custom directive that describes a recipe."""
3
4 has_content = True
5 required_arguments = 1
6 option_spec = {
7 'contains': directives.unchanged_required,
8 }
9
10 def handle_signature(self, sig, signode):
11 signode += addnodes.desc_name(text=sig)
12 return sig
13
14 def add_target_and_index(self, name_cls, sig, signode):
15 signode['ids'].append('recipe' + '-' + sig)
16 if 'contains' in self.options:
17 ingredients = [x.strip() for x in self.options.get('contains').split(',')]
18
19 recipes = self.env.get_domain('recipe')
20 recipes.add_recipe(sig, ingredients)
不像 开发“Hello World”扩展 和 开发“TODO”扩展 ,此指令不是从 docutils.parsers.rst.Directive
并且没有定义 run
方法。相反,它派生自 sphinx.directives.ObjectDescription
并定义了 handle_signature
和 add_target_and_index
方法:研究方法。这是因为 ObjectDescription
是一个专用指令,用于描述类、函数或在我们的例子中是食谱之类的东西。更具体地说, handle_signature
实现对指令签名的解析,并将对象的名称和类型传递给其超类,而 add_target_and_index
将目标(要链接到的)和条目添加到此节点的索引。
我们还看到这个指令定义了 has_content
, required_arguments
和 option_spec
. 不像 TodoDirective
指令添加到 previous tutorial ,此指令接受单个参数、配方名称和选项, contains
除了正文中嵌套的RestructuredText之外。
索引类
待处理
添加索引的简要概述
1class IngredientIndex(Index):
2 """A custom index that creates an ingredient matrix."""
3
4 name = 'ingredient'
5 localname = 'Ingredient Index'
6 shortname = 'Ingredient'
7
8 def generate(self, docnames=None):
9 content = defaultdict(list)
10
11 recipes = {
12 name: (dispname, typ, docname, anchor)
13 for name, dispname, typ, docname, anchor, _ in self.domain.get_objects()
14 }
15 recipe_ingredients = self.domain.data['recipe_ingredients']
16 ingredient_recipes = defaultdict(list)
17
18 # flip from recipe_ingredients to ingredient_recipes
19 for recipe_name, ingredients in recipe_ingredients.items():
20 for ingredient in ingredients:
21 ingredient_recipes[ingredient].append(recipe_name)
22
23 # convert the mapping of ingredient to recipes to produce the expected
24 # output, shown below, using the ingredient name as a key to group
25 #
26 # name, subtype, docname, anchor, extra, qualifier, description
27 for ingredient, recipe_names in ingredient_recipes.items():
28 for recipe_name in recipe_names:
29 dispname, typ, docname, anchor = recipes[recipe_name]
30 content[ingredient].append((dispname, 0, docname, anchor, docname, '', typ))
31
32 # convert the dict to the sorted list of tuples expected
33 content = sorted(content.items())
34
35 return content, True
1class RecipeIndex(Index):
2 """A custom index that creates an recipe matrix."""
3
4 name = 'recipe'
5 localname = 'Recipe Index'
6 shortname = 'Recipe'
7
8 def generate(self, docnames=None):
9 content = defaultdict(list)
10
11 # sort the list of recipes in alphabetical order
12 recipes = self.domain.get_objects()
13 recipes = sorted(recipes, key=lambda recipe: recipe[0])
14
15 # generate the expected output, shown below, from the above using the
16 # first letter of the recipe as a key to group thing
17 #
18 # name, subtype, docname, anchor, extra, qualifier, description
19 for _name, dispname, typ, docname, anchor, _priority in recipes:
20 content[dispname[0].lower()].append((
21 dispname,
22 0,
23 docname,
24 anchor,
25 docname,
26 '',
27 typ,
28 ))
29
30 # convert the dict to the sorted list of tuples expected
31 content = sorted(content.items())
32
33 return content, True
两个 IngredientIndex
和 RecipeIndex
源于 Index
. 它们实现自定义逻辑以生成定义索引的值的元组。注意 RecipeIndex
是一个只有一个条目的简单索引。扩展它以覆盖更多的对象类型还不是代码的一部分。
这两个指数都使用该方法 Index.generate()
做他们的工作。这个方法将来自我们域的信息组合起来,对其进行排序,并以一个列表结构返回它,这个列表结构将被sphinx接受。这看起来可能很复杂,但实际上只是一个元组列表 ('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup',...)
. 参考 domain API guide 有关此API的详细信息。
这些索引页可以使用 ref
通过将域名和索引相结合来实现 name
价值。例如, RecipeIndex
可以用来引用 :ref:`recipe-recipe
和 ``IngredientIndex` 可以用来引用 :ref:`recipe-ingredient
`。
领域
Sphinx域是一个专门的容器,它将角色、指令和索引等联系在一起。让我们看看我们在这里创建的域。
1class RecipeDomain(Domain):
2 name = 'recipe'
3 label = 'Recipe Sample'
4 roles = {
5 'ref': XRefRole(),
6 }
7 directives = {
8 'recipe': RecipeDirective,
9 }
10 indices = {
11 RecipeIndex,
12 IngredientIndex,
13 }
14 initial_data = {
15 'recipes': [], # object list
16 'recipe_ingredients': {}, # name -> object
17 }
18
19 def get_full_qualified_name(self, node):
20 return f'recipe.{node.arguments[0]}'
21
22 def get_objects(self):
23 yield from self.data['recipes']
24
25 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
26 match = [
27 (docname, anchor)
28 for name, sig, typ, docname, anchor, prio in self.get_objects()
29 if sig == target
30 ]
31
32 if len(match) > 0:
33 todocname = match[0][0]
34 targ = match[0][1]
35
36 return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
37 else:
38 print('Awww, found nothing')
39 return None
40
41 def add_recipe(self, signature, ingredients):
42 """Add a new recipe to the domain."""
43 name = f'recipe.{signature}'
44 anchor = f'recipe-{signature}'
45
46 self.data['recipe_ingredients'][name] = ingredients
47 # name, dispname, type, docname, anchor, priority
48 self.data['recipes'].append((name, signature, 'Recipe', self.env.docname, anchor, 0))
有一些有趣的事情要注意 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
和 recipe_ingredients
。每个都包含一个已定义的所有对象的列表(即所有食谱)和一个将规范配料名称映射到对象列表的散列。我们命名对象的方式在我们的扩展模块中是通用的,并且在 get_full_qualified_name
方法。对于创建的每个对象,规范名称为 recipe.<recipename>
,在哪里 <recipename>
是文档编写者为对象指定的名称(配方)。这使扩展能够使用共享相同名称的不同对象类型。拥有一个规范的名称和我们的对象的中心位置是一个巨大的优势。我们的索引和交叉引用代码都使用此功能。
这个 setup
功能
As always , the setup
功能是一种需求,用于将我们的延伸部分的各个部分连接到Sphinx上。让我们看看 setup
此扩展的函数。
1def setup(app: Sphinx) -> ExtensionMetadata:
2 app.add_domain(RecipeDomain)
3
4 return {
5 'version': '0.1',
6 'parallel_read_safe': True,
7 'parallel_write_safe': True,
8 }
这看起来和我们习惯看到的有点不同。没有电话打到 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:
指令)。
进一步阅读¶
有关详细信息,请参阅 docutils 文件和 Sphinx扩展API .