Hinzufügen einer Referenzdomäne¶
Das Ziel dieses Tutorials ist es, Rollen, Direktiven und Domänen zu veranschaulichen. Wenn wir fertig sind, können wir diese Erweiterung verwenden, um ein Rezept zu beschreiben und dieses Rezept von anderen Stellen in unserer Dokumentation zu referenzieren.
Hinweis
Dieses Tutorial basiert auf einer Anleitung, die erstmals unter opensource.com veröffentlicht wurde, und wird hier mit der Erlaubnis des ursprünglichen Autors zur Verfügung gestellt.
Übersicht¶
Wir möchten, dass die Erweiterung Sphinx Folgendes hinzufügt:
Eine
recipeDirektive, die einige Inhalte zur Beschreibung der Rezeptionsschritte enthält, zusammen mit einer:contains:Option, die die Hauptzutaten des Rezepts hervorhebt.Eine
refRolle, die eine Querverweisfunktion zum Rezept selbst bereitstellt.Eine
recipeDomäne, die es uns ermöglicht, die obige Rolle und Domäne zusammen mit Dingen wie Indizes zu verknüpfen.
Dazu müssen wir Sphinx die folgenden Elemente hinzufügen:
Eine neue Direktive namens
recipeNeue Indizes, die es uns ermöglichen, Zutaten und Rezepte zu referenzieren
Eine neue Domäne namens
recipe, die dierecipeDirektive und dierefRolle enthält
Voraussetzungen¶
Wir benötigen dieselbe Einrichtung wie in den vorherigen Erweiterungen. Dieses Mal werden wir unsere Erweiterung in einer Datei namens recipe.py ablegen.
Hier ist ein Beispiel für die Ordnerstruktur, die Sie erhalten könnten
└── source
├── _ext
│ └── recipe.py
├── conf.py
└── index.rst
Schreiben der Erweiterung¶
Öffnen Sie recipe.py und fügen Sie den folgenden Code ein, den wir alle kurz im Detail erklären werden.
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.current_document.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 }
Betrachten wir jeden Teil dieser Erweiterung Schritt für Schritt, um zu erklären, was vor sich geht.
Die Direktivenklasse
Das erste zu untersuchende Element ist die RecipeDirective Direktive.
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)
Im Gegensatz zu Erweiterung der Syntax mit Rollen und Direktiven und Erweiterung des Build-Prozesses leitet sich diese Direktive nicht von docutils.parsers.rst.Directive ab und definiert keine run Methode. Stattdessen leitet sie sich von sphinx.directives.ObjectDescription ab und definiert die Methoden handle_signature und add_target_and_index. Dies liegt daran, dass ObjectDescription eine Spezialdirektive ist, die für die Beschreibung von Dingen wie Klassen, Funktionen oder in unserem Fall Rezepten bestimmt ist. Genauer gesagt implementiert handle_signature das Parsen der Signatur der Direktive und übergibt den Objektnamen und -typ an seine Oberklasse, während add_target_and_index ein Ziel (zum Verlinken) und einen Eintrag im Index für diesen Knoten hinzufügt.
Wir sehen auch, dass diese Direktive has_content, required_arguments und option_spec definiert. Im Gegensatz zur TodoDirective Direktive, die im vorherigen Tutorial hinzugefügt wurde, nimmt diese Direktive zusätzlich zum verschachtelten reStructuredText im Rumpf ein einzelnes Argument, den Rezeptnamen, und eine Option, contains, entgegen.
Die Indexklassen
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
Sowohl IngredientIndex als auch RecipeIndex leiten sich von Index ab. Sie implementieren benutzerdefinierte Logik, um ein Tupel von Werten zu generieren, die den Index definieren. Beachten Sie, dass RecipeIndex ein einfacher Index ist, der nur einen Eintrag hat. Eine Erweiterung zur Abdeckung weiterer Objekttypen ist noch nicht Teil des Codes.
Beide Indizes verwenden die Methode Index.generate(), um ihre Arbeit zu erledigen. Diese Methode kombiniert die Informationen aus unserer Domäne, sortiert sie und gibt sie in einer Listenstruktur zurück, die von Sphinx akzeptiert wird. Dies mag kompliziert erscheinen, aber im Grunde ist es nur eine Liste von Tupeln wie ('tomato', 'TomatoSoup', 'test', 'rec-TomatoSoup',...). Weitere Informationen zu dieser API finden Sie im Domänen-API-Leitfaden.
Diese Indexseiten können mit der ref Rolle referenziert werden, indem der Domänenname und der name Wert des Index kombiniert werden. Zum Beispiel kann RecipeIndex mit :ref:`recipe-recipe` und IngredientIndex mit :ref:`recipe-ingredient` referenziert werden.
Die Domäne
Eine Sphinx-Domäne ist ein spezialisierter Container, der Rollen, Direktiven und Indizes unter anderem miteinander verknüpft. Betrachten wir die Domäne, die wir hier erstellen.
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.current_document.docname,
54 anchor,
55 0,
56 ))
Es gibt einige interessante Dinge, die man zu dieser recipe Domäne und Domänen im Allgemeinen bemerken kann. Erstens registrieren wir unsere Direktiven, Rollen und Indizes hier über die Attribute directives, roles und indices, anstatt über Aufrufe später in setup. Wir können auch feststellen, dass wir keine benutzerdefinierte Rolle definieren, sondern stattdessen die Rolle sphinx.roles.XRefRole wiederverwenden und die Methode sphinx.domains.Domain.resolve_xref definieren. Diese Methode nimmt zwei Argumente entgegen, typ und target, die sich auf den Querverweistyp und seinen Zielnamen beziehen. Wir werden target verwenden, um unser Ziel aus den recipes unserer Domäne aufzulösen, da wir derzeit nur eine Art von Knoten haben.
Weiter geht es mit initial_data. Die in initial_data definierten Werte werden in env.domaindata[domain_name] als Initialdaten der Domäne kopiert und können von Domäneninstanzen über self.data abgerufen werden. Wir sehen, dass wir zwei Elemente in initial_data definiert haben: recipes und recipe_ingredients. Jedes enthält eine Liste aller definierten Objekte (d.h. aller Rezepte) und einen Hash, der einen kanonischen Zutatenamen der Liste der Objekte zuordnet. Die Art und Weise, wie wir Objekte benennen, ist in unserer Erweiterung üblich und wird in der Methode get_full_qualified_name definiert. Für jedes erstellte Objekt ist der kanonische Name recipe.<recipename>, wobei <recipename> der Name ist, den der Dokumentationsautor dem Objekt gibt (ein Rezept). Dies ermöglicht es der Erweiterung, verschiedene Objekttypen zu verwenden, die denselben Namen haben. Einen kanonischen Namen und einen zentralen Ort für unsere Objekte zu haben, ist ein großer Vorteil. Sowohl unsere Indizes als auch unser Querverweiskode nutzen diese Funktion.
Die setup Funktion
Wie immer ist die Funktion setup eine Anforderung und dient dazu, die verschiedenen Teile unserer Erweiterung in Sphinx einzuhaken. Betrachten wir die Funktion setup für diese Erweiterung.
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 }
Das sieht etwas anders aus als das, was wir gewohnt sind. Es gibt keine Aufrufe von add_directive() oder gar add_role(). Stattdessen haben wir einen einzigen Aufruf von add_domain() gefolgt von einer Initialisierung der Standarddomäne. Dies liegt daran, dass wir unsere Direktiven, Rollen und Indizes bereits als Teil der Direktive selbst registriert hatten.
Verwenden der Erweiterung¶
Sie können die Erweiterung jetzt in Ihrem Projekt verwenden. Zum Beispiel:
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.
Die wichtigen Punkte, die man beachten sollte, sind die Verwendung der :recipe:ref: Rolle zur Querverweisung auf das Rezept, das tatsächlich anderswo definiert ist (unter Verwendung der :recipe:recipe: Direktive).
Weitere Lektüre¶
Weitere Informationen finden Sie in der docutils Dokumentation und in der Sphinx API.
Wenn Sie Ihre Erweiterung über mehrere Projekte oder mit anderen teilen möchten, lesen Sie den Abschnitt Drittanbieter-Erweiterungen.