| # 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 merge_domaindata(self, docnames, otherdata): |
| for name, obj in otherdata['objects'].items(): |
| if obj.docname in docnames: |
| self.data['objects'][name] = obj |
| |
| def setup(app): |
| app.add_domain(DBusDomain) |
| app.add_config_value("dbus_index_common_prefix", [], "env") |