aboutsummaryrefslogtreecommitdiff
path: root/docs/sphinx/dbusdoc.py
blob: be284ed08fd73b9ede9be6c82ff46d3eb325525f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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)