Marc-André Lureau | 2668dc7 | 2021-10-06 01:00:35 +0400 | [diff] [blame] | 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 | |
| 9 | from typing import ( |
| 10 | Any, |
| 11 | Dict, |
| 12 | Iterable, |
| 13 | Iterator, |
| 14 | List, |
| 15 | NamedTuple, |
| 16 | Optional, |
| 17 | Tuple, |
| 18 | cast, |
| 19 | ) |
| 20 | |
| 21 | from docutils import nodes |
| 22 | from docutils.nodes import Element, Node |
| 23 | from docutils.parsers.rst import directives |
| 24 | from sphinx import addnodes |
| 25 | from sphinx.addnodes import desc_signature, pending_xref |
| 26 | from sphinx.directives import ObjectDescription |
| 27 | from sphinx.domains import Domain, Index, IndexEntry, ObjType |
| 28 | from sphinx.locale import _ |
| 29 | from sphinx.roles import XRefRole |
| 30 | from sphinx.util import nodes as node_utils |
| 31 | from sphinx.util.docfields import Field, TypedField |
| 32 | from sphinx.util.typing import OptionSpec |
| 33 | |
| 34 | |
| 35 | class 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 | |
| 71 | class 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 | |
| 99 | class DBusMember(DBusDescription): |
| 100 | |
| 101 | signal = False |
| 102 | |
| 103 | |
| 104 | class 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 | |
| 169 | class 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 | |
| 190 | class 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 | |
| 252 | class 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 | |
| 273 | class 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 | |
| 319 | class ObjectEntry(NamedTuple): |
| 320 | docname: str |
| 321 | node_id: str |
| 322 | objtype: str |
| 323 | |
| 324 | |
| 325 | class 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 | |
Fabiano Rosas | ed8d951 | 2023-05-03 17:39:46 -0300 | [diff] [blame] | 403 | def merge_domaindata(self, docnames, otherdata): |
| 404 | for name, obj in otherdata['objects'].items(): |
| 405 | if obj.docname in docnames: |
| 406 | self.data['objects'][name] = obj |
Marc-André Lureau | 2668dc7 | 2021-10-06 01:00:35 +0400 | [diff] [blame] | 407 | |
| 408 | def setup(app): |
| 409 | app.add_domain(DBusDomain) |
| 410 | app.add_config_value("dbus_index_common_prefix", [], "env") |