This is the first step towards QAPI domain cross-references and a QAPI reference index.
This patch just creates the object registry and amends the qapi:module directive to use that registry. Update the merge_domaindata stub method now that we have actual data we may need to merge. This patch also defines that "module" entities can be referenced with :qapi:mod:`foo` or :qapi:any:`foo` references, although the implementation for those roles is handled in a forthcoming patch. Note that how to handle merge conflict resolution is unhandled, as the Sphinx python domain itself does not handle it either. I do not know how to intentionally trigger it, so I've left an assertion instead if it should ever come up ... Signed-off-by: John Snow <js...@redhat.com> --- docs/sphinx/qapi_domain.py | 81 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py index 8ce3caf933d..b17fcb93f24 100644 --- a/docs/sphinx/qapi_domain.py +++ b/docs/sphinx/qapi_domain.py @@ -12,6 +12,7 @@ Dict, Iterable, List, + NamedTuple, Tuple, cast, ) @@ -22,6 +23,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 @@ -36,6 +38,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. @@ -80,6 +89,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 @@ -92,11 +102,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=[ @@ -127,7 +140,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", "any"), + } # Each of these provides a rST directive, # e.g. .. qapi:module:: block-core @@ -136,13 +154,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: AbstractSet[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 resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any: # pylint: disable=unused-argument -- 2.48.1