开发 “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_signatureadd_target_and_index 方法.这是因为 ObjectDescription 是一个特殊用途的指令, 用于描述类, 函数或者在我们的例子中的配方.更具体地说, handle_signature 实现解析指令的签名, 并将对象的名称和类型传递给它的超类, 而 add_taget_and_index 添加一个目标(链接到)和该节点的索引条目.

我们还看到这个指令定义了 has_content, required_argumentsoption_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):

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

继续, 我们可以看到我们已经定义了 initial_data. initial_data 中定义的值将被复制到 env.domaindata [domain_name] 作为域的初始数据, 域实例可以通过 self.data 访问它.我们看到我们在 initial_data 中定义了两个项:recipesrecipe2ingredient.它们包含定义的所有对象(即所有配方)的列表以及将规范成分名称映射到对象列表的哈希.我们命名对象的方式在我们的扩展中很常见, 并且在 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.这是因为我们已经将指令, 角色和索引注册为指令本身的一部分.

使用

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

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
tomato-soup.rst
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 文档和 开发扩展.