diff mbox series

[RFC,v2,06/35] docs/qapi-domain: add QAPI domain object registry

Message ID 20241213011307.2942030-7-jsnow@redhat.com (mailing list archive)
State New
Headers show
Series Add qapi-domain Sphinx extension | expand

Commit Message

John Snow Dec. 13, 2024, 1:12 a.m. UTC
This is the first step towards QAPI domain cross-references and a QAPI
reference index.

For now, just create the object registry and amend the qapi:module
directive to use that registry. Update the merge_domaindata method now
that we have actual data we may need to merge.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapi-domain.py | 81 ++++++++++++++++++++++++++++++++++++--
 1 file changed, 78 insertions(+), 3 deletions(-)
diff mbox series

Patch

diff --git a/docs/sphinx/qapi-domain.py b/docs/sphinx/qapi-domain.py
index 57b35cf1f53..76f10b942d7 100644
--- a/docs/sphinx/qapi-domain.py
+++ b/docs/sphinx/qapi-domain.py
@@ -11,6 +11,7 @@ 
     Dict,
     Iterable,
     List,
+    NamedTuple,
     Tuple,
     cast,
 )
@@ -21,6 +22,7 @@ 
 from compat import nested_parse
 from sphinx import addnodes
 from sphinx.domains import Domain, ObjType
+from sphinx.locale import _, __
 from sphinx.util import logging
 from sphinx.util.docutils import SphinxDirective
 from sphinx.util.nodes import make_id
@@ -35,6 +37,13 @@ 
 logger = logging.getLogger(__name__)
 
 
+class ObjectEntry(NamedTuple):
+    docname: str
+    node_id: str
+    objtype: str
+    aliased: bool
+
+
 class QAPIModule(SphinxDirective):
     """
     Directive to mark description of a new module.
@@ -79,6 +88,7 @@  class QAPIModule(SphinxDirective):
     }
 
     def run(self) -> List[Node]:
+        domain = cast(QAPIDomain, self.env.get_domain("qapi"))
         modname = self.arguments[0].strip()
         no_index = "no-index" in self.options or "noindex" in self.options
 
@@ -91,11 +101,14 @@  def run(self) -> List[Node]:
         inode = addnodes.index(entries=[])
 
         if not no_index:
+            # note module to the domain
             node_id = make_id(self.env, self.state.document, "module", modname)
             target = nodes.target("", "", ids=[node_id], ismod=True)
             self.set_source_info(target)
             self.state.document.note_explicit_target(target)
 
+            domain.note_object(modname, "module", node_id, location=target)
+
             indextext = f"QAPI module; {modname}"
             inode = addnodes.index(
                 entries=[
@@ -126,7 +139,12 @@  class QAPIDomain(Domain):
     name = "qapi"
     label = "QAPI"
 
-    object_types: Dict[str, ObjType] = {}
+    # This table associates cross-reference object types (key) with an
+    # ObjType instance, which defines the valid cross-reference roles
+    # for each object type.
+    object_types: Dict[str, ObjType] = {
+        "module": ObjType(_("module"), "mod", "obj"),
+    }
 
     # Each of these provides a rST directive,
     # e.g. .. qapi:module:: block-core
@@ -135,13 +153,70 @@  class QAPIDomain(Domain):
     }
 
     roles = {}
-    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {}
+
+    # Moved into the data property at runtime;
+    # this is the internal index of reference-able objects.
+    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
+        "objects": {},  # fullname -> ObjectEntry
+    }
+
     indices = []
 
+    @property
+    def objects(self) -> Dict[str, ObjectEntry]:
+        ret = self.data.setdefault("objects", {})
+        return ret  # type: ignore[no-any-return]
+
+    def note_object(
+        self,
+        name: str,
+        objtype: str,
+        node_id: str,
+        aliased: bool = False,
+        location: Any = None,
+    ) -> None:
+        """Note a QAPI object for cross reference."""
+        if name in self.objects:
+            other = self.objects[name]
+            if other.aliased and aliased is False:
+                # The original definition found. Override it!
+                pass
+            elif other.aliased is False and aliased:
+                # The original definition is already registered.
+                return
+            else:
+                # duplicated
+                logger.warning(
+                    __(
+                        "duplicate object description of %s, "
+                        "other instance in %s, use :no-index: for one of them"
+                    ),
+                    name,
+                    other.docname,
+                    location=location,
+                )
+        self.objects[name] = ObjectEntry(
+            self.env.docname, node_id, objtype, aliased
+        )
+
+    def clear_doc(self, docname: str) -> None:
+        for fullname, obj in list(self.objects.items()):
+            if obj.docname == docname:
+                del self.objects[fullname]
+
     def merge_domaindata(
         self, docnames: List[str], otherdata: Dict[str, Any]
     ) -> None:
-        pass
+        for fullname, obj in otherdata["objects"].items():
+            if obj.docname in docnames:
+                # Sphinx's own python domain doesn't appear to bother to
+                # check for collisions. Assert they don't happen and
+                # we'll fix it if/when the case arises.
+                assert fullname not in self.objects, (
+                    "bug - collision on merge?"
+                    f" {fullname=} {obj=} {self.objects[fullname]=}"
+                )
+                self.objects[fullname] = obj
 
 
 def setup(app: Sphinx) -> Dict[str, Any]: