blob: be284ed08fd73b9ede9be6c82ff46d3eb325525f [file] [log] [blame]
# 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)