From nobody Tue Feb 10 07:43:52 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=none dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1640072915; cv=none; d=zohomail.com; s=zohoarc; b=D+0kOFwi2/0T63fug9YXY7GG3QSsSDoyiRMf65r29/p8X5pm2EcpfC8OgR3EeMuibWxtqwpNkc/fDANoVvOzniA+QBwrTjCSenB3yUGtFkTSXbTuezxlHe2+rRYB/gTd2GvhpRRHUnF8e2X00tODe9xlfhrpFFNV2HHHzN+cdXg= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1640072915; h=Content-Type:Content-Transfer-Encoding:Cc:Date:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:To; bh=aRILXhOIpdiTtyMy0stLTqiN3WJFSqWP8QCKwXfAz0E=; b=FQLk7GC6GDlwTC4BjvEABWe/DPnQkSx1S3v6xFJp3bViwAWTJNVK6R8z8JJXsxafy1k5S5qwVo9tuZQTmWSzByI/nmqnLDKLYaAFaieQ23DzECEeLPTmeEoaTRAcKGn75kiZyTGorZ6g5XNeTBaGCMrp2Iwl3kl6+rfM6Q3U9EY= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=none dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1640072915626576.3273349950932; Mon, 20 Dec 2021 23:48:35 -0800 (PST) Received: from localhost ([::1]:41192 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mzZsw-0002UY-Bt for importer@patchew.org; Tue, 21 Dec 2021 02:48:34 -0500 Received: from eggs.gnu.org ([209.51.188.92]:58746) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mzZC3-0006o9-UC for qemu-devel@nongnu.org; Tue, 21 Dec 2021 02:04:16 -0500 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]:22590) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mzZC0-00029A-F8 for qemu-devel@nongnu.org; Tue, 21 Dec 2021 02:04:15 -0500 Received: from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com [209.132.183.4]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id us-mta-664-ZS63TXCmNmCRouv_Wp1M3w-1; Tue, 21 Dec 2021 02:04:08 -0500 Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com [10.5.11.16]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx01.redhat.com (Postfix) with ESMTPS id 66D0A2F46; Tue, 21 Dec 2021 07:04:07 +0000 (UTC) Received: from localhost (unknown [10.39.208.37]) by smtp.corp.redhat.com (Postfix) with ESMTP id CAC75838E4; Tue, 21 Dec 2021 07:03:50 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1640070251; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=aRILXhOIpdiTtyMy0stLTqiN3WJFSqWP8QCKwXfAz0E=; b=b5eYx4F2BI3740lmC/ESM/t75vmGo5l/U/EUX8HPnEOY2kH/kVeG2CL0tYihW0oqLopExO iyMUMRSm5Dy3qgETC3kw+/gdYO2eld8jF6MVerwVb9hhSB6oNA2Y0qam2/t+XVJNvXqMaL N+5+PxMCZwlt10T9Wn7UD+8BWhVrnyk= X-MC-Unique: ZS63TXCmNmCRouv_Wp1M3w-1 From: marcandre.lureau@redhat.com To: qemu-devel@nongnu.org Subject: [PULL v2 20/36] docs/sphinx: add sphinx modules to include D-Bus documentation Date: Tue, 21 Dec 2021 10:58:39 +0400 Message-Id: <20211221065855.142578-21-marcandre.lureau@redhat.com> In-Reply-To: <20211221065855.142578-1-marcandre.lureau@redhat.com> References: <20211221065855.142578-1-marcandre.lureau@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 2.79 on 10.5.11.16 Authentication-Results: relay.mimecast.com; auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=marcandre.lureau@redhat.com X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.133.124; envelope-from=marcandre.lureau@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -29 X-Spam_score: -3.0 X-Spam_bar: --- X-Spam_report: (-3.0 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.203, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001, T_FILL_THIS_FORM_SHORT=0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: peter.maydell@linaro.org, richard.henderson@linaro.org, =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: "Qemu-devel" X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1640072915993100001 From: Marc-Andr=C3=A9 Lureau 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=C3=A9 Lureau Acked-by: Gerd Hoffmann --- docs/conf.py | 8 + docs/sphinx/dbusdoc.py | 166 +++++++++++++++ docs/sphinx/dbusdomain.py | 406 +++++++++++++++++++++++++++++++++++++ docs/sphinx/dbusparser.py | 373 ++++++++++++++++++++++++++++++++++ docs/sphinx/fakedbusdoc.py | 25 +++ 5 files changed, 978 insertions(+) create mode 100644 docs/sphinx/dbusdoc.py create mode 100644 docs/sphinx/dbusdomain.py create mode 100644 docs/sphinx/dbusparser.py create mode 100644 docs/sphinx/fakedbusdoc.py diff --git a/docs/conf.py b/docs/conf.py index 763e7d243448..e79015975e6a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,6 +73,12 @@ # ones. extensions =3D ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc'] =20 +if sphinx.version_info[:3] > (4, 0, 0): + tags.add('sphinx4') + extensions +=3D ['dbusdoc'] +else: + extensions +=3D ['fakedbusdoc'] + # Add any paths that contain templates here, relative to this directory. templates_path =3D [os.path.join(qemu_docdir, '_templates')] =20 @@ -311,3 +317,5 @@ kerneldoc_srctree =3D os.path.join(qemu_docdir, '..') hxtool_srctree =3D os.path.join(qemu_docdir, '..') qapidoc_srctree =3D os.path.join(qemu_docdir, '..') +dbusdoc_srctree =3D os.path.join(qemu_docdir, '..') +dbus_index_common_prefix =3D ["org.qemu."] diff --git a/docs/sphinx/dbusdoc.py b/docs/sphinx/dbusdoc.py new file mode 100644 index 000000000000..be284ed08fd7 --- /dev/null +++ b/docs/sphinx/dbusdoc.py @@ -0,0 +1,166 @@ +# D-Bus XML documentation extension +# +# Copyright (C) 2021, Red Hat Inc. +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Author: Marc-Andr=C3=A9 Lureau +"""dbus-doc is a Sphinx extension that provides documentation from D-Bus X= ML.""" + +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 =3D logging.getLogger(__name__) + +__version__ =3D "1.0" + + +class DBusDoc: + def __init__(self, sphinx_directive, dbusfile): + self._cur_doc =3D None + self._sphinx_directive =3D sphinx_directive + self._dbusfile =3D dbusfile + self._top_node =3D nodes.section() + self.result =3D StringList() + self.indent =3D "" + + 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 +=3D " " + for arg in method.in_args: + self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_stri= ng}") + for arg in method.out_args: + self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_stri= ng}") + self.add_line("") + for line in prepare_docstring("\n" + method.doc_string): + self.add_line(line) + self.indent =3D self.indent[:-3] + + def add_signal(self, signal): + self.add_line(f".. dbus:signal:: {signal.name}") + self.add_line("") + self.indent +=3D " " + for arg in signal.args: + self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_stri= ng}") + self.add_line("") + for line in prepare_docstring("\n" + signal.doc_string): + self.add_line(line) + self.indent =3D self.indent[:-3] + + def add_property(self, prop): + self.add_line(f".. dbus:property:: {prop.name}") + self.indent +=3D " " + self.add_line(f":type: {prop.signature}") + access =3D {"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 =3D self.indent[:-3] + + def add_interface(self, iface): + self.add_line(f".. dbus:interface:: {iface.name}") + self.add_line("") + self.indent +=3D " " + 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 =3D 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 =3D nodes.paragraph() + node.document =3D 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 =3D True + required_arguments =3D 1 + optional_arguments =3D 0 + final_argument_whitespace =3D True + + def run(self): + reporter =3D self.state.document.reporter + + try: + source, lineno =3D reporter.get_source_and_line(self.lineno) = # type: ignore + except AttributeError: + source, lineno =3D (None, None) + + logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.b= lock_text) + + env =3D self.state.document.settings.env + dbusfile =3D env.config.qapidoc_srctree + "/" + self.arguments[0] + with open(dbusfile, "rb") as f: + xml_data =3D f.read() + xml =3D parse_dbus_xml(xml_data) + doc =3D DBusDoc(self, dbusfile) + for iface in xml: + doc.add_interface(iface) + + result =3D 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=3D__version__, parallel_read_safe=3DTrue, parallel= _write_safe=3DTrue) diff --git a/docs/sphinx/dbusdomain.py b/docs/sphinx/dbusdomain.py new file mode 100644 index 000000000000..2ea95af623d2 --- /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=C3=A9 Lureau + +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 =3D 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 =3D self.env.ref_context.get("dbus:interface") + node_id =3D name + if ifacename: + node_id =3D f"{ifacename}.{node_id}" + + signode["names"].append(name) + signode["ids"].append(node_id) + + if "noindexentry" not in self.options: + indextext =3D self.get_index_text(ifacename, name) + if indextext: + self.indexnode["entries"].append( + ("single", indextext, node_id, "", None) + ) + + domain =3D cast(DBusDomain, self.env.get_domain("dbus")) + domain.note_object(name, self.objtype, node_id, location=3Dsignode) + + +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"] =3D 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 +=3D addnodes.desc_annotation("interface ", "interface ") + signode +=3D addnodes.desc_name(sig, sig) + return sig + + def run(self) -> List[Node]: + _, node =3D super().run() + name =3D self.arguments[0] + section =3D nodes.section(ids=3D[name + "-section"]) + section +=3D nodes.title(name, "%s interface" % name) + section +=3D node + return [self.indexnode, section] + + +class DBusMember(DBusDescription): + + signal =3D False + + +class DBusMethod(DBusMember): + """ + Implementation of ``dbus:method``. + """ + + option_spec: OptionSpec =3D DBusMember.option_spec.copy() + option_spec.update( + { + "noreply": directives.flag, + } + ) + + doc_field_types: List[Field] =3D [ + TypedField( + "arg", + label=3D_("Arguments"), + names=3D("arg",), + rolename=3D"arg", + typerolename=3DNone, + typenames=3D("argtype", "type"), + ), + TypedField( + "ret", + label=3D_("Returns"), + names=3D("ret",), + rolename=3D"ret", + typerolename=3DNone, + typenames=3D("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 =3D addnodes.desc_parameterlist() + returns =3D addnodes.desc_parameterlist() + + contentnode =3D addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, content= node) + for child in contentnode: + if isinstance(child, nodes.field_list): + for field in child: + ty, sg, name =3D field[0].astext().split(None, 2) + param =3D addnodes.desc_parameter() + param +=3D addnodes.desc_sig_keyword_type(sg, sg) + param +=3D addnodes.desc_sig_space() + param +=3D addnodes.desc_sig_name(name, name) + if ty =3D=3D "arg": + params +=3D param + elif ty =3D=3D "ret": + returns +=3D param + + anno =3D "signal " if self.signal else "method " + signode +=3D addnodes.desc_annotation(anno, anno) + signode +=3D addnodes.desc_name(sig, sig) + signode +=3D params + if not self.signal and "noreply" not in self.options: + ret =3D addnodes.desc_returns() + ret +=3D returns + signode +=3D ret + + return sig + + +class DBusSignal(DBusMethod): + """ + Implementation of ``dbus:signal``. + """ + + doc_field_types: List[Field] =3D [ + TypedField( + "arg", + label=3D_("Arguments"), + names=3D("arg",), + rolename=3D"arg", + typerolename=3DNone, + typenames=3D("argtype", "type"), + ), + ] + signal =3D 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 =3D 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] =3D [] + + def get_index_text(self, ifacename: str, name: str) -> str: + return _("%s (%s property)") % (name, ifacename) + + def transform_content(self, contentnode: addnodes.desc_content) -> Non= e: + fieldlist =3D nodes.field_list() + access =3D None + if "readonly" in self.options: + access =3D _("read-only") + if "writeonly" in self.options: + access =3D _("write-only") + if "readwrite" in self.options: + access =3D _("read & write") + if access: + content =3D nodes.Text(access) + fieldname =3D nodes.field_name("", _("Access")) + fieldbody =3D nodes.field_body("", nodes.paragraph("", "", con= tent)) + field =3D nodes.field("", fieldname, fieldbody) + fieldlist +=3D field + emits =3D self.options.get("emits-changed", None) + if emits: + content =3D nodes.Text(emits) + fieldname =3D nodes.field_name("", _("Emits Changed")) + fieldbody =3D nodes.field_body("", nodes.paragraph("", "", con= tent)) + field =3D nodes.field("", fieldname, fieldbody) + fieldlist +=3D field + if len(fieldlist) > 0: + contentnode.insert(0, fieldlist) + + def handle_signature(self, sig: str, signode: desc_signature) -> str: + contentnode =3D addnodes.desc_content() + self.state.nested_parse(self.content, self.content_offset, content= node) + ty =3D self.options.get("type") + + signode +=3D addnodes.desc_annotation("property ", "property ") + signode +=3D addnodes.desc_name(sig, sig) + signode +=3D addnodes.desc_sig_punctuation("", ":") + signode +=3D addnodes.desc_sig_keyword_type(ty, ty) + return sig + + def run(self) -> List[Node]: + self.name =3D "dbus:member" + return super().run() + + +class DBusXRef(XRefRole): + def process_link(self, env, refnode, has_explicit_title, title, target= ): + refnode["dbus:interface"] =3D env.ref_context.get("dbus:interface") + if not has_explicit_title: + title =3D title.lstrip(".") # only has a meaning for the targ= et + target =3D target.lstrip("~") # only has a meaning for the ti= tle + # if the first character is a tilde, don't display the module/= class + # parts of the contents + if title[0:1] =3D=3D "~": + title =3D title[1:] + dot =3D title.rfind(".") + if dot !=3D -1: + title =3D title[dot + 1 :] + # if the first character is a dot, search more specific namespaces= first + # else search builtins first + if target[0:1] =3D=3D ".": + target =3D target[1:] + refnode["refspecific"] =3D True + return title, target + + +class DBusIndex(Index): + """ + Index subclass to provide a D-Bus interfaces index. + """ + + name =3D "dbusindex" + localname =3D _("D-Bus Interfaces Index") + shortname =3D _("dbus") + + def generate( + self, docnames: Iterable[str] =3D None + ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]: + content: Dict[str, List[IndexEntry]] =3D {} + # list of prefixes to ignore + ignores: List[str] =3D self.domain.env.config["dbus_index_common_p= refix"] + ignores =3D sorted(ignores, key=3Dlen, reverse=3DTrue) + + ifaces =3D sorted( + [ + x + for x in self.domain.data["objects"].items() + if x[1].objtype =3D=3D "interface" + ], + key=3Dlambda 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 =3D name[len(ignore) :] + stripped =3D ignore + break + else: + stripped =3D "" + + entries =3D content.setdefault(name[0].lower(), []) + entries.append(IndexEntry(stripped + name, 0, docname, node_id= , "", "", "")) + + # sort by first letter + sorted_content =3D 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 =3D "dbus" + label =3D "D-Bus" + object_types: Dict[str, ObjType] =3D { + "interface": ObjType(_("interface"), "iface", "obj"), + "method": ObjType(_("method"), "meth", "obj"), + "signal": ObjType(_("signal"), "sig", "obj"), + "property": ObjType(_("property"), "attr", "_prop", "obj"), + } + directives =3D { + "interface": DBusInterface, + "method": DBusMethod, + "signal": DBusSignal, + "property": DBusProperty, + } + roles =3D { + "iface": DBusXRef(), + "meth": DBusXRef(), + "sig": DBusXRef(), + "prop": DBusXRef(), + } + initial_data: Dict[str, Dict[str, Tuple[Any]]] =3D { + "objects": {}, # fullname -> ObjectEntry + } + indices =3D [ + DBusIndex, + ] + + @property + def objects(self) -> Dict[str, ObjectEntry]: + return self.data.setdefault("objects", {}) # fullname -> ObjectEn= try + + def note_object( + self, name: str, objtype: str, node_id: str, location: Any =3D None + ) -> None: + self.objects[name] =3D ObjectEntry(self.env.docname, node_id, objt= ype) + + def clear_doc(self, docname: str) -> None: + for fullname, obj in list(self.objects.items()): + if obj.docname =3D=3D docname: + del self.objects[fullname] + + def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectE= ntry]]: + # skip parens + if name[-2:] =3D=3D "()": + name =3D name[:-2] + if typ in ("meth", "sig", "prop"): + try: + ifacename, name =3D 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 *targe= t*.""" + objdef =3D self.find_obj(typ, target) + if objdef: + return node_utils.make_refnode( + builder, fromdocname, objdef.docname, objdef.node_id, cont= node + ) + + 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") diff --git a/docs/sphinx/dbusparser.py b/docs/sphinx/dbusparser.py new file mode 100644 index 000000000000..024553eae7b5 --- /dev/null +++ b/docs/sphinx/dbusparser.py @@ -0,0 +1,373 @@ +# Based from "GDBus - GLib D-Bus Library": +# +# Copyright (C) 2008-2011 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General +# Public License along with this library; if not, see . +# +# Author: David Zeuthen + +import xml.parsers.expat + + +class Annotation: + def __init__(self, key, value): + self.key =3D key + self.value =3D value + self.annotations =3D [] + self.since =3D "" + + +class Arg: + def __init__(self, name, signature): + self.name =3D name + self.signature =3D signature + self.annotations =3D [] + self.doc_string =3D "" + self.since =3D "" + + +class Method: + def __init__(self, name, h_type_implies_unix_fd=3DTrue): + self.name =3D name + self.h_type_implies_unix_fd =3D h_type_implies_unix_fd + self.in_args =3D [] + self.out_args =3D [] + self.annotations =3D [] + self.doc_string =3D "" + self.since =3D "" + self.deprecated =3D False + self.unix_fd =3D False + + +class Signal: + def __init__(self, name): + self.name =3D name + self.args =3D [] + self.annotations =3D [] + self.doc_string =3D "" + self.since =3D "" + self.deprecated =3D False + + +class Property: + def __init__(self, name, signature, access): + self.name =3D name + self.signature =3D signature + self.access =3D access + self.annotations =3D [] + self.arg =3D Arg("value", self.signature) + self.arg.annotations =3D self.annotations + self.readable =3D False + self.writable =3D False + if self.access =3D=3D "readwrite": + self.readable =3D True + self.writable =3D True + elif self.access =3D=3D "read": + self.readable =3D True + elif self.access =3D=3D "write": + self.writable =3D True + else: + raise ValueError('Invalid access type "{}"'.format(self.access= )) + self.doc_string =3D "" + self.since =3D "" + self.deprecated =3D False + self.emits_changed_signal =3D True + + +class Interface: + def __init__(self, name): + self.name =3D name + self.methods =3D [] + self.signals =3D [] + self.properties =3D [] + self.annotations =3D [] + self.doc_string =3D "" + self.doc_string_brief =3D "" + self.since =3D "" + self.deprecated =3D False + + +class DBusXMLParser: + STATE_TOP =3D "top" + STATE_NODE =3D "node" + STATE_INTERFACE =3D "interface" + STATE_METHOD =3D "method" + STATE_SIGNAL =3D "signal" + STATE_PROPERTY =3D "property" + STATE_ARG =3D "arg" + STATE_ANNOTATION =3D "annotation" + STATE_IGNORED =3D "ignored" + + def __init__(self, xml_data, h_type_implies_unix_fd=3DTrue): + self._parser =3D xml.parsers.expat.ParserCreate() + self._parser.CommentHandler =3D self.handle_comment + self._parser.CharacterDataHandler =3D self.handle_char_data + self._parser.StartElementHandler =3D self.handle_start_element + self._parser.EndElementHandler =3D self.handle_end_element + + self.parsed_interfaces =3D [] + self._cur_object =3D None + + self.state =3D DBusXMLParser.STATE_TOP + self.state_stack =3D [] + self._cur_object =3D None + self._cur_object_stack =3D [] + + self.doc_comment_last_symbol =3D "" + + self._h_type_implies_unix_fd =3D h_type_implies_unix_fd + + self._parser.Parse(xml_data) + + COMMENT_STATE_BEGIN =3D "begin" + COMMENT_STATE_PARAMS =3D "params" + COMMENT_STATE_BODY =3D "body" + COMMENT_STATE_SKIP =3D "skip" + + def handle_comment(self, data): + comment_state =3D DBusXMLParser.COMMENT_STATE_BEGIN + lines =3D data.split("\n") + symbol =3D "" + body =3D "" + in_para =3D False + params =3D {} + for line in lines: + orig_line =3D line + line =3D line.lstrip() + if comment_state =3D=3D DBusXMLParser.COMMENT_STATE_BEGIN: + if len(line) > 0: + colon_index =3D line.find(": ") + if colon_index =3D=3D -1: + if line.endswith(":"): + symbol =3D line[0 : len(line) - 1] + comment_state =3D DBusXMLParser.COMMENT_STATE_= PARAMS + else: + comment_state =3D DBusXMLParser.COMMENT_STATE_= SKIP + else: + symbol =3D line[0:colon_index] + rest_of_line =3D line[colon_index + 2 :].strip() + if len(rest_of_line) > 0: + body +=3D rest_of_line + "\n" + comment_state =3D DBusXMLParser.COMMENT_STATE_PARA= MS + elif comment_state =3D=3D DBusXMLParser.COMMENT_STATE_PARAMS: + if line.startswith("@"): + colon_index =3D line.find(": ") + if colon_index =3D=3D -1: + comment_state =3D DBusXMLParser.COMMENT_STATE_BODY + if not in_para: + in_para =3D True + body +=3D orig_line + "\n" + else: + param =3D line[1:colon_index] + docs =3D line[colon_index + 2 :] + params[param] =3D docs + else: + comment_state =3D DBusXMLParser.COMMENT_STATE_BODY + if len(line) > 0: + if not in_para: + in_para =3D True + body +=3D orig_line + "\n" + elif comment_state =3D=3D DBusXMLParser.COMMENT_STATE_BODY: + if len(line) > 0: + if not in_para: + in_para =3D True + body +=3D orig_line + "\n" + else: + if in_para: + body +=3D "\n" + in_para =3D False + if in_para: + body +=3D "\n" + + if symbol !=3D "": + self.doc_comment_last_symbol =3D symbol + self.doc_comment_params =3D params + self.doc_comment_body =3D body + + def handle_char_data(self, data): + # print 'char_data=3D%s'%data + pass + + def handle_start_element(self, name, attrs): + old_state =3D self.state + old_cur_object =3D self._cur_object + if self.state =3D=3D DBusXMLParser.STATE_IGNORED: + self.state =3D DBusXMLParser.STATE_IGNORED + elif self.state =3D=3D DBusXMLParser.STATE_TOP: + if name =3D=3D DBusXMLParser.STATE_NODE: + self.state =3D DBusXMLParser.STATE_NODE + else: + self.state =3D DBusXMLParser.STATE_IGNORED + elif self.state =3D=3D DBusXMLParser.STATE_NODE: + if name =3D=3D DBusXMLParser.STATE_INTERFACE: + self.state =3D DBusXMLParser.STATE_INTERFACE + iface =3D Interface(attrs["name"]) + self._cur_object =3D iface + self.parsed_interfaces.append(iface) + elif name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if "name" in attrs and self.doc_comment_last_symbol =3D=3D att= rs["name"]: + self._cur_object.doc_string =3D self.doc_comment_body + if "short_description" in self.doc_comment_params: + short_description =3D self.doc_comment_params["short_d= escription"] + self._cur_object.doc_string_brief =3D short_description + if "since" in self.doc_comment_params: + self._cur_object.since =3D self.doc_comment_params["si= nce"].strip() + + elif self.state =3D=3D DBusXMLParser.STATE_INTERFACE: + if name =3D=3D DBusXMLParser.STATE_METHOD: + self.state =3D DBusXMLParser.STATE_METHOD + method =3D Method( + attrs["name"], h_type_implies_unix_fd=3Dself._h_type_i= mplies_unix_fd + ) + self._cur_object.methods.append(method) + self._cur_object =3D method + elif name =3D=3D DBusXMLParser.STATE_SIGNAL: + self.state =3D DBusXMLParser.STATE_SIGNAL + signal =3D Signal(attrs["name"]) + self._cur_object.signals.append(signal) + self._cur_object =3D signal + elif name =3D=3D DBusXMLParser.STATE_PROPERTY: + self.state =3D DBusXMLParser.STATE_PROPERTY + prop =3D Property(attrs["name"], attrs["type"], attrs["acc= ess"]) + self._cur_object.properties.append(prop) + self._cur_object =3D prop + elif name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if "name" in attrs and self.doc_comment_last_symbol =3D=3D att= rs["name"]: + self._cur_object.doc_string =3D self.doc_comment_body + if "since" in self.doc_comment_params: + self._cur_object.since =3D self.doc_comment_params["si= nce"].strip() + + elif self.state =3D=3D DBusXMLParser.STATE_METHOD: + if name =3D=3D DBusXMLParser.STATE_ARG: + self.state =3D DBusXMLParser.STATE_ARG + arg_name =3D None + if "name" in attrs: + arg_name =3D attrs["name"] + arg =3D Arg(arg_name, attrs["type"]) + direction =3D attrs.get("direction", "in") + if direction =3D=3D "in": + self._cur_object.in_args.append(arg) + elif direction =3D=3D "out": + self._cur_object.out_args.append(arg) + else: + raise ValueError('Invalid direction "{}"'.format(direc= tion)) + self._cur_object =3D arg + elif name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if self.doc_comment_last_symbol =3D=3D old_cur_object.name: + if "name" in attrs and attrs["name"] in self.doc_comment_p= arams: + doc_string =3D self.doc_comment_params[attrs["name"]] + if doc_string is not None: + self._cur_object.doc_string =3D doc_string + if "since" in self.doc_comment_params: + self._cur_object.since =3D self.doc_comment_params[ + "since" + ].strip() + + elif self.state =3D=3D DBusXMLParser.STATE_SIGNAL: + if name =3D=3D DBusXMLParser.STATE_ARG: + self.state =3D DBusXMLParser.STATE_ARG + arg_name =3D None + if "name" in attrs: + arg_name =3D attrs["name"] + arg =3D Arg(arg_name, attrs["type"]) + self._cur_object.args.append(arg) + self._cur_object =3D arg + elif name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + # assign docs, if any + if self.doc_comment_last_symbol =3D=3D old_cur_object.name: + if "name" in attrs and attrs["name"] in self.doc_comment_p= arams: + doc_string =3D self.doc_comment_params[attrs["name"]] + if doc_string is not None: + self._cur_object.doc_string =3D doc_string + if "since" in self.doc_comment_params: + self._cur_object.since =3D self.doc_comment_params[ + "since" + ].strip() + + elif self.state =3D=3D DBusXMLParser.STATE_PROPERTY: + if name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + elif self.state =3D=3D DBusXMLParser.STATE_ARG: + if name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + elif self.state =3D=3D DBusXMLParser.STATE_ANNOTATION: + if name =3D=3D DBusXMLParser.STATE_ANNOTATION: + self.state =3D DBusXMLParser.STATE_ANNOTATION + anno =3D Annotation(attrs["name"], attrs["value"]) + self._cur_object.annotations.append(anno) + self._cur_object =3D anno + else: + self.state =3D DBusXMLParser.STATE_IGNORED + + else: + raise ValueError( + 'Unhandled state "{}" while entering element with name "{}= "'.format( + self.state, name + ) + ) + + self.state_stack.append(old_state) + self._cur_object_stack.append(old_cur_object) + + def handle_end_element(self, name): + self.state =3D self.state_stack.pop() + self._cur_object =3D self._cur_object_stack.pop() + + +def parse_dbus_xml(xml_data): + parser =3D DBusXMLParser(xml_data, True) + return parser.parsed_interfaces diff --git a/docs/sphinx/fakedbusdoc.py b/docs/sphinx/fakedbusdoc.py new file mode 100644 index 000000000000..a680b257547f --- /dev/null +++ b/docs/sphinx/fakedbusdoc.py @@ -0,0 +1,25 @@ +# D-Bus XML documentation extension, compatibility gunk for +"""dbus-doc is a Sphinx extension that provides documentation from D-Bus X= ML.""" + +from sphinx.application import Sphinx +from sphinx.util.docutils import SphinxDirective +from typing import Any, Dict + + +class FakeDBusDocDirective(SphinxDirective): + has_content =3D True + required_arguments =3D 1 + + def run(self): + return [] + + +def setup(app: Sphinx) -> Dict[str, Any]: + """Register a fake dbus-doc directive with Sphinx""" + app.add_directive("dbus-doc", FakeDBusDocDirective) --=20 2.34.1.8.g35151cf07204