Import QEMU upstream snapshot d2e570c
Upstream: https://gitlab.com/qemu-project/qemu.git Upstream-Commit: d2e570cc0f97b936902a5b1b86b73c0f5998b475
This commit is contained in:
commit
cf256aa081
11315 files changed
+3598369
No files matched your search
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Sphinx cross-version compatibility goop
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
Type,
|
||||
)
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, Node, Text
|
||||
from docutils.statemachine import StringList
|
||||
|
||||
import sphinx
|
||||
from sphinx import addnodes, util
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.environment import BuildEnvironment
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.util import docfields
|
||||
from sphinx.util.docutils import (
|
||||
ReferenceRole,
|
||||
SphinxDirective,
|
||||
switch_source_input,
|
||||
)
|
||||
from sphinx.util.typing import TextlikeNode
|
||||
|
||||
|
||||
MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
|
||||
|
||||
|
||||
SpaceNode: Callable[[str], Node]
|
||||
KeywordNode: Callable[[str, str], Node]
|
||||
|
||||
if sphinx.version_info[:3] >= (4, 0, 0):
|
||||
SpaceNode = addnodes.desc_sig_space
|
||||
KeywordNode = addnodes.desc_sig_keyword
|
||||
else:
|
||||
SpaceNode = Text
|
||||
KeywordNode = addnodes.desc_annotation
|
||||
|
||||
|
||||
def nested_parse_with_titles(
|
||||
directive: SphinxDirective, content_node: Element
|
||||
) -> None:
|
||||
"""
|
||||
This helper preserves error parsing context across sphinx versions.
|
||||
"""
|
||||
|
||||
# necessary so that the child nodes get the right source/line set
|
||||
content_node.document = directive.state.document
|
||||
|
||||
try:
|
||||
# Modern sphinx (6.2.0+) supports proper offsetting for
|
||||
# nested parse error context management
|
||||
util.nodes.nested_parse_with_titles(
|
||||
directive.state,
|
||||
directive.content,
|
||||
content_node,
|
||||
content_offset=directive.content_offset,
|
||||
)
|
||||
except TypeError:
|
||||
# No content_offset argument. Fall back to SSI method.
|
||||
with switch_source_input(directive.state, directive.content):
|
||||
util.nodes.nested_parse_with_titles(
|
||||
directive.state, directive.content, content_node
|
||||
)
|
||||
|
||||
|
||||
# ###########################################
|
||||
# xref compatibility hacks for Sphinx < 4.1 #
|
||||
# ###########################################
|
||||
|
||||
# When we require >= Sphinx 4.1, the following function and the
|
||||
# subsequent 3 compatibility classes can be removed. Anywhere in
|
||||
# qapi_domain that uses one of these Compat* types can be switched to
|
||||
# using the garden-variety lib-provided classes with no trickery.
|
||||
|
||||
|
||||
def _compat_make_xref( # pylint: disable=unused-argument
|
||||
self: sphinx.util.docfields.Field,
|
||||
rolename: str,
|
||||
domain: str,
|
||||
target: str,
|
||||
innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
|
||||
contnode: Optional[Node] = None,
|
||||
env: Optional[BuildEnvironment] = None,
|
||||
inliner: Any = None,
|
||||
location: Any = None,
|
||||
) -> Node:
|
||||
"""
|
||||
Compatibility workaround for Sphinx versions prior to 4.1.0.
|
||||
|
||||
Older sphinx versions do not use the domain's XRefRole for parsing
|
||||
and formatting cross-references, so we need to perform this magick
|
||||
ourselves to avoid needing to write the parser/formatter in two
|
||||
separate places.
|
||||
|
||||
This workaround isn't brick-for-brick compatible with modern Sphinx
|
||||
versions, because we do not have access to the parent directive's
|
||||
state during this parsing like we do in more modern versions.
|
||||
|
||||
It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
|
||||
"""
|
||||
|
||||
# Yes, this function is gross. Pre-4.1 support is a miracle.
|
||||
# pylint: disable=too-many-locals
|
||||
|
||||
assert env
|
||||
# Note: Sphinx's own code ignores the type warning here, too.
|
||||
if not rolename:
|
||||
return contnode or innernode(target, target) # type: ignore[call-arg]
|
||||
|
||||
# Get the role instance, but don't *execute it* - we lack the
|
||||
# correct state to do so. Instead, we'll just use its public
|
||||
# methods to do our reference formatting, and emulate the rest.
|
||||
role = env.get_domain(domain).roles[rolename]
|
||||
assert isinstance(role, XRefRole)
|
||||
|
||||
# XRefRole features not supported by this compatibility shim;
|
||||
# these were not supported in Sphinx 3.x either, so nothing of
|
||||
# value is really lost.
|
||||
assert not target.startswith("!")
|
||||
assert not re.match(ReferenceRole.explicit_title_re, target)
|
||||
assert not role.lowercase
|
||||
assert not role.fix_parens
|
||||
|
||||
# Code below based mostly on sphinx.roles.XRefRole; run() and
|
||||
# create_xref_node()
|
||||
options = {
|
||||
"refdoc": env.docname,
|
||||
"refdomain": domain,
|
||||
"reftype": rolename,
|
||||
"refexplicit": False,
|
||||
"refwarn": role.warn_dangling,
|
||||
}
|
||||
refnode = role.nodeclass(target, **options)
|
||||
title, target = role.process_link(env, refnode, False, target, target)
|
||||
refnode["reftarget"] = target
|
||||
classes = ["xref", domain, f"{domain}-{rolename}"]
|
||||
refnode += role.innernodeclass(target, title, classes=classes)
|
||||
|
||||
# This is the very gross part of the hack. Normally,
|
||||
# result_nodes takes a document object to which we would pass
|
||||
# self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
|
||||
# inliner to pass, so we have nothing to pass here. However, the
|
||||
# actual implementation of role.result_nodes in this case
|
||||
# doesn't actually use that argument, so this winds up being
|
||||
# ... fine. Rest easy at night knowing this code only runs under
|
||||
# old versions of Sphinx, so at least it won't change in the
|
||||
# future on us and lead to surprising new failures.
|
||||
# Gross, I know.
|
||||
result_nodes, _messages = role.result_nodes(
|
||||
None, # type: ignore
|
||||
env,
|
||||
refnode,
|
||||
is_ref=True,
|
||||
)
|
||||
return nodes.inline(target, "", *result_nodes)
|
||||
|
||||
|
||||
class CompatField(docfields.Field):
|
||||
if MAKE_XREF_WORKAROUND:
|
||||
make_xref = _compat_make_xref
|
||||
|
||||
|
||||
class CompatGroupedField(docfields.GroupedField):
|
||||
if MAKE_XREF_WORKAROUND:
|
||||
make_xref = _compat_make_xref
|
||||
|
||||
|
||||
class CompatTypedField(docfields.TypedField):
|
||||
if MAKE_XREF_WORKAROUND:
|
||||
make_xref = _compat_make_xref
|
||||
|
||||
|
||||
# ################################################################
|
||||
# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 #
|
||||
# ################################################################
|
||||
|
||||
# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid
|
||||
# subscripting ObjectDescription at runtime can be removed in favor of
|
||||
# just always subscripting the class.
|
||||
|
||||
# When we require Sphinx > 6.2.0, the rest of this compatibility hack
|
||||
# can be dropped and QAPIObject can just inherit directly from
|
||||
# ObjectDescription[Signature].
|
||||
|
||||
SOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0)
|
||||
|
||||
Signature = str
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_BaseClass = ObjectDescription[Signature]
|
||||
else:
|
||||
_BaseClass = ObjectDescription
|
||||
|
||||
|
||||
class ParserFix(_BaseClass):
|
||||
|
||||
_temp_content: StringList
|
||||
_temp_offset: int
|
||||
_temp_node: Optional[addnodes.desc_content]
|
||||
|
||||
def before_content(self) -> None:
|
||||
# Work around a sphinx bug and parse the content ourselves.
|
||||
self._temp_content = self.content
|
||||
self._temp_offset = self.content_offset
|
||||
self._temp_node = None
|
||||
|
||||
if SOURCE_LOCATION_FIX:
|
||||
self._temp_node = addnodes.desc_content()
|
||||
self.state.nested_parse(
|
||||
self.content, self.content_offset, self._temp_node
|
||||
)
|
||||
# Sphinx will try to parse the content block itself,
|
||||
# Give it nothingness to parse instead.
|
||||
self.content = StringList()
|
||||
self.content_offset = 0
|
||||
|
||||
def transform_content(self, content_node: addnodes.desc_content) -> None:
|
||||
# Sphinx workaround: Inject our parsed content and restore state.
|
||||
if self._temp_node:
|
||||
content_node += self._temp_node.children
|
||||
self.content = self._temp_content
|
||||
self.content_offset = self._temp_offset
|
||||
@@ -0,0 +1,166 @@
|
||||
# D-Bus XML documentation extension
|
||||
#
|
||||
# Copyright (C) 2021, Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Author: Marc-André Lureau <[email protected]>
|
||||
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import sphinx
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, Node
|
||||
from docutils.parsers.rst import Directive, directives
|
||||
from docutils.parsers.rst.states import RSTState
|
||||
from docutils.statemachine import StringList, ViewList
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.errors import ExtensionError
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.docstrings import prepare_docstring
|
||||
from sphinx.util.docutils import SphinxDirective, switch_source_input
|
||||
from sphinx.util.nodes import nested_parse_with_titles
|
||||
|
||||
import dbusdomain
|
||||
from dbusparser import parse_dbus_xml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__version__ = "1.0"
|
||||
|
||||
|
||||
class DBusDoc:
|
||||
def __init__(self, sphinx_directive, dbusfile):
|
||||
self._cur_doc = None
|
||||
self._sphinx_directive = sphinx_directive
|
||||
self._dbusfile = dbusfile
|
||||
self._top_node = nodes.section()
|
||||
self.result = StringList()
|
||||
self.indent = ""
|
||||
|
||||
def add_line(self, line: str, *lineno: int) -> None:
|
||||
"""Append one line of generated reST to the output."""
|
||||
if line.strip(): # not a blank line
|
||||
self.result.append(self.indent + line, self._dbusfile, *lineno)
|
||||
else:
|
||||
self.result.append("", self._dbusfile, *lineno)
|
||||
|
||||
def add_method(self, method):
|
||||
self.add_line(f".. dbus:method:: {method.name}")
|
||||
self.add_line("")
|
||||
self.indent += " "
|
||||
for arg in method.in_args:
|
||||
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
for arg in method.out_args:
|
||||
self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
self.add_line("")
|
||||
for line in prepare_docstring("\n" + method.doc_string):
|
||||
self.add_line(line)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
def add_signal(self, signal):
|
||||
self.add_line(f".. dbus:signal:: {signal.name}")
|
||||
self.add_line("")
|
||||
self.indent += " "
|
||||
for arg in signal.args:
|
||||
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
self.add_line("")
|
||||
for line in prepare_docstring("\n" + signal.doc_string):
|
||||
self.add_line(line)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
def add_property(self, prop):
|
||||
self.add_line(f".. dbus:property:: {prop.name}")
|
||||
self.indent += " "
|
||||
self.add_line(f":type: {prop.signature}")
|
||||
access = {"read": "readonly", "write": "writeonly", "readwrite": "readwrite"}[
|
||||
prop.access
|
||||
]
|
||||
self.add_line(f":{access}:")
|
||||
if prop.emits_changed_signal:
|
||||
self.add_line(f":emits-changed: yes")
|
||||
self.add_line("")
|
||||
for line in prepare_docstring("\n" + prop.doc_string):
|
||||
self.add_line(line)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
def add_interface(self, iface):
|
||||
self.add_line(f".. dbus:interface:: {iface.name}")
|
||||
self.add_line("")
|
||||
self.indent += " "
|
||||
for line in prepare_docstring("\n" + iface.doc_string):
|
||||
self.add_line(line)
|
||||
for method in iface.methods:
|
||||
self.add_method(method)
|
||||
for sig in iface.signals:
|
||||
self.add_signal(sig)
|
||||
for prop in iface.properties:
|
||||
self.add_property(prop)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
|
||||
def parse_generated_content(state: RSTState, content: StringList) -> List[Node]:
|
||||
"""Parse a generated content by Documenter."""
|
||||
with switch_source_input(state, content):
|
||||
node = nodes.paragraph()
|
||||
node.document = state.document
|
||||
state.nested_parse(content, 0, node)
|
||||
|
||||
return node.children
|
||||
|
||||
|
||||
class DBusDocDirective(SphinxDirective):
|
||||
"""Extract documentation from the specified D-Bus XML file"""
|
||||
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
|
||||
def run(self):
|
||||
reporter = self.state.document.reporter
|
||||
|
||||
try:
|
||||
source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore
|
||||
except AttributeError:
|
||||
source, lineno = (None, None)
|
||||
|
||||
logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text)
|
||||
|
||||
env = self.state.document.settings.env
|
||||
dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0]
|
||||
with open(dbusfile, "rb") as f:
|
||||
xml_data = f.read()
|
||||
xml = parse_dbus_xml(xml_data)
|
||||
doc = DBusDoc(self, dbusfile)
|
||||
for iface in xml:
|
||||
doc.add_interface(iface)
|
||||
|
||||
result = parse_generated_content(self.state, doc.result)
|
||||
return result
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
"""Register dbus-doc directive with Sphinx"""
|
||||
app.add_config_value("dbusdoc_srctree", None, "env")
|
||||
app.add_directive("dbus-doc", DBusDocDirective)
|
||||
dbusdomain.setup(app)
|
||||
|
||||
return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
|
||||
@@ -0,0 +1,410 @@
|
||||
# D-Bus sphinx domain extension
|
||||
#
|
||||
# Copyright (C) 2021, Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Author: Marc-André Lureau <[email protected]>
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, Node
|
||||
from docutils.parsers.rst import directives
|
||||
from sphinx import addnodes
|
||||
from sphinx.addnodes import desc_signature, pending_xref
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.domains import Domain, Index, IndexEntry, ObjType
|
||||
from sphinx.locale import _
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.util import nodes as node_utils
|
||||
from sphinx.util.docfields import Field, TypedField
|
||||
from sphinx.util.typing import OptionSpec
|
||||
|
||||
|
||||
class DBusDescription(ObjectDescription[str]):
|
||||
"""Base class for DBus objects"""
|
||||
|
||||
option_spec: OptionSpec = ObjectDescription.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"deprecated": directives.flag,
|
||||
}
|
||||
)
|
||||
|
||||
def get_index_text(self, modname: str, name: str) -> str:
|
||||
"""Return the text for the index entry of the object."""
|
||||
raise NotImplementedError("must be implemented in subclasses")
|
||||
|
||||
def add_target_and_index(
|
||||
self, name: str, sig: str, signode: desc_signature
|
||||
) -> None:
|
||||
ifacename = self.env.ref_context.get("dbus:interface")
|
||||
node_id = name
|
||||
if ifacename:
|
||||
node_id = f"{ifacename}.{node_id}"
|
||||
|
||||
signode["names"].append(name)
|
||||
signode["ids"].append(node_id)
|
||||
|
||||
if "noindexentry" not in self.options:
|
||||
indextext = self.get_index_text(ifacename, name)
|
||||
if indextext:
|
||||
self.indexnode["entries"].append(
|
||||
("single", indextext, node_id, "", None)
|
||||
)
|
||||
|
||||
domain = cast(DBusDomain, self.env.get_domain("dbus"))
|
||||
domain.note_object(name, self.objtype, node_id, location=signode)
|
||||
|
||||
|
||||
class DBusInterface(DBusDescription):
|
||||
"""
|
||||
Implementation of ``dbus:interface``.
|
||||
"""
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return ifacename
|
||||
|
||||
def before_content(self) -> None:
|
||||
self.env.ref_context["dbus:interface"] = self.arguments[0]
|
||||
|
||||
def after_content(self) -> None:
|
||||
self.env.ref_context.pop("dbus:interface")
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
signode += addnodes.desc_annotation("interface ", "interface ")
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
return sig
|
||||
|
||||
def run(self) -> List[Node]:
|
||||
_, node = super().run()
|
||||
name = self.arguments[0]
|
||||
section = nodes.section(ids=[name + "-section"])
|
||||
section += nodes.title(name, "%s interface" % name)
|
||||
section += node
|
||||
return [self.indexnode, section]
|
||||
|
||||
|
||||
class DBusMember(DBusDescription):
|
||||
|
||||
signal = False
|
||||
|
||||
|
||||
class DBusMethod(DBusMember):
|
||||
"""
|
||||
Implementation of ``dbus:method``.
|
||||
"""
|
||||
|
||||
option_spec: OptionSpec = DBusMember.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"noreply": directives.flag,
|
||||
}
|
||||
)
|
||||
|
||||
doc_field_types: List[Field] = [
|
||||
TypedField(
|
||||
"arg",
|
||||
label=_("Arguments"),
|
||||
names=("arg",),
|
||||
rolename="arg",
|
||||
typerolename=None,
|
||||
typenames=("argtype", "type"),
|
||||
),
|
||||
TypedField(
|
||||
"ret",
|
||||
label=_("Returns"),
|
||||
names=("ret",),
|
||||
rolename="ret",
|
||||
typerolename=None,
|
||||
typenames=("rettype", "type"),
|
||||
),
|
||||
]
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s() (%s method)") % (name, ifacename)
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
params = addnodes.desc_parameterlist()
|
||||
returns = addnodes.desc_parameterlist()
|
||||
|
||||
contentnode = addnodes.desc_content()
|
||||
self.state.nested_parse(self.content, self.content_offset, contentnode)
|
||||
for child in contentnode:
|
||||
if isinstance(child, nodes.field_list):
|
||||
for field in child:
|
||||
ty, sg, name = field[0].astext().split(None, 2)
|
||||
param = addnodes.desc_parameter()
|
||||
param += addnodes.desc_sig_keyword_type(sg, sg)
|
||||
param += addnodes.desc_sig_space()
|
||||
param += addnodes.desc_sig_name(name, name)
|
||||
if ty == "arg":
|
||||
params += param
|
||||
elif ty == "ret":
|
||||
returns += param
|
||||
|
||||
anno = "signal " if self.signal else "method "
|
||||
signode += addnodes.desc_annotation(anno, anno)
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
signode += params
|
||||
if not self.signal and "noreply" not in self.options:
|
||||
ret = addnodes.desc_returns()
|
||||
ret += returns
|
||||
signode += ret
|
||||
|
||||
return sig
|
||||
|
||||
|
||||
class DBusSignal(DBusMethod):
|
||||
"""
|
||||
Implementation of ``dbus:signal``.
|
||||
"""
|
||||
|
||||
doc_field_types: List[Field] = [
|
||||
TypedField(
|
||||
"arg",
|
||||
label=_("Arguments"),
|
||||
names=("arg",),
|
||||
rolename="arg",
|
||||
typerolename=None,
|
||||
typenames=("argtype", "type"),
|
||||
),
|
||||
]
|
||||
signal = True
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s() (%s signal)") % (name, ifacename)
|
||||
|
||||
|
||||
class DBusProperty(DBusMember):
|
||||
"""
|
||||
Implementation of ``dbus:property``.
|
||||
"""
|
||||
|
||||
option_spec: OptionSpec = DBusMember.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"type": directives.unchanged,
|
||||
"readonly": directives.flag,
|
||||
"writeonly": directives.flag,
|
||||
"readwrite": directives.flag,
|
||||
"emits-changed": directives.unchanged,
|
||||
}
|
||||
)
|
||||
|
||||
doc_field_types: List[Field] = []
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s (%s property)") % (name, ifacename)
|
||||
|
||||
def transform_content(self, contentnode: addnodes.desc_content) -> None:
|
||||
fieldlist = nodes.field_list()
|
||||
access = None
|
||||
if "readonly" in self.options:
|
||||
access = _("read-only")
|
||||
if "writeonly" in self.options:
|
||||
access = _("write-only")
|
||||
if "readwrite" in self.options:
|
||||
access = _("read & write")
|
||||
if access:
|
||||
content = nodes.Text(access)
|
||||
fieldname = nodes.field_name("", _("Access"))
|
||||
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
|
||||
field = nodes.field("", fieldname, fieldbody)
|
||||
fieldlist += field
|
||||
emits = self.options.get("emits-changed", None)
|
||||
if emits:
|
||||
content = nodes.Text(emits)
|
||||
fieldname = nodes.field_name("", _("Emits Changed"))
|
||||
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
|
||||
field = nodes.field("", fieldname, fieldbody)
|
||||
fieldlist += field
|
||||
if len(fieldlist) > 0:
|
||||
contentnode.insert(0, fieldlist)
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
contentnode = addnodes.desc_content()
|
||||
self.state.nested_parse(self.content, self.content_offset, contentnode)
|
||||
ty = self.options.get("type")
|
||||
|
||||
signode += addnodes.desc_annotation("property ", "property ")
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
signode += addnodes.desc_sig_punctuation("", ":")
|
||||
signode += addnodes.desc_sig_keyword_type(ty, ty)
|
||||
return sig
|
||||
|
||||
def run(self) -> List[Node]:
|
||||
self.name = "dbus:member"
|
||||
return super().run()
|
||||
|
||||
|
||||
class DBusXRef(XRefRole):
|
||||
def process_link(self, env, refnode, has_explicit_title, title, target):
|
||||
refnode["dbus:interface"] = env.ref_context.get("dbus:interface")
|
||||
if not has_explicit_title:
|
||||
title = title.lstrip(".") # only has a meaning for the target
|
||||
target = target.lstrip("~") # only has a meaning for the title
|
||||
# if the first character is a tilde, don't display the module/class
|
||||
# parts of the contents
|
||||
if title[0:1] == "~":
|
||||
title = title[1:]
|
||||
dot = title.rfind(".")
|
||||
if dot != -1:
|
||||
title = title[dot + 1 :]
|
||||
# if the first character is a dot, search more specific namespaces first
|
||||
# else search builtins first
|
||||
if target[0:1] == ".":
|
||||
target = target[1:]
|
||||
refnode["refspecific"] = True
|
||||
return title, target
|
||||
|
||||
|
||||
class DBusIndex(Index):
|
||||
"""
|
||||
Index subclass to provide a D-Bus interfaces index.
|
||||
"""
|
||||
|
||||
name = "dbusindex"
|
||||
localname = _("D-Bus Interfaces Index")
|
||||
shortname = _("dbus")
|
||||
|
||||
def generate(
|
||||
self, docnames: Iterable[str] = None
|
||||
) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
|
||||
content: Dict[str, List[IndexEntry]] = {}
|
||||
# list of prefixes to ignore
|
||||
ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"]
|
||||
ignores = sorted(ignores, key=len, reverse=True)
|
||||
|
||||
ifaces = sorted(
|
||||
[
|
||||
x
|
||||
for x in self.domain.data["objects"].items()
|
||||
if x[1].objtype == "interface"
|
||||
],
|
||||
key=lambda x: x[0].lower(),
|
||||
)
|
||||
for name, (docname, node_id, _) in ifaces:
|
||||
if docnames and docname not in docnames:
|
||||
continue
|
||||
|
||||
for ignore in ignores:
|
||||
if name.startswith(ignore):
|
||||
name = name[len(ignore) :]
|
||||
stripped = ignore
|
||||
break
|
||||
else:
|
||||
stripped = ""
|
||||
|
||||
entries = content.setdefault(name[0].lower(), [])
|
||||
entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", ""))
|
||||
|
||||
# sort by first letter
|
||||
sorted_content = sorted(content.items())
|
||||
|
||||
return sorted_content, False
|
||||
|
||||
|
||||
class ObjectEntry(NamedTuple):
|
||||
docname: str
|
||||
node_id: str
|
||||
objtype: str
|
||||
|
||||
|
||||
class DBusDomain(Domain):
|
||||
"""
|
||||
Implementation of the D-Bus domain.
|
||||
"""
|
||||
|
||||
name = "dbus"
|
||||
label = "D-Bus"
|
||||
object_types: Dict[str, ObjType] = {
|
||||
"interface": ObjType(_("interface"), "iface", "obj"),
|
||||
"method": ObjType(_("method"), "meth", "obj"),
|
||||
"signal": ObjType(_("signal"), "sig", "obj"),
|
||||
"property": ObjType(_("property"), "attr", "_prop", "obj"),
|
||||
}
|
||||
directives = {
|
||||
"interface": DBusInterface,
|
||||
"method": DBusMethod,
|
||||
"signal": DBusSignal,
|
||||
"property": DBusProperty,
|
||||
}
|
||||
roles = {
|
||||
"iface": DBusXRef(),
|
||||
"meth": DBusXRef(),
|
||||
"sig": DBusXRef(),
|
||||
"prop": DBusXRef(),
|
||||
}
|
||||
initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
|
||||
"objects": {}, # fullname -> ObjectEntry
|
||||
}
|
||||
indices = [
|
||||
DBusIndex,
|
||||
]
|
||||
|
||||
@property
|
||||
def objects(self) -> Dict[str, ObjectEntry]:
|
||||
return self.data.setdefault("objects", {}) # fullname -> ObjectEntry
|
||||
|
||||
def note_object(
|
||||
self, name: str, objtype: str, node_id: str, location: Any = None
|
||||
) -> None:
|
||||
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
|
||||
|
||||
def clear_doc(self, docname: str) -> None:
|
||||
for fullname, obj in list(self.objects.items()):
|
||||
if obj.docname == docname:
|
||||
del self.objects[fullname]
|
||||
|
||||
def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:
|
||||
# skip parens
|
||||
if name[-2:] == "()":
|
||||
name = name[:-2]
|
||||
if typ in ("meth", "sig", "prop"):
|
||||
try:
|
||||
ifacename, name = name.rsplit(".", 1)
|
||||
except ValueError:
|
||||
pass
|
||||
return self.objects.get(name)
|
||||
|
||||
def resolve_xref(
|
||||
self,
|
||||
env: "BuildEnvironment",
|
||||
fromdocname: str,
|
||||
builder: "Builder",
|
||||
typ: str,
|
||||
target: str,
|
||||
node: pending_xref,
|
||||
contnode: Element,
|
||||
) -> Optional[Element]:
|
||||
"""Resolve the pending_xref *node* with the given *typ* and *target*."""
|
||||
objdef = self.find_obj(typ, target)
|
||||
if objdef:
|
||||
return node_utils.make_refnode(
|
||||
builder, fromdocname, objdef.docname, objdef.node_id, contnode
|
||||
)
|
||||
|
||||
def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
|
||||
for refname, obj in self.objects.items():
|
||||
yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
|
||||
|
||||
def merge_domaindata(self, docnames, otherdata):
|
||||
for name, obj in otherdata['objects'].items():
|
||||
if obj.docname in docnames:
|
||||
self.data['objects'][name] = obj
|
||||
|
||||
def setup(app):
|
||||
app.add_domain(DBusDomain)
|
||||
app.add_config_value("dbus_index_common_prefix", [], "env")
|
||||
@@ -0,0 +1,373 @@
|
||||
# Based from "GDBus - GLib D-Bus Library":
|
||||
#
|
||||
# Copyright (C) 2008-2011 Red Hat, Inc.
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General
|
||||
# Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Author: David Zeuthen <[email protected]>
|
||||
|
||||
import xml.parsers.expat
|
||||
|
||||
|
||||
class Annotation:
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.annotations = []
|
||||
self.since = ""
|
||||
|
||||
|
||||
class Arg:
|
||||
def __init__(self, name, signature):
|
||||
self.name = name
|
||||
self.signature = signature
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
|
||||
|
||||
class Method:
|
||||
def __init__(self, name, h_type_implies_unix_fd=True):
|
||||
self.name = name
|
||||
self.h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||
self.in_args = []
|
||||
self.out_args = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
self.unix_fd = False
|
||||
|
||||
|
||||
class Signal:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.args = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
|
||||
|
||||
class Property:
|
||||
def __init__(self, name, signature, access):
|
||||
self.name = name
|
||||
self.signature = signature
|
||||
self.access = access
|
||||
self.annotations = []
|
||||
self.arg = Arg("value", self.signature)
|
||||
self.arg.annotations = self.annotations
|
||||
self.readable = False
|
||||
self.writable = False
|
||||
if self.access == "readwrite":
|
||||
self.readable = True
|
||||
self.writable = True
|
||||
elif self.access == "read":
|
||||
self.readable = True
|
||||
elif self.access == "write":
|
||||
self.writable = True
|
||||
else:
|
||||
raise ValueError('Invalid access type "{}"'.format(self.access))
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
self.emits_changed_signal = True
|
||||
|
||||
|
||||
class Interface:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.methods = []
|
||||
self.signals = []
|
||||
self.properties = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.doc_string_brief = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
|
||||
|
||||
class DBusXMLParser:
|
||||
STATE_TOP = "top"
|
||||
STATE_NODE = "node"
|
||||
STATE_INTERFACE = "interface"
|
||||
STATE_METHOD = "method"
|
||||
STATE_SIGNAL = "signal"
|
||||
STATE_PROPERTY = "property"
|
||||
STATE_ARG = "arg"
|
||||
STATE_ANNOTATION = "annotation"
|
||||
STATE_IGNORED = "ignored"
|
||||
|
||||
def __init__(self, xml_data, h_type_implies_unix_fd=True):
|
||||
self._parser = xml.parsers.expat.ParserCreate()
|
||||
self._parser.CommentHandler = self.handle_comment
|
||||
self._parser.CharacterDataHandler = self.handle_char_data
|
||||
self._parser.StartElementHandler = self.handle_start_element
|
||||
self._parser.EndElementHandler = self.handle_end_element
|
||||
|
||||
self.parsed_interfaces = []
|
||||
self._cur_object = None
|
||||
|
||||
self.state = DBusXMLParser.STATE_TOP
|
||||
self.state_stack = []
|
||||
self._cur_object = None
|
||||
self._cur_object_stack = []
|
||||
|
||||
self.doc_comment_last_symbol = ""
|
||||
|
||||
self._h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||
|
||||
self._parser.Parse(xml_data)
|
||||
|
||||
COMMENT_STATE_BEGIN = "begin"
|
||||
COMMENT_STATE_PARAMS = "params"
|
||||
COMMENT_STATE_BODY = "body"
|
||||
COMMENT_STATE_SKIP = "skip"
|
||||
|
||||
def handle_comment(self, data):
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BEGIN
|
||||
lines = data.split("\n")
|
||||
symbol = ""
|
||||
body = ""
|
||||
in_para = False
|
||||
params = {}
|
||||
for line in lines:
|
||||
orig_line = line
|
||||
line = line.lstrip()
|
||||
if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN:
|
||||
if len(line) > 0:
|
||||
colon_index = line.find(": ")
|
||||
if colon_index == -1:
|
||||
if line.endswith(":"):
|
||||
symbol = line[0 : len(line) - 1]
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||
else:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_SKIP
|
||||
else:
|
||||
symbol = line[0:colon_index]
|
||||
rest_of_line = line[colon_index + 2 :].strip()
|
||||
if len(rest_of_line) > 0:
|
||||
body += rest_of_line + "\n"
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||
elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS:
|
||||
if line.startswith("@"):
|
||||
colon_index = line.find(": ")
|
||||
if colon_index == -1:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
else:
|
||||
param = line[1:colon_index]
|
||||
docs = line[colon_index + 2 :]
|
||||
params[param] = docs
|
||||
else:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||
if len(line) > 0:
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
elif comment_state == DBusXMLParser.COMMENT_STATE_BODY:
|
||||
if len(line) > 0:
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
else:
|
||||
if in_para:
|
||||
body += "\n"
|
||||
in_para = False
|
||||
if in_para:
|
||||
body += "\n"
|
||||
|
||||
if symbol != "":
|
||||
self.doc_comment_last_symbol = symbol
|
||||
self.doc_comment_params = params
|
||||
self.doc_comment_body = body
|
||||
|
||||
def handle_char_data(self, data):
|
||||
# print 'char_data=%s'%data
|
||||
pass
|
||||
|
||||
def handle_start_element(self, name, attrs):
|
||||
old_state = self.state
|
||||
old_cur_object = self._cur_object
|
||||
if self.state == DBusXMLParser.STATE_IGNORED:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
elif self.state == DBusXMLParser.STATE_TOP:
|
||||
if name == DBusXMLParser.STATE_NODE:
|
||||
self.state = DBusXMLParser.STATE_NODE
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
elif self.state == DBusXMLParser.STATE_NODE:
|
||||
if name == DBusXMLParser.STATE_INTERFACE:
|
||||
self.state = DBusXMLParser.STATE_INTERFACE
|
||||
iface = Interface(attrs["name"])
|
||||
self._cur_object = iface
|
||||
self.parsed_interfaces.append(iface)
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||
self._cur_object.doc_string = self.doc_comment_body
|
||||
if "short_description" in self.doc_comment_params:
|
||||
short_description = self.doc_comment_params["short_description"]
|
||||
self._cur_object.doc_string_brief = short_description
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_INTERFACE:
|
||||
if name == DBusXMLParser.STATE_METHOD:
|
||||
self.state = DBusXMLParser.STATE_METHOD
|
||||
method = Method(
|
||||
attrs["name"], h_type_implies_unix_fd=self._h_type_implies_unix_fd
|
||||
)
|
||||
self._cur_object.methods.append(method)
|
||||
self._cur_object = method
|
||||
elif name == DBusXMLParser.STATE_SIGNAL:
|
||||
self.state = DBusXMLParser.STATE_SIGNAL
|
||||
signal = Signal(attrs["name"])
|
||||
self._cur_object.signals.append(signal)
|
||||
self._cur_object = signal
|
||||
elif name == DBusXMLParser.STATE_PROPERTY:
|
||||
self.state = DBusXMLParser.STATE_PROPERTY
|
||||
prop = Property(attrs["name"], attrs["type"], attrs["access"])
|
||||
self._cur_object.properties.append(prop)
|
||||
self._cur_object = prop
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||
self._cur_object.doc_string = self.doc_comment_body
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_METHOD:
|
||||
if name == DBusXMLParser.STATE_ARG:
|
||||
self.state = DBusXMLParser.STATE_ARG
|
||||
arg_name = None
|
||||
if "name" in attrs:
|
||||
arg_name = attrs["name"]
|
||||
arg = Arg(arg_name, attrs["type"])
|
||||
direction = attrs.get("direction", "in")
|
||||
if direction == "in":
|
||||
self._cur_object.in_args.append(arg)
|
||||
elif direction == "out":
|
||||
self._cur_object.out_args.append(arg)
|
||||
else:
|
||||
raise ValueError('Invalid direction "{}"'.format(direction))
|
||||
self._cur_object = arg
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||
doc_string = self.doc_comment_params[attrs["name"]]
|
||||
if doc_string is not None:
|
||||
self._cur_object.doc_string = doc_string
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params[
|
||||
"since"
|
||||
].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_SIGNAL:
|
||||
if name == DBusXMLParser.STATE_ARG:
|
||||
self.state = DBusXMLParser.STATE_ARG
|
||||
arg_name = None
|
||||
if "name" in attrs:
|
||||
arg_name = attrs["name"]
|
||||
arg = Arg(arg_name, attrs["type"])
|
||||
self._cur_object.args.append(arg)
|
||||
self._cur_object = arg
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||
doc_string = self.doc_comment_params[attrs["name"]]
|
||||
if doc_string is not None:
|
||||
self._cur_object.doc_string = doc_string
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params[
|
||||
"since"
|
||||
].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_PROPERTY:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_ARG:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_ANNOTATION:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
'Unhandled state "{}" while entering element with name "{}"'.format(
|
||||
self.state, name
|
||||
)
|
||||
)
|
||||
|
||||
self.state_stack.append(old_state)
|
||||
self._cur_object_stack.append(old_cur_object)
|
||||
|
||||
def handle_end_element(self, name):
|
||||
self.state = self.state_stack.pop()
|
||||
self._cur_object = self._cur_object_stack.pop()
|
||||
|
||||
|
||||
def parse_dbus_xml(xml_data):
|
||||
parser = DBusXMLParser(xml_data, True)
|
||||
return parser.parsed_interfaces
|
||||
@@ -0,0 +1,69 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# QEMU depfile generation extension
|
||||
#
|
||||
# Copyright (c) 2020 Red Hat, Inc.
|
||||
#
|
||||
# This work is licensed under the terms of the GNU GPLv2 or later.
|
||||
# See the COPYING file in the top-level directory.
|
||||
|
||||
"""depfile is a Sphinx extension that writes a dependency file for
|
||||
an external build system"""
|
||||
|
||||
import os
|
||||
import sphinx
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = '1.0'
|
||||
|
||||
def get_infiles(env):
|
||||
for x in env.found_docs:
|
||||
yield str(env.doc2path(x))
|
||||
yield from ((os.path.join(env.srcdir, dep)
|
||||
for dep in env.dependencies[x]))
|
||||
for mod in sys.modules.values():
|
||||
if hasattr(mod, '__file__'):
|
||||
if mod.__file__:
|
||||
yield mod.__file__
|
||||
# this is perhaps going to include unused files:
|
||||
for static_path in env.config.html_static_path + env.config.templates_path:
|
||||
for path in Path(static_path).rglob('*'):
|
||||
yield str(path)
|
||||
|
||||
# also include kdoc script
|
||||
yield str(env.config.kerneldoc_bin[1])
|
||||
|
||||
|
||||
def write_depfile(app, exception):
|
||||
if exception:
|
||||
return
|
||||
|
||||
env = app.env
|
||||
if not env.config.depfile:
|
||||
return
|
||||
|
||||
# Using a directory as the output file does not work great because
|
||||
# its timestamp does not necessarily change when the contents change.
|
||||
# So create a timestamp file.
|
||||
if env.config.depfile_stamp:
|
||||
with open(env.config.depfile_stamp, 'w') as f:
|
||||
pass
|
||||
|
||||
with open(env.config.depfile, 'w') as f:
|
||||
print((env.config.depfile_stamp or app.outdir) + ": \\", file=f)
|
||||
print(*get_infiles(env), file=f)
|
||||
for x in get_infiles(env):
|
||||
print(x + ":", file=f)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_config_value('depfile', None, 'env')
|
||||
app.add_config_value('depfile_stamp', None, 'env')
|
||||
app.connect('build-finished', write_depfile)
|
||||
|
||||
return dict(
|
||||
version = __version__,
|
||||
parallel_read_safe = True,
|
||||
parallel_write_safe = True
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
# D-Bus XML documentation extension, compatibility gunk for <sphinx4
|
||||
#
|
||||
# Copyright (C) 2021, Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Author: Marc-André Lureau <[email protected]>
|
||||
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||
|
||||
from docutils.parsers.rst import Directive
|
||||
from sphinx.application import Sphinx
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class FakeDBusDocDirective(Directive):
|
||||
has_content = True
|
||||
required_arguments = 1
|
||||
|
||||
def run(self):
|
||||
return []
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
"""Register a fake dbus-doc directive with Sphinx"""
|
||||
app.add_directive("dbus-doc", FakeDBusDocDirective)
|
||||
|
||||
return dict(
|
||||
parallel_read_safe = True,
|
||||
parallel_write_safe = True
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# QEMU hxtool .hx file parsing extension
|
||||
#
|
||||
# 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.
|
||||
"""hxtool is a Sphinx extension that implements the hxtool-doc directive"""
|
||||
|
||||
# The purpose of this extension is to read fragments of rST
|
||||
# from .hx files, and insert them all into the current document.
|
||||
# The rST fragments are delimited by SRST/ERST lines.
|
||||
# The conf.py file must set the hxtool_srctree config value to
|
||||
# the root of the QEMU source tree.
|
||||
# Each hxtool-doc:: directive takes one argument which is the
|
||||
# path of the .hx file to process, relative to the source tree.
|
||||
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.statemachine import ViewList
|
||||
from docutils.parsers.rst import directives, Directive
|
||||
from sphinx.errors import ExtensionError
|
||||
from sphinx.util.docutils import switch_source_input
|
||||
from sphinx.util.nodes import nested_parse_with_titles
|
||||
import sphinx
|
||||
|
||||
|
||||
__version__ = '1.0'
|
||||
|
||||
# We parse hx files with a state machine which may be in one of two
|
||||
# states: reading the C code fragment, or inside a rST fragment.
|
||||
class HxState(Enum):
|
||||
CTEXT = 1
|
||||
RST = 2
|
||||
|
||||
def serror(file, lnum, errtext):
|
||||
"""Raise an exception giving a user-friendly syntax error message"""
|
||||
raise ExtensionError('%s line %d: syntax error: %s' % (file, lnum, errtext))
|
||||
|
||||
def parse_directive(line):
|
||||
"""Return first word of line, if any"""
|
||||
return re.split(r'\W', line)[0]
|
||||
|
||||
def parse_defheading(file, lnum, line):
|
||||
"""Handle a DEFHEADING directive"""
|
||||
# The input should be "DEFHEADING(some string)", though note that
|
||||
# the 'some string' could be the empty string. If the string is
|
||||
# empty we ignore the directive -- these are used only to add
|
||||
# blank lines in the plain-text content of the --help output.
|
||||
#
|
||||
# Return the heading text. We strip out any trailing ':' for
|
||||
# consistency with other headings in the rST documentation.
|
||||
match = re.match(r'DEFHEADING\((.*?):?\)', line)
|
||||
if match is None:
|
||||
serror(file, lnum, "Invalid DEFHEADING line")
|
||||
return match.group(1)
|
||||
|
||||
def parse_archheading(file, lnum, line):
|
||||
"""Handle an ARCHHEADING directive"""
|
||||
# The input should be "ARCHHEADING(some string, other arg)",
|
||||
# though note that the 'some string' could be the empty string.
|
||||
# As with DEFHEADING, empty string ARCHHEADINGs will be ignored.
|
||||
#
|
||||
# Return the heading text. We strip out any trailing ':' for
|
||||
# consistency with other headings in the rST documentation.
|
||||
match = re.match(r'ARCHHEADING\((.*?):?,.*\)', line)
|
||||
if match is None:
|
||||
serror(file, lnum, "Invalid ARCHHEADING line")
|
||||
return match.group(1)
|
||||
|
||||
def parse_srst(file, lnum, line):
|
||||
"""Handle an SRST directive"""
|
||||
# The input should be either "SRST", or "SRST(label)".
|
||||
match = re.match(r'SRST(\((.*?)\))?', line)
|
||||
if match is None:
|
||||
serror(file, lnum, "Invalid SRST line")
|
||||
return match.group(2)
|
||||
|
||||
class HxtoolDocDirective(Directive):
|
||||
"""Extract rST fragments from the specified .hx file"""
|
||||
required_argument = 1
|
||||
optional_arguments = 1
|
||||
option_spec = {
|
||||
'hxfile': directives.unchanged_required
|
||||
}
|
||||
has_content = False
|
||||
|
||||
def run(self):
|
||||
env = self.state.document.settings.env
|
||||
hxfile = env.config.hxtool_srctree + '/' + self.arguments[0]
|
||||
|
||||
# Tell sphinx of the dependency
|
||||
env.note_dependency(os.path.abspath(hxfile))
|
||||
|
||||
state = HxState.CTEXT
|
||||
# We build up lines of rST in this ViewList, which we will
|
||||
# later put into a 'section' node.
|
||||
rstlist = ViewList()
|
||||
current_node = None
|
||||
node_list = []
|
||||
|
||||
with open(hxfile) as f:
|
||||
lines = (l.rstrip() for l in f)
|
||||
for lnum, line in enumerate(lines, 1):
|
||||
directive = parse_directive(line)
|
||||
|
||||
if directive == 'HXCOMM':
|
||||
pass
|
||||
elif directive == 'SRST':
|
||||
if state == HxState.RST:
|
||||
serror(hxfile, lnum, 'expected ERST, found SRST')
|
||||
else:
|
||||
state = HxState.RST
|
||||
label = parse_srst(hxfile, lnum, line)
|
||||
if label:
|
||||
rstlist.append("", hxfile, lnum - 1)
|
||||
# Build label as _DOCNAME-HXNAME-LABEL
|
||||
hx = os.path.splitext(os.path.basename(hxfile))[0]
|
||||
refline = ".. _" + env.docname + "-" + hx + \
|
||||
"-" + label + ":"
|
||||
rstlist.append(refline, hxfile, lnum - 1)
|
||||
elif directive == 'ERST':
|
||||
if state == HxState.CTEXT:
|
||||
serror(hxfile, lnum, 'expected SRST, found ERST')
|
||||
else:
|
||||
state = HxState.CTEXT
|
||||
elif directive == 'DEFHEADING' or directive == 'ARCHHEADING':
|
||||
if directive == 'DEFHEADING':
|
||||
heading = parse_defheading(hxfile, lnum, line)
|
||||
else:
|
||||
heading = parse_archheading(hxfile, lnum, line)
|
||||
if heading == "":
|
||||
continue
|
||||
# Put the accumulated rST into the previous node,
|
||||
# and then start a fresh section with this heading.
|
||||
if len(rstlist) > 0:
|
||||
if current_node is None:
|
||||
# We had some rST fragments before the first
|
||||
# DEFHEADING. We don't have a section to put
|
||||
# these in, so rather than magicing up a section,
|
||||
# make it a syntax error.
|
||||
serror(hxfile, lnum,
|
||||
'first DEFHEADING must precede all rST text')
|
||||
self.do_parse(rstlist, current_node)
|
||||
rstlist = ViewList()
|
||||
if current_node is not None:
|
||||
node_list.append(current_node)
|
||||
section_id = 'hxtool-%d' % env.new_serialno('hxtool')
|
||||
current_node = nodes.section(ids=[section_id])
|
||||
current_node += nodes.title(heading, heading)
|
||||
else:
|
||||
# Not a directive: put in output if we are in rST fragment
|
||||
if state == HxState.RST:
|
||||
# Sphinx counts its lines from 0
|
||||
rstlist.append(line, hxfile, lnum - 1)
|
||||
|
||||
if current_node is None:
|
||||
# We don't have multiple sections, so just parse the rst
|
||||
# fragments into a dummy node so we can return the children.
|
||||
current_node = nodes.section()
|
||||
self.do_parse(rstlist, current_node)
|
||||
return current_node.children
|
||||
else:
|
||||
# Put the remaining accumulated rST into the last section, and
|
||||
# return all the sections.
|
||||
if len(rstlist) > 0:
|
||||
self.do_parse(rstlist, current_node)
|
||||
node_list.append(current_node)
|
||||
return node_list
|
||||
|
||||
# This is from kerneldoc.py -- it works around an API change in
|
||||
# Sphinx between 1.6 and 1.7. Unlike kerneldoc.py, we use
|
||||
# sphinx.util.nodes.nested_parse_with_titles() rather than the
|
||||
# plain self.state.nested_parse(), and so we can drop the saving
|
||||
# of title_styles and section_level that kerneldoc.py does,
|
||||
# because nested_parse_with_titles() does that for us.
|
||||
def do_parse(self, result, node):
|
||||
with switch_source_input(self.state, result):
|
||||
nested_parse_with_titles(self.state, result, node)
|
||||
|
||||
|
||||
def setup(app):
|
||||
""" Register hxtool-doc directive with Sphinx"""
|
||||
app.add_config_value('hxtool_srctree', None, 'env')
|
||||
app.add_directive('hxtool-doc', HxtoolDocDirective)
|
||||
|
||||
return dict(
|
||||
version = __version__,
|
||||
parallel_read_safe = True,
|
||||
parallel_write_safe = True
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
# coding=utf-8
|
||||
#
|
||||
# Copyright © 2016 Intel Corporation
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the "Software"),
|
||||
# to deal in the Software without restriction, including without limitation
|
||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
# and/or sell copies of the Software, and to permit persons to whom the
|
||||
# Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice (including the next
|
||||
# paragraph) shall be included in all copies or substantial portions of the
|
||||
# Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
#
|
||||
# Authors:
|
||||
# Jani Nikula <[email protected]>
|
||||
#
|
||||
# Please make sure this works on both python2 and python3.
|
||||
#
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import glob
|
||||
|
||||
from docutils import nodes, statemachine
|
||||
from docutils.statemachine import ViewList
|
||||
from docutils.parsers.rst import directives, Directive
|
||||
|
||||
import sphinx
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.docutils import switch_source_input
|
||||
|
||||
|
||||
__version__ = '1.0'
|
||||
logger = logging.getLogger('kerneldoc')
|
||||
|
||||
|
||||
class KernelDocDirective(Directive):
|
||||
"""Extract kernel-doc comments from the specified file"""
|
||||
required_argument = 1
|
||||
optional_arguments = 4
|
||||
option_spec = {
|
||||
'doc': directives.unchanged_required,
|
||||
'functions': directives.unchanged,
|
||||
'export': directives.unchanged,
|
||||
'internal': directives.unchanged,
|
||||
}
|
||||
has_content = False
|
||||
|
||||
def run(self):
|
||||
env = self.state.document.settings.env
|
||||
cmd = env.config.kerneldoc_bin + ['-rst', '-enable-lineno']
|
||||
|
||||
# Pass through the warnings-as-errors flag
|
||||
if env.config.kerneldoc_werror:
|
||||
cmd += ['-Werror']
|
||||
|
||||
filename = env.config.kerneldoc_srctree + '/' + self.arguments[0]
|
||||
export_file_patterns = []
|
||||
|
||||
# Tell sphinx of the dependency
|
||||
env.note_dependency(os.path.abspath(filename))
|
||||
|
||||
tab_width = self.options.get('tab-width', self.state.document.settings.tab_width)
|
||||
|
||||
# FIXME: make this nicer and more robust against errors
|
||||
if 'export' in self.options:
|
||||
cmd += ['-export']
|
||||
export_file_patterns = str(self.options.get('export')).split()
|
||||
elif 'internal' in self.options:
|
||||
cmd += ['-internal']
|
||||
export_file_patterns = str(self.options.get('internal')).split()
|
||||
elif 'doc' in self.options:
|
||||
cmd += ['-function', str(self.options.get('doc'))]
|
||||
elif 'functions' in self.options:
|
||||
functions = self.options.get('functions').split()
|
||||
if functions:
|
||||
for f in functions:
|
||||
cmd += ['-function', f]
|
||||
else:
|
||||
cmd += ['-no-doc-sections']
|
||||
|
||||
for pattern in export_file_patterns:
|
||||
for f in glob.glob(env.config.kerneldoc_srctree + '/' + pattern):
|
||||
env.note_dependency(os.path.abspath(f))
|
||||
cmd += ['-export-file', f]
|
||||
|
||||
cmd += [filename]
|
||||
|
||||
try:
|
||||
logger.verbose('calling kernel-doc \'%s\'' % (" ".join(cmd)))
|
||||
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = p.communicate()
|
||||
|
||||
out, err = codecs.decode(out, 'utf-8'), codecs.decode(err, 'utf-8')
|
||||
|
||||
if p.returncode != 0:
|
||||
sys.stderr.write(err)
|
||||
|
||||
logger.warning(
|
||||
'kernel-doc \'%s\' failed with return code %d' %
|
||||
(" ".join(cmd), p.returncode)
|
||||
)
|
||||
return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))]
|
||||
elif env.config.kerneldoc_verbosity > 0:
|
||||
sys.stderr.write(err)
|
||||
|
||||
lines = statemachine.string2lines(out, tab_width, convert_whitespace=True)
|
||||
result = ViewList()
|
||||
|
||||
lineoffset = 0;
|
||||
line_regex = re.compile(r"^(?:\.\.|#define) LINENO ([0-9]+)$")
|
||||
for line in lines:
|
||||
match = line_regex.search(line)
|
||||
if match:
|
||||
# sphinx counts lines from 0
|
||||
lineoffset = int(match.group(1)) - 1
|
||||
# we must eat our comments since the upset the markup
|
||||
else:
|
||||
result.append(line, filename, lineoffset)
|
||||
lineoffset += 1
|
||||
|
||||
node = nodes.section()
|
||||
self.do_parse(result, node)
|
||||
|
||||
return node.children
|
||||
|
||||
except Exception as e: # pylint: disable=W0703
|
||||
logger.warning('kernel-doc \'%s\' processing failed with: %s' %
|
||||
(" ".join(cmd), str(e)))
|
||||
return [nodes.error(None, nodes.paragraph(text = "kernel-doc missing"))]
|
||||
|
||||
def do_parse(self, result, node):
|
||||
with switch_source_input(self.state, result):
|
||||
self.state.nested_parse(result, 0, node, match_titles=1)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_config_value('kerneldoc_bin', None, 'env')
|
||||
app.add_config_value('kerneldoc_srctree', None, 'env')
|
||||
app.add_config_value('kerneldoc_verbosity', 1, 'env')
|
||||
app.add_config_value('kerneldoc_werror', 0, 'env')
|
||||
|
||||
app.add_directive('kernel-doc', KernelDocDirective)
|
||||
|
||||
return dict(
|
||||
version = __version__,
|
||||
parallel_read_safe = True,
|
||||
parallel_write_safe = True
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,701 @@
|
||||
# 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
|
||||
import textwrap
|
||||
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 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,
|
||||
dedent: bool = False,
|
||||
) -> None:
|
||||
lines = content.splitlines(True)
|
||||
|
||||
if dedent:
|
||||
lines = textwrap.dedent(content).splitlines(True)
|
||||
else:
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def reformat_arobase(text: str) -> str:
|
||||
""" reformats @var to ``var`` """
|
||||
return re.sub(r"@([\w-]+)", r"``\1``", text)
|
||||
|
||||
# Transmogrification helpers
|
||||
|
||||
def visit_plaintext(self, section: QAPIDoc.Section) -> None:
|
||||
# Squelch empty paragraphs.
|
||||
if not section.text:
|
||||
return
|
||||
|
||||
# Intro sections, which are indented in QAPI source, need to
|
||||
# be dedented to avoid accidental block quotes in ReST syntax.
|
||||
dedent = bool(section.kind == QAPIDoc.Kind.INTRO)
|
||||
self.ensure_blank_line()
|
||||
self.add_lines(section.text, section.info, dedent)
|
||||
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
|
||||
# return statements will not be present (and won't be
|
||||
# autogenerated) for any command that doesn't return
|
||||
# *something*, so rtype will always be defined here.
|
||||
assert rtype
|
||||
|
||||
typ = self.format_type(rtype)
|
||||
assert typ
|
||||
|
||||
if section.text:
|
||||
self.add_field("return", typ, section.text, section.info)
|
||||
else:
|
||||
self.add_lines(f":return-nodesc: {typ}", section.info)
|
||||
|
||||
def visit_errors(self, section: QAPIDoc.Section) -> None:
|
||||
# If the section text does not start with a space, it means text
|
||||
# began on the same line as the "Error:" string and we should
|
||||
# not insert a newline in this case.
|
||||
if section.text[0].isspace():
|
||||
text = f":error:\n{section.text}"
|
||||
else:
|
||||
text = f":error: {section.text}"
|
||||
self.add_lines(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:
|
||||
# Generate a placeholder right after the member section(s) which
|
||||
# may be used to generate documentation for "The members of..."
|
||||
# pointers in the rendered document.
|
||||
#
|
||||
# This is a temporary hack until the inliner is merged. Note
|
||||
# that although we modify the caller's section list, the
|
||||
# Sphinx document generator has its own copy of the parsed
|
||||
# schema in memory, so this action does not interfere with
|
||||
# other users of the QAPISchema or QAPIDoc objects outside of
|
||||
# the document generator. Fishy, but not harmful.
|
||||
if ent.doc:
|
||||
ent.doc.append_member_stub(
|
||||
QAPIDoc.ArgSection(
|
||||
ent.doc.info, QAPIDoc.Kind.MEMBER, "q_dummy"
|
||||
)
|
||||
)
|
||||
|
||||
sections = ent.doc.all_sections if ent.doc else []
|
||||
|
||||
# Add sections in source order:
|
||||
for section in sections:
|
||||
section.text = self.reformat_arobase(section.text)
|
||||
|
||||
if section.kind.name in ("PLAIN", "INTRO"):
|
||||
self.visit_plaintext(section)
|
||||
elif section.kind == QAPIDoc.Kind.MEMBER:
|
||||
assert isinstance(section, QAPIDoc.ArgSection)
|
||||
if section.name == "q_dummy":
|
||||
# Generate "The members of ..." entries if necessary
|
||||
self._insert_member_pointer(ent)
|
||||
else:
|
||||
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
|
||||
|
||||
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:
|
||||
assert len(doc.all_sections) == 1, doc.all_sections
|
||||
body = doc.all_sections[0]
|
||||
self.add_lines(self.reformat_arobase(body.text), doc.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,
|
||||
}
|
||||
has_content = False
|
||||
|
||||
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 run(self) -> Sequence[nodes.Node]:
|
||||
env = self.state.document.settings.env
|
||||
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
|
||||
qapidir = os.path.dirname(qapifile)
|
||||
|
||||
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
|
||||
|
||||
contentnode = self.transmogrify(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,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# QEMU Monitor Protocol Lexer Extension
|
||||
#
|
||||
# Copyright (C) 2019, Red Hat Inc.
|
||||
#
|
||||
# Authors:
|
||||
# Eduardo Habkost <[email protected]>
|
||||
# John Snow <[email protected]>
|
||||
#
|
||||
# This work is licensed under the terms of the GNU GPLv2 or later.
|
||||
# See the COPYING file in the top-level directory.
|
||||
"""qmp_lexer is a Sphinx extension that provides a QMP lexer for code blocks."""
|
||||
|
||||
from pygments.lexer import RegexLexer, DelegatingLexer
|
||||
from pygments.lexers.data import JsonLexer
|
||||
from pygments import token
|
||||
from sphinx import errors
|
||||
|
||||
class QMPExampleMarkersLexer(RegexLexer):
|
||||
"""
|
||||
QMPExampleMarkersLexer lexes QMP example annotations.
|
||||
This lexer adds support for directionality flow and elision indicators.
|
||||
"""
|
||||
tokens = {
|
||||
'root': [
|
||||
(r'-> ', token.Generic.Prompt),
|
||||
(r'<- ', token.Generic.Prompt),
|
||||
(r'\.{3}( .* \.{3})?', token.Comment.Multiline),
|
||||
]
|
||||
}
|
||||
|
||||
class QMPExampleLexer(DelegatingLexer):
|
||||
"""QMPExampleLexer lexes annotated QMP examples."""
|
||||
def __init__(self, **options):
|
||||
super(QMPExampleLexer, self).__init__(JsonLexer, QMPExampleMarkersLexer,
|
||||
token.Error, **options)
|
||||
|
||||
def setup(sphinx):
|
||||
"""For use by the Sphinx extensions API."""
|
||||
try:
|
||||
sphinx.require_sphinx('2.1')
|
||||
sphinx.add_lexer('QMP', QMPExampleLexer)
|
||||
except errors.VersionRequirementError:
|
||||
sphinx.add_lexer('QMP', QMPExampleLexer())
|
||||
|
||||
return dict(
|
||||
parallel_read_safe = True,
|
||||
parallel_write_safe = True
|
||||
)
|
||||
Reference in new issue
Block a user