docs/qapidoc: Add "the members of" pointers

Add "the members of ..." pointers to Members and Arguments lists where
appropriate, with clickable cross-references - so it's a slight
improvement over the old system :)

This patch is meant to be a temporary solution until we can review and
merge the inliner.

The implementation of this patch is a little bit of a hack: Sphinx is
not designed to allow you to mix fields of different "type"; i.e. mixing
member descriptions and free-form text under the same heading. To
accomplish this with a minimum of hackery, we technically document a
"dummy field" and then just strip off the documentation for that dummy
field in a post-processing step. We use the "q_dummy" variable for this
purpose, then strip it back out before final processing. If this
processing step should fail, you'll see warnings for a bad
cross-reference. (So if you don't see any, it must be working!)

Signed-off-by: John Snow <jsnow@redhat.com>
Message-ID: <20250311034303.75779-58-jsnow@redhat.com>
Acked-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
This commit is contained in:
John Snow 2025-03-10 23:42:55 -04:00 committed by Markus Armbruster
parent 7f6f24aaf5
commit 1884492e64
2 changed files with 77 additions and 3 deletions

View file

@ -433,6 +433,24 @@ class QAPIObject(QAPIDescription):
self._validate_field(field)
class SpecialTypedField(CompatTypedField):
def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
ret = super().make_field(*args, **kwargs)
# Look for the characteristic " -- " text node that Sphinx
# inserts for each TypedField entry ...
for node in ret.traverse(lambda n: str(n) == " -- "):
par = node.parent
if par.children[0].astext() != "q_dummy":
continue
# If the first node's text is q_dummy, this is a dummy
# field we want to strip down to just its contents.
del par.children[:-1]
return ret
class QAPICommand(QAPIObject):
"""Description of a QAPI Command."""
@ -440,7 +458,7 @@ class QAPICommand(QAPIObject):
doc_field_types.extend(
[
# :arg TypeName ArgName: descr
CompatTypedField(
SpecialTypedField(
"argument",
label=_("Arguments"),
names=("arg",),
@ -508,7 +526,7 @@ class QAPIObjectWithMembers(QAPIObject):
doc_field_types.extend(
[
# :member type name: descr
CompatTypedField(
SpecialTypedField(
"member",
label=_("Members"),
names=("memb",),

View file

@ -47,8 +47,10 @@ from qapi.schema import (
QAPISchemaCommand,
QAPISchemaDefinition,
QAPISchemaEnumMember,
QAPISchemaEvent,
QAPISchemaFeature,
QAPISchemaMember,
QAPISchemaObjectType,
QAPISchemaObjectTypeMember,
QAPISchemaType,
QAPISchemaVisitor,
@ -298,11 +300,61 @@ class Transmogrifier:
self.ensure_blank_line()
def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
def _get_target(
ent: QAPISchemaDefinition,
) -> Optional[QAPISchemaDefinition]:
if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
return ent.arg_type
if isinstance(ent, QAPISchemaObjectType):
return ent.base
return None
target = _get_target(ent)
if target is not None and not target.is_implicit():
assert ent.info
self.add_field(
self.member_field_type,
"q_dummy",
f"The members of :qapi:type:`{target.name}`.",
ent.info,
"q_dummy",
)
if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
for variant in ent.branches.variants:
if variant.type.name == "q_empty":
continue
assert ent.info
self.add_field(
self.member_field_type,
"q_dummy",
f" When ``{ent.branches.tag_member.name}`` is "
f"``{variant.name}``: "
f"The members of :qapi:type:`{variant.type.name}`.",
ent.info,
"q_dummy",
)
def visit_sections(self, ent: QAPISchemaDefinition) -> None:
sections = ent.doc.all_sections if ent.doc else []
# Determine the index location at which we should generate
# documentation for "The members of ..." pointers. This should
# go at the end of the members section(s) if any. Note that
# index 0 is assumed to be a plain intro section, even if it is
# empty; and that a members section if present will always
# immediately follow the opening PLAIN section.
gen_index = 1
if len(sections) > 1:
while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
gen_index += 1
if gen_index >= len(sections):
break
# Add sections in source order:
for section in sections:
for i, section in enumerate(sections):
# @var is translated to ``var``:
section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
@ -324,6 +376,10 @@ class Transmogrifier:
else:
assert False
# Generate "The members of ..." entries if necessary:
if i == gen_index - 1:
self._insert_member_pointer(ent)
self.ensure_blank_line()
# Transmogrification core methods