“レシピ”拡張を開発

このチュートリアルの目標は、役割、コマンド、ドメインを説明することです。完成後,この拡張を用いてレシピを記述し,文書中の他の場所からレシピを引用することができる.

注釈

このチュートリアルは初めての発表に基づいて opensource.com 原作者の許可を得た場合にはここで提供する.

概要

拡張してSphinxに以下を追加したい:

  • A recipe directive レシピを記述する手順が含まれています :contains: レシピの主成分を強調する選択肢。

  • A ref role これは,レシピ自体への交差引用を提供する.

  • A recipe domain それは、私たちが上の役割とドメインとインデックスのようなものを一緒に束ねることを可能にする。

そのためには、Sphinxに以下の要素を追加する必要があります。

  • 1つの名前は recipe

  • 新しいインデックスはトッピングやレシピを参考にすることができます

  • 一つの名は recipe そしてそれは recipe 指令と ref 役柄

前提条件.

中と同じ設定が必要です the previous extensions それがそうです。今回私たちは 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
from collections import defaultdict

from docutils.parsers.rst import directives

from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, 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
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)

似ていない “Hello world”拡張の開発 そして “TODO”拡張の開発 この指示は派生したものではありません docutils.parsers.rst.Directive 定義されていません run 方法です。逆に派生したのは sphinx.directives.ObjectDescription そして定義しました handle_signature そして add_target_and_index 方法です。それは ObjectDescription クラス、関数、またはレシピ(我々の例では)などを記述するための特別な用途の命令である。より具体的には handle_signature 命令署名の解析を実現し,オブジェクトの名前とタイプをクラスに渡す. add_taget_and_index ターゲット(リンク)とエントリをそのノードのインデックスに追加する.

We also see that this directive defines has_content, required_arguments and option_spec. Unlike the TodoDirective directive added in the previous tutorial, this directive takes a single argument, the recipe name, and an option, contains, in addition to the nested reStructuredText in the body.

索引類.

課題

索引付けの簡単な概要

 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
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
 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
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

どちらもある IngredientIndex そして RecipeIndex 次のような由来から派生しています Index それがそうです。これらは、インデックスを定義する値タプルを生成するためにカスタム論理を実装する。ご注意ください RecipeIndex 1つの項目しかない簡単なインデックスです。これは、より多くのオブジェクトタイプがコードの一部ではないことをカバーするように拡張される。

この2つの指数はどちらもこの方法を使っています Index.generate() 彼らの仕事をしていますこの方法では,我々のドメインからの情報を組み合わせてソートし,受信したリスト構造をSphinxで返す.これは複雑に見えるかもしれませんが実際は次のようにタプルのリストにすぎません ('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup',...) それがそうです。参考にする domain API guide このインタフェースのより多くの情報を表示します。

これらのインデックスページはドメイン名とその組み合わせで参照することができる name Vbl.使用 ref 役。例えば RecipeIndex 以下のように引用できる :ref:`recipe-recipe `.

定義域.

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
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))

There are some interesting things to note about this recipe domain and domains in general. Firstly, we actually register our directives, roles and indices here, via the directives, roles and indices attributes, rather than via calls later on in setup. We can also note that we aren't actually defining a custom role and are instead reusing the sphinx.roles.XRefRole role and defining the sphinx.domains.Domain.resolve_xref method. This method takes two arguments, typ and target, which refer to the cross-reference type and its target name. We'll use target to resolve our destination from our domain's recipes because we currently have only one type of node.

次に私たちが定義したのは initial_data それがそうです。定義された値 initial_data コピーすることになります env.domaindata[domain_name] ドメインの初期データとして,ドメインインスタンスは self.data それがそうです。ご覧のように私たちは2つのプロジェクトを定義しています initial_datarecipes そして recipe2ingredient それがそうです。これらは、定義されたすべてのオブジェクトのリスト(すなわち、すべてのレシピ)と、仕様成分名をオブジェクトリストにマッピングするハッシュとを含む。我々が対象を命名する方式は,我々の拡張モジュールでは汎用的であり, get_full_qualified_name 方法です。作成された各オブジェクトについて、仕様名は recipe.<recipename> どこですか <recipename> 文書作成者が対象に指定した名前(レシピ)である.これにより、拡張は、同じ名前を共有する異なるオブジェクトタイプを使用することができる。私たちの対象は規範的な名前と中心的な位置を持っていることが大きな利点だ。我々のインデックスと交差引用コードはこの機能を用いている.

♪the setup 機能

As always vt.的 setup 関数は必要であり,拡張された各部分をSphinxにフックするために用いられる.ちょっと見てみましょう setup 関数はこの拡張に用いる.

1
2
3
4
5
6
7
8
def setup(app):
    app.add_domain(RecipeDomain)

    return {
        'version': '0.1',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }

これは私たちが慣れて見ているのとは少し違うように見える。電話がかかっていない add_directive() 甚だしくは add_role() それがそうです。逆に私たちは1つを呼び出すだけで 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
トマトスープ.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 文書と Sphinxの開発拡張 それがそうです。