参照ドメインの追加¶
このチュートリアルの目的は、ロール、ディレクティブ、ドメインを説明することです。完了すると、この拡張機能を使用してレシピを記述し、ドキュメントの他の場所からそのレシピを参照できるようになります。
注記
このチュートリアルは、opensource.comで最初に公開されたガイドに基づいており、元の著者の許可を得てここに提供されています。
概要¶
この拡張機能によって、Sphinx に以下の機能を追加したいと考えています。
recipe
ディレクティブ。レシピの手順を記述するコンテンツが含まれ、:contains:
オプションを使用してレシピの主要な材料を強調表示します。ref
ロール。レシピ自体への相互参照を提供します。recipe
ドメイン。上記のロールとドメインを、インデックスなどの要素と結び付けることができます。
そのため、Sphinx に以下の要素を追加する必要があります。
recipe
という新しいディレクティブ材料とレシピを参照するための新しいインデックス
recipe
という新しいドメイン。recipe
ディレクティブとref
ロールを含みます。
前提条件¶
以前の拡張機能と同じ設定が必要です。今回は、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((
66 dispname,
67 0,
68 docname,
69 anchor,
70 docname,
71 '',
72 typ,
73 ))
74
75 # convert the dict to the sorted list of tuples expected
76 content = sorted(content.items())
77
78 return content, True
79
80
81class RecipeIndex(Index):
82 """A custom index that creates an recipe matrix."""
83
84 name = 'recipe'
85 localname = 'Recipe Index'
86 shortname = 'Recipe'
87
88 def generate(self, docnames=None):
89 content = defaultdict(list)
90
91 # sort the list of recipes in alphabetical order
92 recipes = self.domain.get_objects()
93 recipes = sorted(recipes, key=lambda recipe: recipe[0])
94
95 # generate the expected output, shown below, from the above using the
96 # first letter of the recipe as a key to group thing
97 #
98 # name, subtype, docname, anchor, extra, qualifier, description
99 for _name, dispname, typ, docname, anchor, _priority in recipes:
100 content[dispname[0].lower()].append((
101 dispname,
102 0,
103 docname,
104 anchor,
105 docname,
106 '',
107 typ,
108 ))
109
110 # convert the dict to the sorted list of tuples expected
111 content = sorted(content.items())
112
113 return content, True
114
115
116class RecipeDomain(Domain):
117 name = 'recipe'
118 label = 'Recipe Sample'
119 roles = {
120 'ref': XRefRole(),
121 }
122 directives = {
123 'recipe': RecipeDirective,
124 }
125 indices = {
126 RecipeIndex,
127 IngredientIndex,
128 }
129 initial_data = {
130 'recipes': [], # object list
131 'recipe_ingredients': {}, # name -> object
132 }
133 data_version = 0
134
135 def get_full_qualified_name(self, node):
136 return f'recipe.{node.arguments[0]}'
137
138 def get_objects(self):
139 yield from self.data['recipes']
140
141 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
142 match = [
143 (docname, anchor)
144 for name, sig, typ, docname, anchor, prio in self.get_objects()
145 if sig == target
146 ]
147
148 if len(match) > 0:
149 todocname = match[0][0]
150 targ = match[0][1]
151
152 return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
153 else:
154 print('Awww, found nothing')
155 return None
156
157 def add_recipe(self, signature, ingredients):
158 """Add a new recipe to the domain."""
159 name = f'recipe.{signature}'
160 anchor = f'recipe-{signature}'
161
162 self.data['recipe_ingredients'][name] = ingredients
163 # name, dispname, type, docname, anchor, priority
164 self.data['recipes'].append((
165 name,
166 signature,
167 'Recipe',
168 self.env.docname,
169 anchor,
170 0,
171 ))
172
173
174def setup(app: Sphinx) -> ExtensionMetadata:
175 app.add_domain(RecipeDomain)
176
177 return {
178 'version': '0.1',
179 'parallel_read_safe': True,
180 'parallel_write_safe': True,
181 }
この拡張機能の各部分を段階的に見て、何が起こっているのかを説明しましょう。
ディレクティブクラス
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)
ロールとディレクティブによる構文の拡張とビルドプロセスの拡張とは異なり、このディレクティブは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
ディレクティブとは異なり、このディレクティブは、レシピ名という1つの引数と、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((
31 dispname,
32 0,
33 docname,
34 anchor,
35 docname,
36 '',
37 typ,
38 ))
39
40 # convert the dict to the sorted list of tuples expected
41 content = sorted(content.items())
42
43 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
は、エントリが1つだけの単純なインデックスであることに注意してください。より多くのオブジェクトタイプをカバーするように拡張するのは、まだコードの一部ではありません。
両方のインデックスは、作業を行うためにIndex.generate()
メソッドを使用しています。このメソッドは、ドメインからの情報を組み合わせ、ソートし、Sphinxで受け入れられるリスト構造で返します。これは複雑に見えるかもしれませんが、実際は('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup',...)
のようなタプルのリストです。ドメインAPIガイドで、このAPIの詳細を参照してください。
これらのインデックスページは、ドメイン名とインデックスのname
値を組み合わせることで、ref
ロールを使用して参照できます。たとえば、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 data_version = 0
19
20 def get_full_qualified_name(self, node):
21 return f'recipe.{node.arguments[0]}'
22
23 def get_objects(self):
24 yield from self.data['recipes']
25
26 def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
27 match = [
28 (docname, anchor)
29 for name, sig, typ, docname, anchor, prio in self.get_objects()
30 if sig == target
31 ]
32
33 if len(match) > 0:
34 todocname = match[0][0]
35 targ = match[0][1]
36
37 return make_refnode(builder, fromdocname, todocname, targ, contnode, targ)
38 else:
39 print('Awww, found nothing')
40 return None
41
42 def add_recipe(self, signature, ingredients):
43 """Add a new recipe to the domain."""
44 name = f'recipe.{signature}'
45 anchor = f'recipe-{signature}'
46
47 self.data['recipe_ingredients'][name] = ingredients
48 # name, dispname, type, docname, anchor, priority
49 self.data['recipes'].append((
50 name,
51 signature,
52 'Recipe',
53 self.env.docname,
54 anchor,
55 0,
56 ))
このrecipe
ドメインとドメイン全般について、注目すべき点がいくつかあります。まず、実際にはdirectives
、roles
、indices
属性を介して、setup
の後で呼び出すのではなく、ここでディレクティブ、ロール、インデックスを登録します。また、カスタムロールを実際には定義しておらず、代わりにsphinx.roles.XRefRole
ロールを再利用し、sphinx.domains.Domain.resolve_xref
メソッドを定義していることにも注目できます。このメソッドは、相互参照タイプとそのターゲット名を指すtyp
とtarget
の2つの引数を取ります。現在、ノードのタイプは1つだけなので、ドメインのrecipes
から宛先を解決するためにtarget
を使用します。
次に、initial_data
を定義していることがわかります。initial_data
で定義された値は、ドメインの初期データとしてenv.domaindata[domain_name]
にコピーされ、ドメインインスタンスはself.data
を介してアクセスできます。initial_data
にはrecipes
とrecipe_ingredients
の2つの項目を定義していることがわかります。それぞれ、定義されているすべてのオブジェクト(つまり、すべてのレシピ)のリストと、標準的な材料名とオブジェクトのリストをマッピングするハッシュが含まれています。オブジェクトの命名方法は、拡張機能全体で共通しており、get_full_qualified_name
メソッドで定義されています。作成された各オブジェクトについて、標準名はrecipe.<recipename>
です。ここで<recipename>
は、ドキュメント作成者がオブジェクト(レシピ)に付ける名前です。これにより、拡張機能は同じ名前を共有する異なるオブジェクトタイプを使用できるようになります。標準名とオブジェクトの中央リポジトリを持つことは、大きな利点です。インデックスと相互参照コードの両方がこの機能を使用しています。
setup
関数
常にそうであるように、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()
への単一の呼び出しと、標準ドメイン の初期化が行われています。これは、ディレクティブ自体の一部として、ディレクティブ、ロール、インデックスを既に登録していたためです。
拡張機能の使用¶
これで、プロジェクト全体で拡張機能を使用できるようになりました。例:
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 を参照してください。
拡張機能を複数のプロジェクト間または他の人と共有したい場合は、サードパーティの拡張機能 セクションを確認してください。