[PATCH 09/57] docs/qapi-domain: add QAPI xref roles

John Snow posted 57 patches 4 weeks ago
There is a newer version of this series
[PATCH 09/57] docs/qapi-domain: add QAPI xref roles
Posted by John Snow 4 weeks ago
Add domain-specific cross-reference syntax. As of this commit, that
means new :qapi:mod:`block-core` and :qapi:any:`block-core` referencing
syntax.

:mod: will only find modules, but :any: will find anything registered to
the QAPI domain. (In forthcoming commits, this means commands, events,
enums, etc.)

Creating the cross-references is powered by the QAPIXRefRole class;
resolving them is handled by QAPIDomain.resolve_xref().

QAPIXrefRole is copied almost verbatim from Sphinx's own
PyXrefRole. PyXrefRole (and QAPIXrefRole) adds two features over the
base class:

(1) Creating a cross-reference with e.g. :py:class:`~class.name`
instructs sphinx to omit the fully qualified parts of the resolved name
from the actual link text. This may be useful in the future if we add
namespaces to QAPI documentation, e.g. :qapi:cmd:`~qsd.blockdev-backup`
could link to the QSD-specific documentation for blockdev-backup while
omitting that prefix from the link text.

(2) Prefixing the link target with a "." changes the search behavior to
prefer locally-scoped items first.

I think both of these are worth keeping to help manage future namespace
issues between QEMU, QSD and QGA; but it's possible it's extraneous. It
may possibly be worth keeping just to keep feature parity with Sphinx's
other domains; e.g. "principle of least surprise". Dunno.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapi_domain.py | 88 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 87 insertions(+), 1 deletion(-)

diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 744956045e8..104bae709f3 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -31,6 +31,7 @@
     ObjType,
 )
 from sphinx.locale import _, __
+from sphinx.roles import XRefRole
 from sphinx.util import logging
 from sphinx.util.docutils import SphinxDirective
 from sphinx.util.nodes import make_id, make_refnode
@@ -54,6 +55,54 @@ class ObjectEntry(NamedTuple):
     aliased: bool
 
 
+class QAPIXRefRole(XRefRole):
+
+    def process_link(
+        self,
+        env: BuildEnvironment,
+        refnode: Element,
+        has_explicit_title: bool,
+        title: str,
+        target: str,
+    ) -> tuple[str, str]:
+        refnode["qapi:module"] = env.ref_context.get("qapi:module")
+
+        # Cross-references that begin with a tilde adjust the title to
+        # only show the reference without a leading module, even if one
+        # was provided. This is a Sphinx-standard syntax; give it
+        # priority over QAPI-specific type markup below.
+        hide_module = False
+        if target.startswith("~"):
+            hide_module = True
+            target = target[1:]
+
+        # Type names that end with "?" are considered optional
+        # arguments and should be documented as such, but it's not
+        # part of the xref itself.
+        if target.endswith("?"):
+            refnode["qapi:optional"] = True
+            target = target[:-1]
+
+        # Type names wrapped in brackets denote lists. strip the
+        # brackets and remember to add them back later.
+        if target.startswith("[") and target.endswith("]"):
+            refnode["qapi:array"] = True
+            target = target[1:-1]
+
+        if has_explicit_title:
+            # Don't mess with the title at all if it was explicitly set.
+            # Explicit title syntax for references is e.g.
+            # :qapi:type:`target <explicit title>`
+            # and this explicit title overrides everything else here.
+            return title, target
+
+        title = target
+        if hide_module:
+            title = target.split(".")[-1]
+
+        return title, target
+
+
 class QAPIModule(SphinxDirective):
     """
     Directive to mark description of a new module.
@@ -219,7 +268,13 @@ class QAPIDomain(Domain):
         "module": QAPIModule,
     }
 
-    roles = {}
+    # These are all cross-reference roles; e.g.
+    # :qapi:cmd:`query-block`. The keys correlate to the names used in
+    # the object_types table values above.
+    roles = {
+        "mod": QAPIXRefRole(),
+        "any": QAPIXRefRole(),  # reference *any* type of QAPI object.
+    }
 
     # Moved into the data property at runtime;
     # this is the internal index of reference-able objects.
@@ -358,6 +413,37 @@ def find_obj(
             matches = [m for m in matches if not m[1].aliased]
         return matches
 
+    def resolve_xref(
+        self,
+        env: BuildEnvironment,
+        fromdocname: str,
+        builder: Builder,
+        typ: str,
+        target: str,
+        node: pending_xref,
+        contnode: Element,
+    ) -> nodes.reference | None:
+        modname = node.get("qapi:module")
+        matches = self.find_obj(modname, target, typ)
+
+        if not matches:
+            return None
+
+        if len(matches) > 1:
+            logger.warning(
+                __("more than one target found for cross-reference %r: %s"),
+                target,
+                ", ".join(match[0] for match in matches),
+                type="ref",
+                subtype="qapi",
+                location=node,
+            )
+
+        name, obj = matches[0]
+        return make_refnode(
+            builder, fromdocname, obj.docname, obj.node_id, contnode, name
+        )
+
     def resolve_any_xref(
         self,
         env: BuildEnvironment,
-- 
2.48.1
Re: [PATCH 09/57] docs/qapi-domain: add QAPI xref roles
Posted by Markus Armbruster 3 weeks, 5 days ago
John Snow <jsnow@redhat.com> writes:

> Add domain-specific cross-reference syntax. As of this commit, that
> means new :qapi:mod:`block-core` and :qapi:any:`block-core` referencing
> syntax.
>
> :mod: will only find modules, but :any: will find anything registered to
> the QAPI domain. (In forthcoming commits, this means commands, events,
> enums, etc.)

I understand :any: will find any QAPI schema definitions.  Does it find
modules, too?

How could roles narrower than "definition" be useful?

I'm asking because naming rules preclude naming collisions among
definitions:

* Events are ALL_CAPS

* Commands are lower-case-with-dashes, except some older ones use
  underscores (pragma command-name-exceptions).

* Types are CamelCase.  Note that "C" is not considered a camel.

Fine print: these are the rules for stems, i.e. the name without RFQDN
or 'x-' prefixes, if any.

If :any: finds modules, then commands and modules could collide.
Nothing else can.

> Creating the cross-references is powered by the QAPIXRefRole class;
> resolving them is handled by QAPIDomain.resolve_xref().
>
> QAPIXrefRole is copied almost verbatim from Sphinx's own
> PyXrefRole. PyXrefRole (and QAPIXrefRole) adds two features over the
> base class:
>
> (1) Creating a cross-reference with e.g. :py:class:`~class.name`
> instructs sphinx to omit the fully qualified parts of the resolved name
> from the actual link text. This may be useful in the future if we add
> namespaces to QAPI documentation, e.g. :qapi:cmd:`~qsd.blockdev-backup`
> could link to the QSD-specific documentation for blockdev-backup while
> omitting that prefix from the link text.
>
> (2) Prefixing the link target with a "." changes the search behavior to
> prefer locally-scoped items first.
>
> I think both of these are worth keeping to help manage future namespace
> issues between QEMU, QSD and QGA; but it's possible it's extraneous. It
> may possibly be worth keeping just to keep feature parity with Sphinx's
> other domains; e.g. "principle of least surprise". Dunno.

I generally avoid features without uses.  But I trust your judgement
here: you decide.

> Signed-off-by: John Snow <jsnow@redhat.com>
Re: [PATCH 09/57] docs/qapi-domain: add QAPI xref roles
Posted by John Snow 3 weeks, 4 days ago
On Fri, Mar 7, 2025 at 5:09 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > Add domain-specific cross-reference syntax. As of this commit, that
> > means new :qapi:mod:`block-core` and :qapi:any:`block-core` referencing
> > syntax.
> >
> > :mod: will only find modules, but :any: will find anything registered to
> > the QAPI domain. (In forthcoming commits, this means commands, events,
> > enums, etc.)
>
> I understand :any: will find any QAPI schema definitions.  Does it find
> modules, too?
>

`block-core` and :any:`block-core` will find modules, yes. So will
:qapi:mod:`block-core` and :qapi:any:`block-core`.


>
> How could roles narrower than "definition" be useful?
>

At the very least, :qapi:type:`foo` - which omits commands, events, and
modules -- is useful for the role to apply to field lists, since those
things can never possibly be anything other than a data type. The parser
may protect for this, but the QAPI domain also doesn't bother allowing any
such mishaps either.


> I'm asking because naming rules preclude naming collisions among
> definitions:
>
> * Events are ALL_CAPS
>
> * Commands are lower-case-with-dashes, except some older ones use
>   underscores (pragma command-name-exceptions).
>
> * Types are CamelCase.  Note that "C" is not considered a camel.
>
> Fine print: these are the rules for stems, i.e. the name without RFQDN
> or 'x-' prefixes, if any.
>
> If :any: finds modules, then commands and modules could collide.
> Nothing else can.
>

Right, the narrow roles might not be that useful in practice, but they also
cost virtually nothing codewise. I will admit to just copying the Python
domain. I almost feel as if they're interesting to leave in if only to
serve as documentation for how one could narrow the xref roles, but ...
yeah, I admit it's a bit superfluous.


>
> > Creating the cross-references is powered by the QAPIXRefRole class;
> > resolving them is handled by QAPIDomain.resolve_xref().
> >
> > QAPIXrefRole is copied almost verbatim from Sphinx's own
> > PyXrefRole. PyXrefRole (and QAPIXrefRole) adds two features over the
> > base class:
> >
> > (1) Creating a cross-reference with e.g. :py:class:`~class.name`
> > instructs sphinx to omit the fully qualified parts of the resolved name
> > from the actual link text. This may be useful in the future if we add
> > namespaces to QAPI documentation, e.g. :qapi:cmd:`~qsd.blockdev-backup`
> > could link to the QSD-specific documentation for blockdev-backup while
> > omitting that prefix from the link text.
> >
> > (2) Prefixing the link target with a "." changes the search behavior to
> > prefer locally-scoped items first.
> >
> > I think both of these are worth keeping to help manage future namespace
> > issues between QEMU, QSD and QGA; but it's possible it's extraneous. It
> > may possibly be worth keeping just to keep feature parity with Sphinx's
> > other domains; e.g. "principle of least surprise". Dunno.
>
> I generally avoid features without uses.  But I trust your judgement
> here: you decide.
>

Generally, I felt it was easiest to "go with the flow". I am actually
halfway through adding namespace support (surprise...!) and I will
re-evaluate how useful these syntax features are when I'm done.

(I don't know if I will include that feature in the respin for this series
or not, it might be a just-after-freeze thing.)


>
> > Signed-off-by: John Snow <jsnow@redhat.com>
>
>