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