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 recipe Direktive, die einige Inhalte zur Beschreibung der Rezeptionsschritte enthält, zusammen mit einer :contains: Option, die die Hauptzutaten des Rezepts hervorhebt.

  • Eine ref Rolle, die eine Querverweisfunktion zum Rezept selbst bereitstellt.

  • Eine recipe Domä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 recipe

  • Neue Indizes, die es uns ermöglichen, Zutaten und Rezepte zu referenzieren

  • Eine neue Domäne namens recipe, die die recipe Direktive und die ref Rolle 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:

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.

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.