Adding upstream version 1:10.0.2+ds.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
This commit is contained in:
parent
bf2768bd0f
commit
ea34ddeea6
37998 changed files with 9510514 additions and 0 deletions
733
docs/sphinx/qapidoc.py
Normal file
733
docs/sphinx/qapidoc.py
Normal file
|
@ -0,0 +1,733 @@
|
|||
# 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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue