xref: /qemu/docs/sphinx/dbusdoc.py (revision ca61e750)
1# D-Bus XML documentation extension
2#
3# Copyright (C) 2021, Red Hat Inc.
4#
5# SPDX-License-Identifier: LGPL-2.1-or-later
6#
7# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
8"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
9
10import os
11import re
12from typing import (
13    TYPE_CHECKING,
14    Any,
15    Callable,
16    Dict,
17    Iterator,
18    List,
19    Optional,
20    Sequence,
21    Set,
22    Tuple,
23    Type,
24    TypeVar,
25    Union,
26)
27
28import sphinx
29from docutils import nodes
30from docutils.nodes import Element, Node
31from docutils.parsers.rst import Directive, directives
32from docutils.parsers.rst.states import RSTState
33from docutils.statemachine import StringList, ViewList
34from sphinx.application import Sphinx
35from sphinx.errors import ExtensionError
36from sphinx.util import logging
37from sphinx.util.docstrings import prepare_docstring
38from sphinx.util.docutils import SphinxDirective, switch_source_input
39from sphinx.util.nodes import nested_parse_with_titles
40
41import dbusdomain
42from dbusparser import parse_dbus_xml
43
44logger = logging.getLogger(__name__)
45
46__version__ = "1.0"
47
48
49class DBusDoc:
50    def __init__(self, sphinx_directive, dbusfile):
51        self._cur_doc = None
52        self._sphinx_directive = sphinx_directive
53        self._dbusfile = dbusfile
54        self._top_node = nodes.section()
55        self.result = StringList()
56        self.indent = ""
57
58    def add_line(self, line: str, *lineno: int) -> None:
59        """Append one line of generated reST to the output."""
60        if line.strip():  # not a blank line
61            self.result.append(self.indent + line, self._dbusfile, *lineno)
62        else:
63            self.result.append("", self._dbusfile, *lineno)
64
65    def add_method(self, method):
66        self.add_line(f".. dbus:method:: {method.name}")
67        self.add_line("")
68        self.indent += "   "
69        for arg in method.in_args:
70            self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
71        for arg in method.out_args:
72            self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}")
73        self.add_line("")
74        for line in prepare_docstring("\n" + method.doc_string):
75            self.add_line(line)
76        self.indent = self.indent[:-3]
77
78    def add_signal(self, signal):
79        self.add_line(f".. dbus:signal:: {signal.name}")
80        self.add_line("")
81        self.indent += "   "
82        for arg in signal.args:
83            self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
84        self.add_line("")
85        for line in prepare_docstring("\n" + signal.doc_string):
86            self.add_line(line)
87        self.indent = self.indent[:-3]
88
89    def add_property(self, prop):
90        self.add_line(f".. dbus:property:: {prop.name}")
91        self.indent += "   "
92        self.add_line(f":type: {prop.signature}")
93        access = {"read": "readonly", "write": "writeonly", "readwrite": "readwrite"}[
94            prop.access
95        ]
96        self.add_line(f":{access}:")
97        if prop.emits_changed_signal:
98            self.add_line(f":emits-changed: yes")
99        self.add_line("")
100        for line in prepare_docstring("\n" + prop.doc_string):
101            self.add_line(line)
102        self.indent = self.indent[:-3]
103
104    def add_interface(self, iface):
105        self.add_line(f".. dbus:interface:: {iface.name}")
106        self.add_line("")
107        self.indent += "   "
108        for line in prepare_docstring("\n" + iface.doc_string):
109            self.add_line(line)
110        for method in iface.methods:
111            self.add_method(method)
112        for sig in iface.signals:
113            self.add_signal(sig)
114        for prop in iface.properties:
115            self.add_property(prop)
116        self.indent = self.indent[:-3]
117
118
119def parse_generated_content(state: RSTState, content: StringList) -> List[Node]:
120    """Parse a generated content by Documenter."""
121    with switch_source_input(state, content):
122        node = nodes.paragraph()
123        node.document = state.document
124        state.nested_parse(content, 0, node)
125
126        return node.children
127
128
129class DBusDocDirective(SphinxDirective):
130    """Extract documentation from the specified D-Bus XML file"""
131
132    has_content = True
133    required_arguments = 1
134    optional_arguments = 0
135    final_argument_whitespace = True
136
137    def run(self):
138        reporter = self.state.document.reporter
139
140        try:
141            source, lineno = reporter.get_source_and_line(self.lineno)  # type: ignore
142        except AttributeError:
143            source, lineno = (None, None)
144
145        logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text)
146
147        env = self.state.document.settings.env
148        dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0]
149        with open(dbusfile, "rb") as f:
150            xml_data = f.read()
151        xml = parse_dbus_xml(xml_data)
152        doc = DBusDoc(self, dbusfile)
153        for iface in xml:
154            doc.add_interface(iface)
155
156        result = parse_generated_content(self.state, doc.result)
157        return result
158
159
160def setup(app: Sphinx) -> Dict[str, Any]:
161    """Register dbus-doc directive with Sphinx"""
162    app.add_config_value("dbusdoc_srctree", None, "env")
163    app.add_directive("dbus-doc", DBusDocDirective)
164    dbusdomain.setup(app)
165
166    return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)
167