diff options
author | Marc-André Lureau | 2021-10-05 23:00:35 +0200 |
---|---|---|
committer | Marc-André Lureau | 2021-12-21 07:50:21 +0100 |
commit | 2668dc7b5d9f56d8c3e6d2876c526fddc7068eca (patch) | |
tree | ae68445b8b96de1378e3e8899808144b54d659a7 /docs/sphinx/dbusdomain.py | |
parent | scripts: teach modinfo to skip non-C sources (diff) | |
download | qemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.tar.gz qemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.tar.xz qemu-2668dc7b5d9f56d8c3e6d2876c526fddc7068eca.zip |
docs/sphinx: add sphinx modules to include D-Bus documentation
Add a new dbus-doc directive to import D-Bus interfaces documentation
from the introspection XML. The comments annotations follow the
gtkdoc/kerneldoc style, and should be formatted with reST.
Note: I realize after the fact that I was implementing those modules
with sphinx 4, and that we have much lower requirements. Instead of
lowering the features and code (removing type annotations etc), let's
have a warning in the documentation when the D-Bus modules can't be
used, and point to the source XML file in that case.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Acked-by: Gerd Hoffmann <kraxel@redhat.com>
Diffstat (limited to 'docs/sphinx/dbusdomain.py')
-rw-r--r-- | docs/sphinx/dbusdomain.py | 406 |
1 files changed, 406 insertions, 0 deletions
diff --git a/docs/sphinx/dbusdomain.py b/docs/sphinx/dbusdomain.py new file mode 100644 index 0000000000..2ea95af623 --- /dev/null +++ b/docs/sphinx/dbusdomain.py @@ -0,0 +1,406 @@ +# D-Bus sphinx domain extension +# +# Copyright (C) 2021, Red Hat Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Author: Marc-André Lureau <marcandre.lureau@redhat.com> + +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + NamedTuple, + Optional, + Tuple, + cast, +) + +from docutils import nodes +from docutils.nodes import Element, Node +from docutils.parsers.rst import directives +from sphinx import addnodes +from sphinx.addnodes import desc_signature, pending_xref +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain, Index, IndexEntry, ObjType +from sphinx.locale import _ +from sphinx.roles import XRefRole +from sphinx.util import nodes as node_utils +from sphinx.util.docfields import Field, TypedField +from sphinx.util.typing import OptionSpec + + +class DBusDescription(ObjectDescription[str]): + """Base class for DBus objects""" + + option_spec: OptionSpec = ObjectDescription.option_spec.copy() + option_spec.update( + { + "deprecated": directives.flag, + } + ) + + def get_index_text(self, modname: str, name: str) -> str: + """Return the text for the index entry of the object.""" + raise NotImplementedError("must be implemented in subclasses") + + def add_target_and_index( + self, name: str, sig: str, signode: desc_signature + ) -> None: + ifacename = self.env.ref_context.get("dbus:interface") + node_id = name + if ifacename: + node_id = f"{ifacename}.{node_id}" + + signode["names"].append(name) + signode["ids"].append(node_id) + + if "noindexentry" not in self.options: + indextext = self.get_index_text(ifacename, name) + if indextext: + self.indexnode["entries"].append( + ("single", indextext, node_id, "", None) + ) + + domain = cast(DBusDomain, self.env.get_domain("dbus")) + domain.note_object(name, self.objtype, node_id, location=signode) + + +class DBusInterface(DBusDescription): + """ + Implementation of ``dbus:interface``. + """ + + def get_index_text(self, ifacename: str, name: str) -> str: + return ifacename + + def before_content(self) -> None: + self.env.ref_context["dbus:interface"] = self.arguments[0] + + def after_content(self) -> None: + self.env.ref_context.pop("dbus:interface") + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + signode += addnodes.desc_annotation("interface ", "interface ") + signode += addnodes.desc_name(sig, sig) + return sig + + def run(self) -> List[Node]: + _, node = super().run() + name = self.arguments[0] + section = nodes.section(ids=[name + "-section"]) + section += nodes.title(name, "%s interface" % name) + section += node + return [self.indexnode, section] + + +class DBusMember(DBusDescription): + + signal = False + + +class DBusMethod(DBusMember): + """ + Implementation of ``dbus:method``. + """ + + option_spec: OptionSpec = DBusMember.option_spec.copy() + option_spec.update( + { + "noreply": directives.flag, + } + ) + + doc_field_types: List[Field] = [ + TypedField( + "arg", + label=_("Arguments"), + names=("arg",), + rolename="arg", + typerolename=None, + typenames=("argtype", "type"), + ), + TypedField( + "ret", + label=_("Returns"), + names=("ret",), + rolename="ret", + typerolename=None, + typenames=("rettype", "type"), + ), + ] + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s() (%s method)") % (name, ifacename) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + params = addnodes.desc_parameterlist() + returns = addnodes.desc_parameterlist() + + contentnode = addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + for child in contentnode: + if isinstance(child, nodes.field_list): + for field in child: + ty, sg, name = field[0].astext().split(None, 2) + param = addnodes.desc_parameter() + param += addnodes.desc_sig_keyword_type(sg, sg) + param += addnodes.desc_sig_space() + param += addnodes.desc_sig_name(name, name) + if ty == "arg": + params += param + elif ty == "ret": + returns += param + + anno = "signal " if self.signal else "method " + signode += addnodes.desc_annotation(anno, anno) + signode += addnodes.desc_name(sig, sig) + signode += params + if not self.signal and "noreply" not in self.options: + ret = addnodes.desc_returns() + ret += returns + signode += ret + + return sig + + +class DBusSignal(DBusMethod): + """ + Implementation of ``dbus:signal``. + """ + + doc_field_types: List[Field] = [ + TypedField( + "arg", + label=_("Arguments"), + names=("arg",), + rolename="arg", + typerolename=None, + typenames=("argtype", "type"), + ), + ] + signal = True + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s() (%s signal)") % (name, ifacename) + + +class DBusProperty(DBusMember): + """ + Implementation of ``dbus:property``. + """ + + option_spec: OptionSpec = DBusMember.option_spec.copy() + option_spec.update( + { + "type": directives.unchanged, + "readonly": directives.flag, + "writeonly": directives.flag, + "readwrite": directives.flag, + "emits-changed": directives.unchanged, + } + ) + + doc_field_types: List[Field] = [] + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s (%s property)") % (name, ifacename) + + def transform_content(self, contentnode: addnodes.desc_content) -> None: + fieldlist = nodes.field_list() + access = None + if "readonly" in self.options: + access = _("read-only") + if "writeonly" in self.options: + access = _("write-only") + if "readwrite" in self.options: + access = _("read & write") + if access: + content = nodes.Text(access) + fieldname = nodes.field_name("", _("Access")) + fieldbody = nodes.field_body("", nodes.paragraph("", "", content)) + field = nodes.field("", fieldname, fieldbody) + fieldlist += field + emits = self.options.get("emits-changed", None) + if emits: + content = nodes.Text(emits) + fieldname = nodes.field_name("", _("Emits Changed")) + fieldbody = nodes.field_body("", nodes.paragraph("", "", content)) + field = nodes.field("", fieldname, fieldbody) + fieldlist += field + if len(fieldlist) > 0: + contentnode.insert(0, fieldlist) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + contentnode = addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + ty = self.options.get("type") + + signode += addnodes.desc_annotation("property ", "property ") + signode += addnodes.desc_name(sig, sig) + signode += addnodes.desc_sig_punctuation("", ":") + signode += addnodes.desc_sig_keyword_type(ty, ty) + return sig + + def run(self) -> List[Node]: + self.name = "dbus:member" + return super().run() + + +class DBusXRef(XRefRole): + def process_link(self, env, refnode, has_explicit_title, title, target): + refnode["dbus:interface"] = env.ref_context.get("dbus:interface") + if not has_explicit_title: + title = title.lstrip(".") # only has a meaning for the target + target = target.lstrip("~") # only has a meaning for the title + # if the first character is a tilde, don't display the module/class + # parts of the contents + if title[0:1] == "~": + title = title[1:] + dot = title.rfind(".") + if dot != -1: + title = title[dot + 1 :] + # if the first character is a dot, search more specific namespaces first + # else search builtins first + if target[0:1] == ".": + target = target[1:] + refnode["refspecific"] = True + return title, target + + +class DBusIndex(Index): + """ + Index subclass to provide a D-Bus interfaces index. + """ + + name = "dbusindex" + localname = _("D-Bus Interfaces Index") + shortname = _("dbus") + + def generate( + self, docnames: Iterable[str] = None + ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: + content: Dict[str, List[IndexEntry]] = {} + # list of prefixes to ignore + ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"] + ignores = sorted(ignores, key=len, reverse=True) + + ifaces = sorted( + [ + x + for x in self.domain.data["objects"].items() + if x[1].objtype == "interface" + ], + key=lambda x: x[0].lower(), + ) + for name, (docname, node_id, _) in ifaces: + if docnames and docname not in docnames: + continue + + for ignore in ignores: + if name.startswith(ignore): + name = name[len(ignore) :] + stripped = ignore + break + else: + stripped = "" + + entries = content.setdefault(name[0].lower(), []) + entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", "")) + + # sort by first letter + sorted_content = sorted(content.items()) + + return sorted_content, False + + +class ObjectEntry(NamedTuple): + docname: str + node_id: str + objtype: str + + +class DBusDomain(Domain): + """ + Implementation of the D-Bus domain. + """ + + name = "dbus" + label = "D-Bus" + object_types: Dict[str, ObjType] = { + "interface": ObjType(_("interface"), "iface", "obj"), + "method": ObjType(_("method"), "meth", "obj"), + "signal": ObjType(_("signal"), "sig", "obj"), + "property": ObjType(_("property"), "attr", "_prop", "obj"), + } + directives = { + "interface": DBusInterface, + "method": DBusMethod, + "signal": DBusSignal, + "property": DBusProperty, + } + roles = { + "iface": DBusXRef(), + "meth": DBusXRef(), + "sig": DBusXRef(), + "prop": DBusXRef(), + } + initial_data: Dict[str, Dict[str, Tuple[Any]]] = { + "objects": {}, # fullname -> ObjectEntry + } + indices = [ + DBusIndex, + ] + + @property + def objects(self) -> Dict[str, ObjectEntry]: + return self.data.setdefault("objects", {}) # fullname -> ObjectEntry + + def note_object( + self, name: str, objtype: str, node_id: str, location: Any = None + ) -> None: + self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype) + + def clear_doc(self, docname: str) -> None: + for fullname, obj in list(self.objects.items()): + if obj.docname == docname: + del self.objects[fullname] + + def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]: + # skip parens + if name[-2:] == "()": + name = name[:-2] + if typ in ("meth", "sig", "prop"): + try: + ifacename, name = name.rsplit(".", 1) + except ValueError: + pass + return self.objects.get(name) + + def resolve_xref( + self, + env: "BuildEnvironment", + fromdocname: str, + builder: "Builder", + typ: str, + target: str, + node: pending_xref, + contnode: Element, + ) -> Optional[Element]: + """Resolve the pending_xref *node* with the given *typ* and *target*.""" + objdef = self.find_obj(typ, target) + if objdef: + return node_utils.make_refnode( + builder, fromdocname, objdef.docname, objdef.node_id, contnode + ) + + def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]: + for refname, obj in self.objects.items(): + yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) + + +def setup(app): + app.add_domain(DBusDomain) + app.add_config_value("dbus_index_common_prefix", [], "env") |