xref: /qemu/docs/sphinx/dbusdomain.py (revision b83a80e8)
1# D-Bus sphinx domain 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
9from typing import (
10    Any,
11    Dict,
12    Iterable,
13    Iterator,
14    List,
15    NamedTuple,
16    Optional,
17    Tuple,
18    cast,
19)
20
21from docutils import nodes
22from docutils.nodes import Element, Node
23from docutils.parsers.rst import directives
24from sphinx import addnodes
25from sphinx.addnodes import desc_signature, pending_xref
26from sphinx.directives import ObjectDescription
27from sphinx.domains import Domain, Index, IndexEntry, ObjType
28from sphinx.locale import _
29from sphinx.roles import XRefRole
30from sphinx.util import nodes as node_utils
31from sphinx.util.docfields import Field, TypedField
32from sphinx.util.typing import OptionSpec
33
34
35class DBusDescription(ObjectDescription[str]):
36    """Base class for DBus objects"""
37
38    option_spec: OptionSpec = ObjectDescription.option_spec.copy()
39    option_spec.update(
40        {
41            "deprecated": directives.flag,
42        }
43    )
44
45    def get_index_text(self, modname: str, name: str) -> str:
46        """Return the text for the index entry of the object."""
47        raise NotImplementedError("must be implemented in subclasses")
48
49    def add_target_and_index(
50        self, name: str, sig: str, signode: desc_signature
51    ) -> None:
52        ifacename = self.env.ref_context.get("dbus:interface")
53        node_id = name
54        if ifacename:
55            node_id = f"{ifacename}.{node_id}"
56
57        signode["names"].append(name)
58        signode["ids"].append(node_id)
59
60        if "noindexentry" not in self.options:
61            indextext = self.get_index_text(ifacename, name)
62            if indextext:
63                self.indexnode["entries"].append(
64                    ("single", indextext, node_id, "", None)
65                )
66
67        domain = cast(DBusDomain, self.env.get_domain("dbus"))
68        domain.note_object(name, self.objtype, node_id, location=signode)
69
70
71class DBusInterface(DBusDescription):
72    """
73    Implementation of ``dbus:interface``.
74    """
75
76    def get_index_text(self, ifacename: str, name: str) -> str:
77        return ifacename
78
79    def before_content(self) -> None:
80        self.env.ref_context["dbus:interface"] = self.arguments[0]
81
82    def after_content(self) -> None:
83        self.env.ref_context.pop("dbus:interface")
84
85    def handle_signature(self, sig: str, signode: desc_signature) -> str:
86        signode += addnodes.desc_annotation("interface ", "interface ")
87        signode += addnodes.desc_name(sig, sig)
88        return sig
89
90    def run(self) -> List[Node]:
91        _, node = super().run()
92        name = self.arguments[0]
93        section = nodes.section(ids=[name + "-section"])
94        section += nodes.title(name, "%s interface" % name)
95        section += node
96        return [self.indexnode, section]
97
98
99class DBusMember(DBusDescription):
100
101    signal = False
102
103
104class DBusMethod(DBusMember):
105    """
106    Implementation of ``dbus:method``.
107    """
108
109    option_spec: OptionSpec = DBusMember.option_spec.copy()
110    option_spec.update(
111        {
112            "noreply": directives.flag,
113        }
114    )
115
116    doc_field_types: List[Field] = [
117        TypedField(
118            "arg",
119            label=_("Arguments"),
120            names=("arg",),
121            rolename="arg",
122            typerolename=None,
123            typenames=("argtype", "type"),
124        ),
125        TypedField(
126            "ret",
127            label=_("Returns"),
128            names=("ret",),
129            rolename="ret",
130            typerolename=None,
131            typenames=("rettype", "type"),
132        ),
133    ]
134
135    def get_index_text(self, ifacename: str, name: str) -> str:
136        return _("%s() (%s method)") % (name, ifacename)
137
138    def handle_signature(self, sig: str, signode: desc_signature) -> str:
139        params = addnodes.desc_parameterlist()
140        returns = addnodes.desc_parameterlist()
141
142        contentnode = addnodes.desc_content()
143        self.state.nested_parse(self.content, self.content_offset, contentnode)
144        for child in contentnode:
145            if isinstance(child, nodes.field_list):
146                for field in child:
147                    ty, sg, name = field[0].astext().split(None, 2)
148                    param = addnodes.desc_parameter()
149                    param += addnodes.desc_sig_keyword_type(sg, sg)
150                    param += addnodes.desc_sig_space()
151                    param += addnodes.desc_sig_name(name, name)
152                    if ty == "arg":
153                        params += param
154                    elif ty == "ret":
155                        returns += param
156
157        anno = "signal " if self.signal else "method "
158        signode += addnodes.desc_annotation(anno, anno)
159        signode += addnodes.desc_name(sig, sig)
160        signode += params
161        if not self.signal and "noreply" not in self.options:
162            ret = addnodes.desc_returns()
163            ret += returns
164            signode += ret
165
166        return sig
167
168
169class DBusSignal(DBusMethod):
170    """
171    Implementation of ``dbus:signal``.
172    """
173
174    doc_field_types: List[Field] = [
175        TypedField(
176            "arg",
177            label=_("Arguments"),
178            names=("arg",),
179            rolename="arg",
180            typerolename=None,
181            typenames=("argtype", "type"),
182        ),
183    ]
184    signal = True
185
186    def get_index_text(self, ifacename: str, name: str) -> str:
187        return _("%s() (%s signal)") % (name, ifacename)
188
189
190class DBusProperty(DBusMember):
191    """
192    Implementation of ``dbus:property``.
193    """
194
195    option_spec: OptionSpec = DBusMember.option_spec.copy()
196    option_spec.update(
197        {
198            "type": directives.unchanged,
199            "readonly": directives.flag,
200            "writeonly": directives.flag,
201            "readwrite": directives.flag,
202            "emits-changed": directives.unchanged,
203        }
204    )
205
206    doc_field_types: List[Field] = []
207
208    def get_index_text(self, ifacename: str, name: str) -> str:
209        return _("%s (%s property)") % (name, ifacename)
210
211    def transform_content(self, contentnode: addnodes.desc_content) -> None:
212        fieldlist = nodes.field_list()
213        access = None
214        if "readonly" in self.options:
215            access = _("read-only")
216        if "writeonly" in self.options:
217            access = _("write-only")
218        if "readwrite" in self.options:
219            access = _("read & write")
220        if access:
221            content = nodes.Text(access)
222            fieldname = nodes.field_name("", _("Access"))
223            fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
224            field = nodes.field("", fieldname, fieldbody)
225            fieldlist += field
226        emits = self.options.get("emits-changed", None)
227        if emits:
228            content = nodes.Text(emits)
229            fieldname = nodes.field_name("", _("Emits Changed"))
230            fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
231            field = nodes.field("", fieldname, fieldbody)
232            fieldlist += field
233        if len(fieldlist) > 0:
234            contentnode.insert(0, fieldlist)
235
236    def handle_signature(self, sig: str, signode: desc_signature) -> str:
237        contentnode = addnodes.desc_content()
238        self.state.nested_parse(self.content, self.content_offset, contentnode)
239        ty = self.options.get("type")
240
241        signode += addnodes.desc_annotation("property ", "property ")
242        signode += addnodes.desc_name(sig, sig)
243        signode += addnodes.desc_sig_punctuation("", ":")
244        signode += addnodes.desc_sig_keyword_type(ty, ty)
245        return sig
246
247    def run(self) -> List[Node]:
248        self.name = "dbus:member"
249        return super().run()
250
251
252class DBusXRef(XRefRole):
253    def process_link(self, env, refnode, has_explicit_title, title, target):
254        refnode["dbus:interface"] = env.ref_context.get("dbus:interface")
255        if not has_explicit_title:
256            title = title.lstrip(".")  # only has a meaning for the target
257            target = target.lstrip("~")  # only has a meaning for the title
258            # if the first character is a tilde, don't display the module/class
259            # parts of the contents
260            if title[0:1] == "~":
261                title = title[1:]
262                dot = title.rfind(".")
263                if dot != -1:
264                    title = title[dot + 1 :]
265        # if the first character is a dot, search more specific namespaces first
266        # else search builtins first
267        if target[0:1] == ".":
268            target = target[1:]
269            refnode["refspecific"] = True
270        return title, target
271
272
273class DBusIndex(Index):
274    """
275    Index subclass to provide a D-Bus interfaces index.
276    """
277
278    name = "dbusindex"
279    localname = _("D-Bus Interfaces Index")
280    shortname = _("dbus")
281
282    def generate(
283        self, docnames: Iterable[str] = None
284    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
285        content: Dict[str, List[IndexEntry]] = {}
286        # list of prefixes to ignore
287        ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"]
288        ignores = sorted(ignores, key=len, reverse=True)
289
290        ifaces = sorted(
291            [
292                x
293                for x in self.domain.data["objects"].items()
294                if x[1].objtype == "interface"
295            ],
296            key=lambda x: x[0].lower(),
297        )
298        for name, (docname, node_id, _) in ifaces:
299            if docnames and docname not in docnames:
300                continue
301
302            for ignore in ignores:
303                if name.startswith(ignore):
304                    name = name[len(ignore) :]
305                    stripped = ignore
306                    break
307            else:
308                stripped = ""
309
310            entries = content.setdefault(name[0].lower(), [])
311            entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", ""))
312
313        # sort by first letter
314        sorted_content = sorted(content.items())
315
316        return sorted_content, False
317
318
319class ObjectEntry(NamedTuple):
320    docname: str
321    node_id: str
322    objtype: str
323
324
325class DBusDomain(Domain):
326    """
327    Implementation of the D-Bus domain.
328    """
329
330    name = "dbus"
331    label = "D-Bus"
332    object_types: Dict[str, ObjType] = {
333        "interface": ObjType(_("interface"), "iface", "obj"),
334        "method": ObjType(_("method"), "meth", "obj"),
335        "signal": ObjType(_("signal"), "sig", "obj"),
336        "property": ObjType(_("property"), "attr", "_prop", "obj"),
337    }
338    directives = {
339        "interface": DBusInterface,
340        "method": DBusMethod,
341        "signal": DBusSignal,
342        "property": DBusProperty,
343    }
344    roles = {
345        "iface": DBusXRef(),
346        "meth": DBusXRef(),
347        "sig": DBusXRef(),
348        "prop": DBusXRef(),
349    }
350    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
351        "objects": {},  # fullname -> ObjectEntry
352    }
353    indices = [
354        DBusIndex,
355    ]
356
357    @property
358    def objects(self) -> Dict[str, ObjectEntry]:
359        return self.data.setdefault("objects", {})  # fullname -> ObjectEntry
360
361    def note_object(
362        self, name: str, objtype: str, node_id: str, location: Any = None
363    ) -> None:
364        self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
365
366    def clear_doc(self, docname: str) -> None:
367        for fullname, obj in list(self.objects.items()):
368            if obj.docname == docname:
369                del self.objects[fullname]
370
371    def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:
372        # skip parens
373        if name[-2:] == "()":
374            name = name[:-2]
375        if typ in ("meth", "sig", "prop"):
376            try:
377                ifacename, name = name.rsplit(".", 1)
378            except ValueError:
379                pass
380        return self.objects.get(name)
381
382    def resolve_xref(
383        self,
384        env: "BuildEnvironment",
385        fromdocname: str,
386        builder: "Builder",
387        typ: str,
388        target: str,
389        node: pending_xref,
390        contnode: Element,
391    ) -> Optional[Element]:
392        """Resolve the pending_xref *node* with the given *typ* and *target*."""
393        objdef = self.find_obj(typ, target)
394        if objdef:
395            return node_utils.make_refnode(
396                builder, fromdocname, objdef.docname, objdef.node_id, contnode
397            )
398
399    def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
400        for refname, obj in self.objects.items():
401            yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
402
403
404def setup(app):
405    app.add_domain(DBusDomain)
406    app.add_config_value("dbus_index_common_prefix", [], "env")
407