All of lore.kernel.org
 help / color / mirror / Atom feed
From: marcandre.lureau@redhat.com
To: qemu-devel@nongnu.org
Cc: "Marc-André Lureau" <marcandre.lureau@redhat.com>, kraxel@redhat.com
Subject: [PATCH v2 21/37] docs/sphinx: add sphinx modules to include D-Bus documentation
Date: Sun, 10 Oct 2021 01:08:22 +0400	[thread overview]
Message-ID: <20211009210838.2219430-22-marcandre.lureau@redhat.com> (raw)
In-Reply-To: <20211009210838.2219430-1-marcandre.lureau@redhat.com>

From: Marc-André Lureau <marcandre.lureau@redhat.com>

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), I
propose to 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>
---
 docs/conf.py               |   8 +
 docs/sphinx/dbusdoc.py     | 166 +++++++++++++++
 docs/sphinx/dbusdomain.py  | 406 +++++++++++++++++++++++++++++++++++++
 docs/sphinx/dbusparser.py  | 373 ++++++++++++++++++++++++++++++++++
 docs/sphinx/fakedbusdoc.py |  25 +++
 5 files changed, 978 insertions(+)
 create mode 100644 docs/sphinx/dbusdoc.py
 create mode 100644 docs/sphinx/dbusdomain.py
 create mode 100644 docs/sphinx/dbusparser.py
 create mode 100644 docs/sphinx/fakedbusdoc.py

diff --git a/docs/conf.py b/docs/conf.py
index ff6e92c6e2..71e2369026 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -73,6 +73,12 @@
 # ones.
 extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
 
+if sphinx.version_info[:3] > (4, 0, 0):
+    tags.add('sphinx4')
+    extensions += ['dbusdoc']
+else:
+    extensions += ['fakedbusdoc']
+
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
 
@@ -301,3 +307,5 @@
 kerneldoc_srctree = os.path.join(qemu_docdir, '..')
 hxtool_srctree = os.path.join(qemu_docdir, '..')
 qapidoc_srctree = os.path.join(qemu_docdir, '..')
+dbusdoc_srctree = os.path.join(qemu_docdir, '..')
+dbus_index_common_prefix = ["org.qemu."]
diff --git a/docs/sphinx/dbusdoc.py b/docs/sphinx/dbusdoc.py
new file mode 100644
index 0000000000..be284ed08f
--- /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 0000000000..2ea95af623
--- /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 0000000000..024553eae7
--- /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 0000000000..a680b25754
--- /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)
-- 
2.33.0.721.g106298f7f9



  parent reply	other threads:[~2021-10-09 21:22 UTC|newest]

Thread overview: 54+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-10-09 21:08 [PATCH v2 00/37] Add D-Bus display backend marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 01/37] build-sys: move Spice configure handling to meson marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 02/37] ui/vdagent: add CHECK_SPICE_PROTOCOL_VERSION marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 03/37] ui/vdagent: replace #if 0 with protocol version check marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 04/37] ui: generalize clipboard notifier marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 05/37] ui/vdagent: add serial capability support marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 06/37] ui/clipboard: add qemu_clipboard_check_serial() marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 07/37] ui/clipboard: add a clipboard reset serial event marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 08/37] hw/display: report an error if virgl initialization failed marcandre.lureau
2021-12-17 12:40   ` Philippe Mathieu-Daudé
2021-10-09 21:08 ` [PATCH v2 09/37] virtio-gpu: use VIRTIO_GPU_RESOURCE_FLAG_Y_0_TOP marcandre.lureau
2021-12-17 12:51   ` Philippe Mathieu-Daudé
2021-10-09 21:08 ` [PATCH v2 10/37] ui: do not delay further remote resize marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 11/37] ui: factor out qemu_console_set_display_gl_ctx() marcandre.lureau
2021-12-17 13:36   ` Philippe Mathieu-Daudé
2021-10-09 21:08 ` [PATCH v2 12/37] ui: associate GL context outside of display listener registration marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 13/37] ui: make gl_block use a counter marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 14/37] ui: add a gl-unblock warning timer marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 15/37] ui: simplify gl unblock & flush marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 16/37] ui: dispatch GL events to all listeners marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 17/37] ui: split the GL context in a different object marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 18/37] ui: move qemu_spice_fill_device_address to ui/util.c marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 19/37] console: save current scanout details marcandre.lureau
2022-01-11  3:29   ` Akihiko Odaki
2022-01-11  8:23     ` Marc-André Lureau
2022-01-11 12:45       ` Akihiko Odaki
2021-10-09 21:08 ` [PATCH v2 20/37] scripts: teach modinfo to skip non-C sources marcandre.lureau
2021-10-09 21:08 ` marcandre.lureau [this message]
2021-10-09 21:08 ` [PATCH v2 22/37] backends: move dbus-vmstate1.xml to backends/ marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 23/37] docs: move D-Bus VMState documentation to source XML marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 24/37] docs: add dbus-display documentation marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 25/37] build-sys: set glib dependency version marcandre.lureau
2021-12-17 13:27   ` Philippe Mathieu-Daudé
2021-12-17 13:40     ` Marc-André Lureau
2021-12-17 14:36       ` Philippe Mathieu-Daudé
2021-10-09 21:08 ` [PATCH v2 26/37] ui: add a D-Bus display backend marcandre.lureau
2021-10-13  8:59   ` Marc-André Lureau
2021-10-09 21:08 ` [PATCH v2 27/37] ui/dbus: add p2p=on/off option marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 28/37] tests/qtests: add qtest_qmp_add_client() marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 29/37] tests: start dbus-display-test marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 30/37] audio: add "dbus" audio backend marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 31/37] ui/dbus: add clipboard interface marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 32/37] chardev: teach socket to accept no addresses marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 33/37] chardev: make socket derivable marcandre.lureau
2021-12-17 13:32   ` Philippe Mathieu-Daudé
2021-10-09 21:08 ` [PATCH v2 34/37] option: add g_auto for QemuOpts marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 35/37] ui/dbus: add chardev backend & interface marcandre.lureau
2021-10-09 21:08 ` [PATCH v2 36/37] ui/dbus: register D-Bus VC handler marcandre.lureau
2021-12-17 13:35   ` Philippe Mathieu-Daudé
2021-12-17 14:21     ` Marc-André Lureau
2021-10-09 21:08 ` [PATCH v2 37/37] MAINTAINERS: update D-Bus section marcandre.lureau
2021-10-13  5:22 ` [PATCH v2 00/37] Add D-Bus display backend Gerd Hoffmann
2021-12-16 20:53   ` Marc-André Lureau
2021-12-17  7:05     ` Gerd Hoffmann

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211009210838.2219430-22-marcandre.lureau@redhat.com \
    --to=marcandre.lureau@redhat.com \
    --cc=kraxel@redhat.com \
    --cc=qemu-devel@nongnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.