Add a :namespace: option to the qapi-doc directive, which inserts a qapi:namespace directive into the start of the generated document. This, in turn, associates all auto-generated definitions by this directive with the specified namespace. The source info for these generated lines are credited to the start of the qapi-doc directive, which isn't precisely correct, but I wasn't sure how to get it more accurate without some re-parsing shenanigans. Signed-off-by: John Snow <jsnow@redhat.com> Message-ID: <20250313044312.189276-7-jsnow@redhat.com> Acked-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
		
			
				
	
	
		
			734 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			734 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# coding=utf-8
 | 
						|
#
 | 
						|
# QEMU qapidoc QAPI file parsing extension
 | 
						|
#
 | 
						|
# Copyright (c) 2024-2025 Red Hat
 | 
						|
# Copyright (c) 2020 Linaro
 | 
						|
#
 | 
						|
# This work is licensed under the terms of the GNU GPLv2 or later.
 | 
						|
# See the COPYING file in the top-level directory.
 | 
						|
 | 
						|
"""
 | 
						|
qapidoc is a Sphinx extension that implements the qapi-doc directive
 | 
						|
 | 
						|
The purpose of this extension is to read the documentation comments
 | 
						|
in QAPI schema files, and insert them all into the current document.
 | 
						|
 | 
						|
It implements one new rST directive, "qapi-doc::".
 | 
						|
Each qapi-doc:: directive takes one argument, which is the
 | 
						|
pathname of the schema file to process, relative to the source tree.
 | 
						|
 | 
						|
The docs/conf.py file must set the qapidoc_srctree config value to
 | 
						|
the root of the QEMU source tree.
 | 
						|
 | 
						|
The Sphinx documentation on writing extensions is at:
 | 
						|
https://www.sphinx-doc.org/en/master/development/index.html
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
__version__ = "2.0"
 | 
						|
 | 
						|
from contextlib import contextmanager
 | 
						|
import os
 | 
						|
from pathlib import Path
 | 
						|
import re
 | 
						|
import sys
 | 
						|
from typing import TYPE_CHECKING
 | 
						|
 | 
						|
from docutils import nodes
 | 
						|
from docutils.parsers.rst import directives
 | 
						|
from docutils.statemachine import StringList
 | 
						|
from qapi.error import QAPIError
 | 
						|
from qapi.parser import QAPIDoc
 | 
						|
from qapi.schema import (
 | 
						|
    QAPISchema,
 | 
						|
    QAPISchemaArrayType,
 | 
						|
    QAPISchemaCommand,
 | 
						|
    QAPISchemaDefinition,
 | 
						|
    QAPISchemaEnumMember,
 | 
						|
    QAPISchemaEvent,
 | 
						|
    QAPISchemaFeature,
 | 
						|
    QAPISchemaMember,
 | 
						|
    QAPISchemaObjectType,
 | 
						|
    QAPISchemaObjectTypeMember,
 | 
						|
    QAPISchemaType,
 | 
						|
    QAPISchemaVisitor,
 | 
						|
)
 | 
						|
from qapi.source import QAPISourceInfo
 | 
						|
 | 
						|
from qapidoc_legacy import QAPISchemaGenRSTVisitor  # type: ignore
 | 
						|
from sphinx import addnodes
 | 
						|
from sphinx.directives.code import CodeBlock
 | 
						|
from sphinx.errors import ExtensionError
 | 
						|
from sphinx.util import logging
 | 
						|
from sphinx.util.docutils import SphinxDirective, switch_source_input
 | 
						|
from sphinx.util.nodes import nested_parse_with_titles
 | 
						|
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from typing import (
 | 
						|
        Any,
 | 
						|
        Generator,
 | 
						|
        List,
 | 
						|
        Optional,
 | 
						|
        Sequence,
 | 
						|
        Union,
 | 
						|
    )
 | 
						|
 | 
						|
    from sphinx.application import Sphinx
 | 
						|
    from sphinx.util.typing import ExtensionMetadata
 | 
						|
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
class Transmogrifier:
 | 
						|
    # pylint: disable=too-many-public-methods
 | 
						|
 | 
						|
    # Field names used for different entity types:
 | 
						|
    field_types = {
 | 
						|
        "enum": "value",
 | 
						|
        "struct": "memb",
 | 
						|
        "union": "memb",
 | 
						|
        "event": "memb",
 | 
						|
        "command": "arg",
 | 
						|
        "alternate": "alt",
 | 
						|
    }
 | 
						|
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self._curr_ent: Optional[QAPISchemaDefinition] = None
 | 
						|
        self._result = StringList()
 | 
						|
        self.indent = 0
 | 
						|
 | 
						|
    @property
 | 
						|
    def result(self) -> StringList:
 | 
						|
        return self._result
 | 
						|
 | 
						|
    @property
 | 
						|
    def entity(self) -> QAPISchemaDefinition:
 | 
						|
        assert self._curr_ent is not None
 | 
						|
        return self._curr_ent
 | 
						|
 | 
						|
    @property
 | 
						|
    def member_field_type(self) -> str:
 | 
						|
        return self.field_types[self.entity.meta]
 | 
						|
 | 
						|
    # General-purpose rST generation functions
 | 
						|
 | 
						|
    def get_indent(self) -> str:
 | 
						|
        return "   " * self.indent
 | 
						|
 | 
						|
    @contextmanager
 | 
						|
    def indented(self) -> Generator[None]:
 | 
						|
        self.indent += 1
 | 
						|
        try:
 | 
						|
            yield
 | 
						|
        finally:
 | 
						|
            self.indent -= 1
 | 
						|
 | 
						|
    def add_line_raw(self, line: str, source: str, *lineno: int) -> None:
 | 
						|
        """Append one line of generated reST to the output."""
 | 
						|
 | 
						|
        # NB: Sphinx uses zero-indexed lines; subtract one.
 | 
						|
        lineno = tuple((n - 1 for n in lineno))
 | 
						|
 | 
						|
        if line.strip():
 | 
						|
            # not a blank line
 | 
						|
            self._result.append(
 | 
						|
                self.get_indent() + line.rstrip("\n"), source, *lineno
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            self._result.append("", source, *lineno)
 | 
						|
 | 
						|
    def add_line(self, content: str, info: QAPISourceInfo) -> None:
 | 
						|
        # NB: We *require* an info object; this works out OK because we
 | 
						|
        # don't document built-in objects that don't have
 | 
						|
        # one. Everything else should.
 | 
						|
        self.add_line_raw(content, info.fname, info.line)
 | 
						|
 | 
						|
    def add_lines(
 | 
						|
        self,
 | 
						|
        content: str,
 | 
						|
        info: QAPISourceInfo,
 | 
						|
    ) -> None:
 | 
						|
        lines = content.splitlines(True)
 | 
						|
        for i, line in enumerate(lines):
 | 
						|
            self.add_line_raw(line, info.fname, info.line + i)
 | 
						|
 | 
						|
    def ensure_blank_line(self) -> None:
 | 
						|
        # Empty document -- no blank line required.
 | 
						|
        if not self._result:
 | 
						|
            return
 | 
						|
 | 
						|
        # Last line isn't blank, add one.
 | 
						|
        if self._result[-1].strip():  # pylint: disable=no-member
 | 
						|
            fname, line = self._result.info(-1)
 | 
						|
            assert isinstance(line, int)
 | 
						|
            # New blank line is credited to one-after the current last line.
 | 
						|
            # +2: correct for zero/one index, then increment by one.
 | 
						|
            self.add_line_raw("", fname, line + 2)
 | 
						|
 | 
						|
    def add_field(
 | 
						|
        self,
 | 
						|
        kind: str,
 | 
						|
        name: str,
 | 
						|
        body: str,
 | 
						|
        info: QAPISourceInfo,
 | 
						|
        typ: Optional[str] = None,
 | 
						|
    ) -> None:
 | 
						|
        if typ:
 | 
						|
            text = f":{kind} {typ} {name}: {body}"
 | 
						|
        else:
 | 
						|
            text = f":{kind} {name}: {body}"
 | 
						|
        self.add_lines(text, info)
 | 
						|
 | 
						|
    def format_type(
 | 
						|
        self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
 | 
						|
    ) -> Optional[str]:
 | 
						|
        if isinstance(ent, (QAPISchemaEnumMember, QAPISchemaFeature)):
 | 
						|
            return None
 | 
						|
 | 
						|
        qapi_type = ent
 | 
						|
        optional = False
 | 
						|
        if isinstance(ent, QAPISchemaObjectTypeMember):
 | 
						|
            qapi_type = ent.type
 | 
						|
            optional = ent.optional
 | 
						|
 | 
						|
        if isinstance(qapi_type, QAPISchemaArrayType):
 | 
						|
            ret = f"[{qapi_type.element_type.doc_type()}]"
 | 
						|
        else:
 | 
						|
            assert isinstance(qapi_type, QAPISchemaType)
 | 
						|
            tmp = qapi_type.doc_type()
 | 
						|
            assert tmp
 | 
						|
            ret = tmp
 | 
						|
        if optional:
 | 
						|
            ret += "?"
 | 
						|
 | 
						|
        return ret
 | 
						|
 | 
						|
    def generate_field(
 | 
						|
        self,
 | 
						|
        kind: str,
 | 
						|
        member: QAPISchemaMember,
 | 
						|
        body: str,
 | 
						|
        info: QAPISourceInfo,
 | 
						|
    ) -> None:
 | 
						|
        typ = self.format_type(member)
 | 
						|
        self.add_field(kind, member.name, body, info, typ)
 | 
						|
 | 
						|
    # Transmogrification helpers
 | 
						|
 | 
						|
    def visit_paragraph(self, section: QAPIDoc.Section) -> None:
 | 
						|
        # Squelch empty paragraphs.
 | 
						|
        if not section.text:
 | 
						|
            return
 | 
						|
 | 
						|
        self.ensure_blank_line()
 | 
						|
        self.add_lines(section.text, section.info)
 | 
						|
        self.ensure_blank_line()
 | 
						|
 | 
						|
    def visit_member(self, section: QAPIDoc.ArgSection) -> None:
 | 
						|
        # FIXME: ifcond for members
 | 
						|
        # TODO: features for members (documented at entity-level,
 | 
						|
        # but sometimes defined per-member. Should we add such
 | 
						|
        # information to member descriptions when we can?)
 | 
						|
        assert section.member
 | 
						|
        self.generate_field(
 | 
						|
            self.member_field_type,
 | 
						|
            section.member,
 | 
						|
            # TODO drop fallbacks when undocumented members are outlawed
 | 
						|
            section.text if section.text else "Not documented",
 | 
						|
            section.info,
 | 
						|
        )
 | 
						|
 | 
						|
    def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
 | 
						|
        # FIXME - ifcond for features is not handled at all yet!
 | 
						|
        # Proposal: decorate the right-hand column with some graphical
 | 
						|
        # element to indicate conditional availability?
 | 
						|
        assert section.text  # Guaranteed by parser.py
 | 
						|
        assert section.member
 | 
						|
 | 
						|
        self.generate_field("feat", section.member, section.text, section.info)
 | 
						|
 | 
						|
    def visit_returns(self, section: QAPIDoc.Section) -> None:
 | 
						|
        assert isinstance(self.entity, QAPISchemaCommand)
 | 
						|
        rtype = self.entity.ret_type
 | 
						|
        # q_empty can produce None, but we won't be documenting anything
 | 
						|
        # without an explicit return statement in the doc block, and we
 | 
						|
        # should not have any such explicit statements when there is no
 | 
						|
        # return value.
 | 
						|
        assert rtype
 | 
						|
 | 
						|
        typ = self.format_type(rtype)
 | 
						|
        assert typ
 | 
						|
        assert section.text
 | 
						|
        self.add_field("return", typ, section.text, section.info)
 | 
						|
 | 
						|
    def visit_errors(self, section: QAPIDoc.Section) -> None:
 | 
						|
        # FIXME: the formatting for errors may be inconsistent and may
 | 
						|
        # or may not require different newline placement to ensure
 | 
						|
        # proper rendering as a nested list.
 | 
						|
        self.add_lines(f":error:\n{section.text}", section.info)
 | 
						|
 | 
						|
    def preamble(self, ent: QAPISchemaDefinition) -> None:
 | 
						|
        """
 | 
						|
        Generate option lines for QAPI entity directives.
 | 
						|
        """
 | 
						|
        if ent.doc and ent.doc.since:
 | 
						|
            assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
 | 
						|
            # Generated from the entity's docblock; info location is exact.
 | 
						|
            self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
 | 
						|
 | 
						|
        if ent.ifcond.is_present():
 | 
						|
            doc = ent.ifcond.docgen()
 | 
						|
            assert ent.info
 | 
						|
            # Generated from entity definition; info location is approximate.
 | 
						|
            self.add_line(f":ifcond: {doc}", ent.info)
 | 
						|
 | 
						|
        # Hoist special features such as :deprecated: and :unstable:
 | 
						|
        # into the options block for the entity. If, in the future, new
 | 
						|
        # special features are added, qapi-domain will chirp about
 | 
						|
        # unrecognized options and fail until they are handled in
 | 
						|
        # qapi-domain.
 | 
						|
        for feat in ent.features:
 | 
						|
            if feat.is_special():
 | 
						|
                # FIXME: handle ifcond if present. How to display that
 | 
						|
                # information is TBD.
 | 
						|
                # Generated from entity def; info location is approximate.
 | 
						|
                assert feat.info
 | 
						|
                self.add_line(f":{feat.name}:", feat.info)
 | 
						|
 | 
						|
        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 i, section in enumerate(sections):
 | 
						|
            # @var is translated to ``var``:
 | 
						|
            section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
 | 
						|
 | 
						|
            if section.kind == QAPIDoc.Kind.PLAIN:
 | 
						|
                self.visit_paragraph(section)
 | 
						|
            elif section.kind == QAPIDoc.Kind.MEMBER:
 | 
						|
                assert isinstance(section, QAPIDoc.ArgSection)
 | 
						|
                self.visit_member(section)
 | 
						|
            elif section.kind == QAPIDoc.Kind.FEATURE:
 | 
						|
                assert isinstance(section, QAPIDoc.ArgSection)
 | 
						|
                self.visit_feature(section)
 | 
						|
            elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
 | 
						|
                # Since is handled in preamble, TODO is skipped intentionally.
 | 
						|
                pass
 | 
						|
            elif section.kind == QAPIDoc.Kind.RETURNS:
 | 
						|
                self.visit_returns(section)
 | 
						|
            elif section.kind == QAPIDoc.Kind.ERRORS:
 | 
						|
                self.visit_errors(section)
 | 
						|
            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
 | 
						|
 | 
						|
    def visit_module(self, path: str) -> None:
 | 
						|
        name = Path(path).stem
 | 
						|
        # module directives are credited to the first line of a module file.
 | 
						|
        self.add_line_raw(f".. qapi:module:: {name}", path, 1)
 | 
						|
        self.ensure_blank_line()
 | 
						|
 | 
						|
    def visit_freeform(self, doc: QAPIDoc) -> None:
 | 
						|
        # TODO: Once the old qapidoc transformer is deprecated, freeform
 | 
						|
        # sections can be updated to pure rST, and this transformed removed.
 | 
						|
        #
 | 
						|
        # For now, translate our micro-format into rST. Code adapted
 | 
						|
        # from Peter Maydell's freeform().
 | 
						|
 | 
						|
        assert len(doc.all_sections) == 1, doc.all_sections
 | 
						|
        body = doc.all_sections[0]
 | 
						|
        text = body.text
 | 
						|
        info = doc.info
 | 
						|
 | 
						|
        if re.match(r"=+ ", text):
 | 
						|
            # Section/subsection heading (if present, will always be the
 | 
						|
            # first line of the block)
 | 
						|
            (heading, _, text) = text.partition("\n")
 | 
						|
            (leader, _, heading) = heading.partition(" ")
 | 
						|
            # Implicit +1 for heading in the containing .rst doc
 | 
						|
            level = len(leader) + 1
 | 
						|
 | 
						|
            # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
 | 
						|
            markers = ' #*=_^"'
 | 
						|
            overline = level <= 2
 | 
						|
            marker = markers[level]
 | 
						|
 | 
						|
            self.ensure_blank_line()
 | 
						|
            # This credits all 2 or 3 lines to the single source line.
 | 
						|
            if overline:
 | 
						|
                self.add_line(marker * len(heading), info)
 | 
						|
            self.add_line(heading, info)
 | 
						|
            self.add_line(marker * len(heading), info)
 | 
						|
            self.ensure_blank_line()
 | 
						|
 | 
						|
            # Eat blank line(s) and advance info
 | 
						|
            trimmed = text.lstrip("\n")
 | 
						|
            text = trimmed
 | 
						|
            info = info.next_line(len(text) - len(trimmed) + 1)
 | 
						|
 | 
						|
        self.add_lines(text, info)
 | 
						|
        self.ensure_blank_line()
 | 
						|
 | 
						|
    def visit_entity(self, ent: QAPISchemaDefinition) -> None:
 | 
						|
        assert ent.info
 | 
						|
 | 
						|
        try:
 | 
						|
            self._curr_ent = ent
 | 
						|
 | 
						|
            # Squish structs and unions together into an "object" directive.
 | 
						|
            meta = ent.meta
 | 
						|
            if meta in ("struct", "union"):
 | 
						|
                meta = "object"
 | 
						|
 | 
						|
            # This line gets credited to the start of the /definition/.
 | 
						|
            self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
 | 
						|
            with self.indented():
 | 
						|
                self.preamble(ent)
 | 
						|
                self.visit_sections(ent)
 | 
						|
        finally:
 | 
						|
            self._curr_ent = None
 | 
						|
 | 
						|
    def set_namespace(self, namespace: str, source: str, lineno: int) -> None:
 | 
						|
        self.add_line_raw(
 | 
						|
            f".. qapi:namespace:: {namespace}", source, lineno + 1
 | 
						|
        )
 | 
						|
        self.ensure_blank_line()
 | 
						|
 | 
						|
 | 
						|
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
 | 
						|
    """A QAPI schema visitor which adds Sphinx dependencies each module
 | 
						|
 | 
						|
    This class calls the Sphinx note_dependency() function to tell Sphinx
 | 
						|
    that the generated documentation output depends on the input
 | 
						|
    schema file associated with each module in the QAPI input.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, env: Any, qapidir: str) -> None:
 | 
						|
        self._env = env
 | 
						|
        self._qapidir = qapidir
 | 
						|
 | 
						|
    def visit_module(self, name: str) -> None:
 | 
						|
        if name != "./builtin":
 | 
						|
            qapifile = self._qapidir + "/" + name
 | 
						|
            self._env.note_dependency(os.path.abspath(qapifile))
 | 
						|
        super().visit_module(name)
 | 
						|
 | 
						|
 | 
						|
class NestedDirective(SphinxDirective):
 | 
						|
    def run(self) -> Sequence[nodes.Node]:
 | 
						|
        raise NotImplementedError
 | 
						|
 | 
						|
    def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
 | 
						|
        """
 | 
						|
        Parse rST source lines and add them to the specified node
 | 
						|
 | 
						|
        Take the list of rST source lines rstlist, parse them as
 | 
						|
        rST, and add the resulting docutils nodes as children of node.
 | 
						|
        The nodes are parsed in a way that allows them to include
 | 
						|
        subheadings (titles) without confusing the rendering of
 | 
						|
        anything else.
 | 
						|
        """
 | 
						|
        with switch_source_input(self.state, rstlist):
 | 
						|
            nested_parse_with_titles(self.state, rstlist, node)
 | 
						|
 | 
						|
 | 
						|
class QAPIDocDirective(NestedDirective):
 | 
						|
    """Extract documentation from the specified QAPI .json file"""
 | 
						|
 | 
						|
    required_argument = 1
 | 
						|
    optional_arguments = 1
 | 
						|
    option_spec = {
 | 
						|
        "qapifile": directives.unchanged_required,
 | 
						|
        "namespace": directives.unchanged,
 | 
						|
        "transmogrify": directives.flag,
 | 
						|
    }
 | 
						|
    has_content = False
 | 
						|
 | 
						|
    def new_serialno(self) -> str:
 | 
						|
        """Return a unique new ID string suitable for use as a node's ID"""
 | 
						|
        env = self.state.document.settings.env
 | 
						|
        return "qapidoc-%d" % env.new_serialno("qapidoc")
 | 
						|
 | 
						|
    def transmogrify(self, schema: QAPISchema) -> nodes.Element:
 | 
						|
        logger.info("Transmogrifying QAPI to rST ...")
 | 
						|
        vis = Transmogrifier()
 | 
						|
        modules = set()
 | 
						|
 | 
						|
        if "namespace" in self.options:
 | 
						|
            vis.set_namespace(
 | 
						|
                self.options["namespace"], *self.get_source_info()
 | 
						|
            )
 | 
						|
 | 
						|
        for doc in schema.docs:
 | 
						|
            module_source = doc.info.fname
 | 
						|
            if module_source not in modules:
 | 
						|
                vis.visit_module(module_source)
 | 
						|
                modules.add(module_source)
 | 
						|
 | 
						|
            if doc.symbol:
 | 
						|
                ent = schema.lookup_entity(doc.symbol)
 | 
						|
                assert isinstance(ent, QAPISchemaDefinition)
 | 
						|
                vis.visit_entity(ent)
 | 
						|
            else:
 | 
						|
                vis.visit_freeform(doc)
 | 
						|
 | 
						|
        logger.info("Transmogrification complete.")
 | 
						|
 | 
						|
        contentnode = nodes.section()
 | 
						|
        content = vis.result
 | 
						|
        titles_allowed = True
 | 
						|
 | 
						|
        logger.info("Transmogrifier running nested parse ...")
 | 
						|
        with switch_source_input(self.state, content):
 | 
						|
            if titles_allowed:
 | 
						|
                node: nodes.Element = nodes.section()
 | 
						|
                node.document = self.state.document
 | 
						|
                nested_parse_with_titles(self.state, content, contentnode)
 | 
						|
            else:
 | 
						|
                node = nodes.paragraph()
 | 
						|
                node.document = self.state.document
 | 
						|
                self.state.nested_parse(content, 0, contentnode)
 | 
						|
        logger.info("Transmogrifier's nested parse completed.")
 | 
						|
 | 
						|
        if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
 | 
						|
            argname = "_".join(Path(self.arguments[0]).parts)
 | 
						|
            name = Path(argname).stem + ".ir"
 | 
						|
            self.write_intermediate(content, name)
 | 
						|
 | 
						|
        sys.stdout.flush()
 | 
						|
        return contentnode
 | 
						|
 | 
						|
    def write_intermediate(self, content: StringList, filename: str) -> None:
 | 
						|
        logger.info(
 | 
						|
            "writing intermediate rST for '%s' to '%s'",
 | 
						|
            self.arguments[0],
 | 
						|
            filename,
 | 
						|
        )
 | 
						|
 | 
						|
        srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
 | 
						|
        outlines = []
 | 
						|
        lcol_width = 0
 | 
						|
 | 
						|
        for i, line in enumerate(content):
 | 
						|
            src, lineno = content.info(i)
 | 
						|
            srcpath = Path(src).resolve()
 | 
						|
            srcpath = srcpath.relative_to(srctree)
 | 
						|
 | 
						|
            lcol = f"{srcpath}:{lineno:04d}"
 | 
						|
            lcol_width = max(lcol_width, len(lcol))
 | 
						|
            outlines.append((lcol, line))
 | 
						|
 | 
						|
        with open(filename, "w", encoding="UTF-8") as outfile:
 | 
						|
            for lcol, rcol in outlines:
 | 
						|
                outfile.write(lcol.rjust(lcol_width))
 | 
						|
                outfile.write(" |")
 | 
						|
                if rcol:
 | 
						|
                    outfile.write(f" {rcol}")
 | 
						|
                outfile.write("\n")
 | 
						|
 | 
						|
    def legacy(self, schema: QAPISchema) -> nodes.Element:
 | 
						|
        vis = QAPISchemaGenRSTVisitor(self)
 | 
						|
        vis.visit_begin(schema)
 | 
						|
        for doc in schema.docs:
 | 
						|
            if doc.symbol:
 | 
						|
                vis.symbol(doc, schema.lookup_entity(doc.symbol))
 | 
						|
            else:
 | 
						|
                vis.freeform(doc)
 | 
						|
        return vis.get_document_node()  # type: ignore
 | 
						|
 | 
						|
    def run(self) -> Sequence[nodes.Node]:
 | 
						|
        env = self.state.document.settings.env
 | 
						|
        qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
 | 
						|
        qapidir = os.path.dirname(qapifile)
 | 
						|
        transmogrify = "transmogrify" in self.options
 | 
						|
 | 
						|
        try:
 | 
						|
            schema = QAPISchema(qapifile)
 | 
						|
 | 
						|
            # First tell Sphinx about all the schema files that the
 | 
						|
            # output documentation depends on (including 'qapifile' itself)
 | 
						|
            schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
 | 
						|
        except QAPIError as err:
 | 
						|
            # Launder QAPI parse errors into Sphinx extension errors
 | 
						|
            # so they are displayed nicely to the user
 | 
						|
            raise ExtensionError(str(err)) from err
 | 
						|
 | 
						|
        if transmogrify:
 | 
						|
            contentnode = self.transmogrify(schema)
 | 
						|
        else:
 | 
						|
            contentnode = self.legacy(schema)
 | 
						|
 | 
						|
        return contentnode.children
 | 
						|
 | 
						|
 | 
						|
class QMPExample(CodeBlock, NestedDirective):
 | 
						|
    """
 | 
						|
    Custom admonition for QMP code examples.
 | 
						|
 | 
						|
    When the :annotated: option is present, the body of this directive
 | 
						|
    is parsed as normal rST, but with any '::' code blocks set to use
 | 
						|
    the QMP lexer. Code blocks must be explicitly written by the user,
 | 
						|
    but this allows for intermingling explanatory paragraphs with
 | 
						|
    arbitrary rST syntax and code blocks for more involved examples.
 | 
						|
 | 
						|
    When :annotated: is absent, the directive body is treated as a
 | 
						|
    simple standalone QMP code block literal.
 | 
						|
    """
 | 
						|
 | 
						|
    required_argument = 0
 | 
						|
    optional_arguments = 0
 | 
						|
    has_content = True
 | 
						|
    option_spec = {
 | 
						|
        "annotated": directives.flag,
 | 
						|
        "title": directives.unchanged,
 | 
						|
    }
 | 
						|
 | 
						|
    def _highlightlang(self) -> addnodes.highlightlang:
 | 
						|
        """Return the current highlightlang setting for the document"""
 | 
						|
        node = None
 | 
						|
        doc = self.state.document
 | 
						|
 | 
						|
        if hasattr(doc, "findall"):
 | 
						|
            # docutils >= 0.18.1
 | 
						|
            for node in doc.findall(addnodes.highlightlang):
 | 
						|
                pass
 | 
						|
        else:
 | 
						|
            for elem in doc.traverse():
 | 
						|
                if isinstance(elem, addnodes.highlightlang):
 | 
						|
                    node = elem
 | 
						|
 | 
						|
        if node:
 | 
						|
            return node
 | 
						|
 | 
						|
        # No explicit directive found, use defaults
 | 
						|
        node = addnodes.highlightlang(
 | 
						|
            lang=self.env.config.highlight_language,
 | 
						|
            force=False,
 | 
						|
            # Yes, Sphinx uses this value to effectively disable line
 | 
						|
            # numbers and not 0 or None or -1 or something. ¯\_(ツ)_/¯
 | 
						|
            linenothreshold=sys.maxsize,
 | 
						|
        )
 | 
						|
        return node
 | 
						|
 | 
						|
    def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
 | 
						|
        title = "Example:"
 | 
						|
        if "title" in self.options:
 | 
						|
            title = f"{title} {self.options['title']}"
 | 
						|
 | 
						|
        admon = nodes.admonition(
 | 
						|
            "",
 | 
						|
            nodes.title("", title),
 | 
						|
            *content,
 | 
						|
            classes=["admonition", "admonition-example"],
 | 
						|
        )
 | 
						|
        return [admon]
 | 
						|
 | 
						|
    def run_annotated(self) -> List[nodes.Node]:
 | 
						|
        lang_node = self._highlightlang()
 | 
						|
 | 
						|
        content_node: nodes.Element = nodes.section()
 | 
						|
 | 
						|
        # Configure QMP highlighting for "::" blocks, if needed
 | 
						|
        if lang_node["lang"] != "QMP":
 | 
						|
            content_node += addnodes.highlightlang(
 | 
						|
                lang="QMP",
 | 
						|
                force=False,  # "True" ignores lexing errors
 | 
						|
                linenothreshold=lang_node["linenothreshold"],
 | 
						|
            )
 | 
						|
 | 
						|
        self.do_parse(self.content, content_node)
 | 
						|
 | 
						|
        # Restore prior language highlighting, if needed
 | 
						|
        if lang_node["lang"] != "QMP":
 | 
						|
            content_node += addnodes.highlightlang(**lang_node.attributes)
 | 
						|
 | 
						|
        return content_node.children
 | 
						|
 | 
						|
    def run(self) -> List[nodes.Node]:
 | 
						|
        annotated = "annotated" in self.options
 | 
						|
 | 
						|
        if annotated:
 | 
						|
            content_nodes = self.run_annotated()
 | 
						|
        else:
 | 
						|
            self.arguments = ["QMP"]
 | 
						|
            content_nodes = super().run()
 | 
						|
 | 
						|
        return self.admonition_wrap(*content_nodes)
 | 
						|
 | 
						|
 | 
						|
def setup(app: Sphinx) -> ExtensionMetadata:
 | 
						|
    """Register qapi-doc directive with Sphinx"""
 | 
						|
    app.setup_extension("qapi_domain")
 | 
						|
    app.add_config_value("qapidoc_srctree", None, "env")
 | 
						|
    app.add_directive("qapi-doc", QAPIDocDirective)
 | 
						|
    app.add_directive("qmp-example", QMPExample)
 | 
						|
 | 
						|
    return {
 | 
						|
        "version": __version__,
 | 
						|
        "parallel_read_safe": True,
 | 
						|
        "parallel_write_safe": True,
 | 
						|
    }
 |