qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH v2 21/37] docs/sphinx: add sphinx modules to include D-Bus docume


From: marcandre . lureau
Subject: [PATCH v2 21/37] docs/sphinx: add sphinx modules to include D-Bus documentation
Date: Sun, 10 Oct 2021 01:08:22 +0400

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




reply via email to

[Prev in Thread] Current Thread [Next in Thread]