开发“配方”扩展

本教程的目标是说明角色、指令和域。一旦完成,我们将能够使用这个扩展来描述一个配方,并从文档的其他地方引用该配方。

备注

本教程基于一个首次发布于 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_signatureadd_target_and_index 方法:研究方法。这是因为 ObjectDescription 是一个专用指令,用于描述类、函数或在我们的例子中是食谱之类的东西。更具体地说, handle_signature 实现对指令签名的解析,并将对象的名称和类型传递给其超类,而 add_target_and_index 将目标(要链接到的)和条目添加到此节点的索引。

我们还看到这个指令定义了 has_contentrequired_argumentsoption_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

两个 IngredientIndexRecipeIndex 源于 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 域和一般域。首先,我们通过 directivesrolesindices 属性,而不是稍后通过调用 setup . 我们还可以注意到,我们实际上没有定义自定义角色,而是重用 sphinx.roles.XRefRole 角色和定义 sphinx.domains.Domain.resolve_xref 方法。这个方法有两个参数, typtarget ,它引用了交叉引用类型及其目标名称。我们将使用 target 从我们的域的 recipes 因为我们目前只有一种类型的节点。

接下来,我们可以看到我们已经定义了 initial_data 。中定义的值 initial_data 将复制到 env.domaindata[domain_name] 作为域的初始数据,域实例可以通过 self.data 。我们看到,我们已经在 initial_datarecipesrecipe_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 。这是因为我们已经将我们的指令、角色和索引注册为指令本身的一部分。

使用扩展名

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

index.rst
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 .