aboutsummaryrefslogtreecommitdiff
path: root/docs/sphinx
diff options
context:
space:
mode:
authorMarc-André Lureau <marcandre.lureau@redhat.com>2021-10-06 01:00:35 +0400
committerMarc-André Lureau <marcandre.lureau@redhat.com>2021-12-21 10:50:21 +0400
commit2668dc7b5d9f56d8c3e6d2876c526fddc7068eca (patch)
treeae68445b8b96de1378e3e8899808144b54d659a7 /docs/sphinx
parent20f19713ef5a33bd9074bb87aea004c08a27be7b (diff)
downloadqemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.zip
qemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.tar.gz
qemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.tar.bz2
docs/sphinx: add sphinx modules to include D-Bus documentation
Add a new dbus-doc directive to import D-Bus interfaces documentation from the introspection XML. The comments annotations follow the gtkdoc/kerneldoc style, and should be formatted with reST. Note: I realize after the fact that I was implementing those modules with sphinx 4, and that we have much lower requirements. Instead of lowering the features and code (removing type annotations etc), let's have a warning in the documentation when the D-Bus modules can't be used, and point to the source XML file in that case. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Acked-by: Gerd Hoffmann <kraxel@redhat.com>
Diffstat (limited to 'docs/sphinx')
-rw-r--r--docs/sphinx/dbusdoc.py166
-rw-r--r--docs/sphinx/dbusdomain.py406
-rw-r--r--docs/sphinx/dbusparser.py373
-rw-r--r--docs/sphinx/fakedbusdoc.py25
4 files changed, 970 insertions, 0 deletions
diff --git a/docs/sphinx/dbusdoc.py b/docs/sphinx/dbusdoc.py
new file mode 100644
index 0000000..be284ed
--- /dev/null
+++ b/docs/sphinx/dbusdoc.py
@@ -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 <marcandre.lureau@redhat.com>
+"""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)
diff --git a/docs/sphinx/dbusdomain.py b/docs/sphinx/dbusdomain.py
new file mode 100644
index 0000000..2ea95af
--- /dev/null
+++ b/docs/sphinx/dbusdomain.py
@@ -0,0 +1,406 @@
+# D-Bus sphinx domain extension
+#
+# Copyright (C) 2021, Red Hat Inc.
+#
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
+
+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 setup(app):
+ app.add_domain(DBusDomain)
+ app.add_config_value("dbus_index_common_prefix", [], "env")
diff --git a/docs/sphinx/dbusparser.py b/docs/sphinx/dbusparser.py
new file mode 100644
index 0000000..024553e
--- /dev/null
+++ b/docs/sphinx/dbusparser.py
@@ -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 <davidz@redhat.com>
+
+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
diff --git a/docs/sphinx/fakedbusdoc.py b/docs/sphinx/fakedbusdoc.py
new file mode 100644
index 0000000..a680b25
--- /dev/null
+++ b/docs/sphinx/fakedbusdoc.py
@@ -0,0 +1,25 @@
+# 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 <marcandre.lureau@redhat.com>
+"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
+
+from sphinx.application import Sphinx
+from sphinx.util.docutils import SphinxDirective
+from typing import Any, Dict
+
+
+class FakeDBusDocDirective(SphinxDirective):
+ 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)