{"id":1571497,"url":"http://patchwork.ozlabs.org/api/patches/1571497/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20211221065855.142578-21-marcandre.lureau@redhat.com/","project":{"id":14,"url":"http://patchwork.ozlabs.org/api/projects/14/?format=json","name":"QEMU Development","link_name":"qemu-devel","list_id":"qemu-devel.nongnu.org","list_email":"qemu-devel@nongnu.org","web_url":"","scm_url":"","webscm_url":"","list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<20211221065855.142578-21-marcandre.lureau@redhat.com>","list_archive_url":null,"date":"2021-12-21T06:58:39","name":"[PULL,v2,20/36] docs/sphinx: add sphinx modules to include D-Bus documentation","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"83599d488d7d0c30ee11fbd12cad23223466d4dd","submitter":{"id":66774,"url":"http://patchwork.ozlabs.org/api/people/66774/?format=json","name":"Marc-André Lureau","email":"marcandre.lureau@redhat.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20211221065855.142578-21-marcandre.lureau@redhat.com/mbox/","series":[{"id":277865,"url":"http://patchwork.ozlabs.org/api/series/277865/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/list/?series=277865","date":"2021-12-21T06:58:19","name":"[PULL,v2,01/36] ui/vdagent: add CHECK_SPICE_PROTOCOL_VERSION","version":2,"mbox":"http://patchwork.ozlabs.org/series/277865/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/1571497/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/1571497/checks/","tags":{},"related":[],"headers":{"Return-Path":"<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>","X-Original-To":"incoming@patchwork.ozlabs.org","Delivered-To":"patchwork-incoming@bilbo.ozlabs.org","Authentication-Results":["bilbo.ozlabs.org;\n\tdkim=pass (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=b5eYx4F2;\n\tdkim-atps=neutral","ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=<UNKNOWN>)","relay.mimecast.com;\n auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=marcandre.lureau@redhat.com"],"Received":["from lists.gnu.org (lists.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby bilbo.ozlabs.org (Postfix) with ESMTPS id 4JJ7pw2qwvz9s3q\n\tfor <incoming@patchwork.ozlabs.org>; Tue, 21 Dec 2021 18:47:52 +1100 (AEDT)","from localhost ([::1]:37122 helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>)\n\tid 1mzZsE-00086H-5r\n\tfor incoming@patchwork.ozlabs.org; Tue, 21 Dec 2021 02:47:50 -0500","from eggs.gnu.org ([209.51.188.92]:58746)\n by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1mzZC3-0006o9-UC\n for qemu-devel@nongnu.org; Tue, 21 Dec 2021 02:04:16 -0500","from us-smtp-delivery-124.mimecast.com ([170.10.133.124]:22590)\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1mzZC0-00029A-F8\n for qemu-devel@nongnu.org; Tue, 21 Dec 2021 02:04:15 -0500","from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com\n [209.132.183.4]) by relay.mimecast.com with ESMTP with STARTTLS\n (version=TLSv1.2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id\n us-mta-664-ZS63TXCmNmCRouv_Wp1M3w-1; Tue, 21 Dec 2021 02:04:08 -0500","from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com\n [10.5.11.16])\n (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits))\n (No client certificate requested)\n by mimecast-mx01.redhat.com (Postfix) with ESMTPS id 66D0A2F46;\n Tue, 21 Dec 2021 07:04:07 +0000 (UTC)","from localhost (unknown [10.39.208.37])\n by smtp.corp.redhat.com (Postfix) with ESMTP id CAC75838E4;\n Tue, 21 Dec 2021 07:03:50 +0000 (UTC)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1640070251;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=aRILXhOIpdiTtyMy0stLTqiN3WJFSqWP8QCKwXfAz0E=;\n b=b5eYx4F2BI3740lmC/ESM/t75vmGo5l/U/EUX8HPnEOY2kH/kVeG2CL0tYihW0oqLopExO\n iyMUMRSm5Dy3qgETC3kw+/gdYO2eld8jF6MVerwVb9hhSB6oNA2Y0qam2/t+XVJNvXqMaL\n 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\n 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","X-Mimecast-Spam-Score":"0","X-Mimecast-Originator":"redhat.com","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Received-SPF":"pass client-ip=170.10.133.124;\n envelope-from=marcandre.lureau@redhat.com;\n 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,\n DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,\n RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001,\n SPF_HELO_NONE=0.001, SPF_PASS=-0.001,\n 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":"<qemu-devel.nongnu.org>","List-Unsubscribe":"<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>","List-Archive":"<https://lists.nongnu.org/archive/html/qemu-devel>","List-Post":"<mailto:qemu-devel@nongnu.org>","List-Help":"<mailto:qemu-devel-request@nongnu.org?subject=help>","List-Subscribe":"<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>","Cc":"peter.maydell@linaro.org, richard.henderson@linaro.org, =?utf-8?q?Marc-A?=\n\t=?utf-8?q?ndr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>","Errors-To":"qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org","Sender":"\"Qemu-devel\"\n <qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>"},"content":"From: Marc-André Lureau <marcandre.lureau@redhat.com>\n\nAdd a new dbus-doc directive to import D-Bus interfaces documentation\nfrom the introspection XML. The comments annotations follow the\ngtkdoc/kerneldoc style, and should be formatted with reST.\n\nNote: I realize after the fact that I was implementing those modules\nwith sphinx 4, and that we have much lower requirements. Instead of\nlowering the features and code (removing type annotations etc), let's\nhave a warning in the documentation when the D-Bus modules can't be\nused, and point to the source XML file in that case.\n\nSigned-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>\nAcked-by: Gerd Hoffmann <kraxel@redhat.com>\n---\n docs/conf.py               |   8 +\n docs/sphinx/dbusdoc.py     | 166 +++++++++++++++\n docs/sphinx/dbusdomain.py  | 406 +++++++++++++++++++++++++++++++++++++\n docs/sphinx/dbusparser.py  | 373 ++++++++++++++++++++++++++++++++++\n docs/sphinx/fakedbusdoc.py |  25 +++\n 5 files changed, 978 insertions(+)\n create mode 100644 docs/sphinx/dbusdoc.py\n create mode 100644 docs/sphinx/dbusdomain.py\n create mode 100644 docs/sphinx/dbusparser.py\n create mode 100644 docs/sphinx/fakedbusdoc.py","diff":"diff --git a/docs/conf.py b/docs/conf.py\nindex 763e7d243448..e79015975e6a 100644\n--- a/docs/conf.py\n+++ b/docs/conf.py\n@@ -73,6 +73,12 @@\n # ones.\n extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']\n \n+if sphinx.version_info[:3] > (4, 0, 0):\n+    tags.add('sphinx4')\n+    extensions += ['dbusdoc']\n+else:\n+    extensions += ['fakedbusdoc']\n+\n # Add any paths that contain templates here, relative to this directory.\n templates_path = [os.path.join(qemu_docdir, '_templates')]\n \n@@ -311,3 +317,5 @@\n kerneldoc_srctree = os.path.join(qemu_docdir, '..')\n hxtool_srctree = os.path.join(qemu_docdir, '..')\n qapidoc_srctree = os.path.join(qemu_docdir, '..')\n+dbusdoc_srctree = os.path.join(qemu_docdir, '..')\n+dbus_index_common_prefix = [\"org.qemu.\"]\ndiff --git a/docs/sphinx/dbusdoc.py b/docs/sphinx/dbusdoc.py\nnew file mode 100644\nindex 000000000000..be284ed08fd7\n--- /dev/null\n+++ b/docs/sphinx/dbusdoc.py\n@@ -0,0 +1,166 @@\n+# D-Bus XML documentation extension\n+#\n+# Copyright (C) 2021, Red Hat Inc.\n+#\n+# SPDX-License-Identifier: LGPL-2.1-or-later\n+#\n+# Author: Marc-André Lureau <marcandre.lureau@redhat.com>\n+\"\"\"dbus-doc is a Sphinx extension that provides documentation from D-Bus XML.\"\"\"\n+\n+import os\n+import re\n+from typing import (\n+    TYPE_CHECKING,\n+    Any,\n+    Callable,\n+    Dict,\n+    Iterator,\n+    List,\n+    Optional,\n+    Sequence,\n+    Set,\n+    Tuple,\n+    Type,\n+    TypeVar,\n+    Union,\n+)\n+\n+import sphinx\n+from docutils import nodes\n+from docutils.nodes import Element, Node\n+from docutils.parsers.rst import Directive, directives\n+from docutils.parsers.rst.states import RSTState\n+from docutils.statemachine import StringList, ViewList\n+from sphinx.application import Sphinx\n+from sphinx.errors import ExtensionError\n+from sphinx.util import logging\n+from sphinx.util.docstrings import prepare_docstring\n+from sphinx.util.docutils import SphinxDirective, switch_source_input\n+from sphinx.util.nodes import nested_parse_with_titles\n+\n+import dbusdomain\n+from dbusparser import parse_dbus_xml\n+\n+logger = logging.getLogger(__name__)\n+\n+__version__ = \"1.0\"\n+\n+\n+class DBusDoc:\n+    def __init__(self, sphinx_directive, dbusfile):\n+        self._cur_doc = None\n+        self._sphinx_directive = sphinx_directive\n+        self._dbusfile = dbusfile\n+        self._top_node = nodes.section()\n+        self.result = StringList()\n+        self.indent = \"\"\n+\n+    def add_line(self, line: str, *lineno: int) -> None:\n+        \"\"\"Append one line of generated reST to the output.\"\"\"\n+        if line.strip():  # not a blank line\n+            self.result.append(self.indent + line, self._dbusfile, *lineno)\n+        else:\n+            self.result.append(\"\", self._dbusfile, *lineno)\n+\n+    def add_method(self, method):\n+        self.add_line(f\".. dbus:method:: {method.name}\")\n+        self.add_line(\"\")\n+        self.indent += \"   \"\n+        for arg in method.in_args:\n+            self.add_line(f\":arg {arg.signature} {arg.name}: {arg.doc_string}\")\n+        for arg in method.out_args:\n+            self.add_line(f\":ret {arg.signature} {arg.name}: {arg.doc_string}\")\n+        self.add_line(\"\")\n+        for line in prepare_docstring(\"\\n\" + method.doc_string):\n+            self.add_line(line)\n+        self.indent = self.indent[:-3]\n+\n+    def add_signal(self, signal):\n+        self.add_line(f\".. dbus:signal:: {signal.name}\")\n+        self.add_line(\"\")\n+        self.indent += \"   \"\n+        for arg in signal.args:\n+            self.add_line(f\":arg {arg.signature} {arg.name}: {arg.doc_string}\")\n+        self.add_line(\"\")\n+        for line in prepare_docstring(\"\\n\" + signal.doc_string):\n+            self.add_line(line)\n+        self.indent = self.indent[:-3]\n+\n+    def add_property(self, prop):\n+        self.add_line(f\".. dbus:property:: {prop.name}\")\n+        self.indent += \"   \"\n+        self.add_line(f\":type: {prop.signature}\")\n+        access = {\"read\": \"readonly\", \"write\": \"writeonly\", \"readwrite\": \"readwrite\"}[\n+            prop.access\n+        ]\n+        self.add_line(f\":{access}:\")\n+        if prop.emits_changed_signal:\n+            self.add_line(f\":emits-changed: yes\")\n+        self.add_line(\"\")\n+        for line in prepare_docstring(\"\\n\" + prop.doc_string):\n+            self.add_line(line)\n+        self.indent = self.indent[:-3]\n+\n+    def add_interface(self, iface):\n+        self.add_line(f\".. dbus:interface:: {iface.name}\")\n+        self.add_line(\"\")\n+        self.indent += \"   \"\n+        for line in prepare_docstring(\"\\n\" + iface.doc_string):\n+            self.add_line(line)\n+        for method in iface.methods:\n+            self.add_method(method)\n+        for sig in iface.signals:\n+            self.add_signal(sig)\n+        for prop in iface.properties:\n+            self.add_property(prop)\n+        self.indent = self.indent[:-3]\n+\n+\n+def parse_generated_content(state: RSTState, content: StringList) -> List[Node]:\n+    \"\"\"Parse a generated content by Documenter.\"\"\"\n+    with switch_source_input(state, content):\n+        node = nodes.paragraph()\n+        node.document = state.document\n+        state.nested_parse(content, 0, node)\n+\n+        return node.children\n+\n+\n+class DBusDocDirective(SphinxDirective):\n+    \"\"\"Extract documentation from the specified D-Bus XML file\"\"\"\n+\n+    has_content = True\n+    required_arguments = 1\n+    optional_arguments = 0\n+    final_argument_whitespace = True\n+\n+    def run(self):\n+        reporter = self.state.document.reporter\n+\n+        try:\n+            source, lineno = reporter.get_source_and_line(self.lineno)  # type: ignore\n+        except AttributeError:\n+            source, lineno = (None, None)\n+\n+        logger.debug(\"[dbusdoc] %s:%s: input:\\n%s\", source, lineno, self.block_text)\n+\n+        env = self.state.document.settings.env\n+        dbusfile = env.config.qapidoc_srctree + \"/\" + self.arguments[0]\n+        with open(dbusfile, \"rb\") as f:\n+            xml_data = f.read()\n+        xml = parse_dbus_xml(xml_data)\n+        doc = DBusDoc(self, dbusfile)\n+        for iface in xml:\n+            doc.add_interface(iface)\n+\n+        result = parse_generated_content(self.state, doc.result)\n+        return result\n+\n+\n+def setup(app: Sphinx) -> Dict[str, Any]:\n+    \"\"\"Register dbus-doc directive with Sphinx\"\"\"\n+    app.add_config_value(\"dbusdoc_srctree\", None, \"env\")\n+    app.add_directive(\"dbus-doc\", DBusDocDirective)\n+    dbusdomain.setup(app)\n+\n+    return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True)\ndiff --git a/docs/sphinx/dbusdomain.py b/docs/sphinx/dbusdomain.py\nnew file mode 100644\nindex 000000000000..2ea95af623d2\n--- /dev/null\n+++ b/docs/sphinx/dbusdomain.py\n@@ -0,0 +1,406 @@\n+# D-Bus sphinx domain extension\n+#\n+# Copyright (C) 2021, Red Hat Inc.\n+#\n+# SPDX-License-Identifier: LGPL-2.1-or-later\n+#\n+# Author: Marc-André Lureau <marcandre.lureau@redhat.com>\n+\n+from typing import (\n+    Any,\n+    Dict,\n+    Iterable,\n+    Iterator,\n+    List,\n+    NamedTuple,\n+    Optional,\n+    Tuple,\n+    cast,\n+)\n+\n+from docutils import nodes\n+from docutils.nodes import Element, Node\n+from docutils.parsers.rst import directives\n+from sphinx import addnodes\n+from sphinx.addnodes import desc_signature, pending_xref\n+from sphinx.directives import ObjectDescription\n+from sphinx.domains import Domain, Index, IndexEntry, ObjType\n+from sphinx.locale import _\n+from sphinx.roles import XRefRole\n+from sphinx.util import nodes as node_utils\n+from sphinx.util.docfields import Field, TypedField\n+from sphinx.util.typing import OptionSpec\n+\n+\n+class DBusDescription(ObjectDescription[str]):\n+    \"\"\"Base class for DBus objects\"\"\"\n+\n+    option_spec: OptionSpec = ObjectDescription.option_spec.copy()\n+    option_spec.update(\n+        {\n+            \"deprecated\": directives.flag,\n+        }\n+    )\n+\n+    def get_index_text(self, modname: str, name: str) -> str:\n+        \"\"\"Return the text for the index entry of the object.\"\"\"\n+        raise NotImplementedError(\"must be implemented in subclasses\")\n+\n+    def add_target_and_index(\n+        self, name: str, sig: str, signode: desc_signature\n+    ) -> None:\n+        ifacename = self.env.ref_context.get(\"dbus:interface\")\n+        node_id = name\n+        if ifacename:\n+            node_id = f\"{ifacename}.{node_id}\"\n+\n+        signode[\"names\"].append(name)\n+        signode[\"ids\"].append(node_id)\n+\n+        if \"noindexentry\" not in self.options:\n+            indextext = self.get_index_text(ifacename, name)\n+            if indextext:\n+                self.indexnode[\"entries\"].append(\n+                    (\"single\", indextext, node_id, \"\", None)\n+                )\n+\n+        domain = cast(DBusDomain, self.env.get_domain(\"dbus\"))\n+        domain.note_object(name, self.objtype, node_id, location=signode)\n+\n+\n+class DBusInterface(DBusDescription):\n+    \"\"\"\n+    Implementation of ``dbus:interface``.\n+    \"\"\"\n+\n+    def get_index_text(self, ifacename: str, name: str) -> str:\n+        return ifacename\n+\n+    def before_content(self) -> None:\n+        self.env.ref_context[\"dbus:interface\"] = self.arguments[0]\n+\n+    def after_content(self) -> None:\n+        self.env.ref_context.pop(\"dbus:interface\")\n+\n+    def handle_signature(self, sig: str, signode: desc_signature) -> str:\n+        signode += addnodes.desc_annotation(\"interface \", \"interface \")\n+        signode += addnodes.desc_name(sig, sig)\n+        return sig\n+\n+    def run(self) -> List[Node]:\n+        _, node = super().run()\n+        name = self.arguments[0]\n+        section = nodes.section(ids=[name + \"-section\"])\n+        section += nodes.title(name, \"%s interface\" % name)\n+        section += node\n+        return [self.indexnode, section]\n+\n+\n+class DBusMember(DBusDescription):\n+\n+    signal = False\n+\n+\n+class DBusMethod(DBusMember):\n+    \"\"\"\n+    Implementation of ``dbus:method``.\n+    \"\"\"\n+\n+    option_spec: OptionSpec = DBusMember.option_spec.copy()\n+    option_spec.update(\n+        {\n+            \"noreply\": directives.flag,\n+        }\n+    )\n+\n+    doc_field_types: List[Field] = [\n+        TypedField(\n+            \"arg\",\n+            label=_(\"Arguments\"),\n+            names=(\"arg\",),\n+            rolename=\"arg\",\n+            typerolename=None,\n+            typenames=(\"argtype\", \"type\"),\n+        ),\n+        TypedField(\n+            \"ret\",\n+            label=_(\"Returns\"),\n+            names=(\"ret\",),\n+            rolename=\"ret\",\n+            typerolename=None,\n+            typenames=(\"rettype\", \"type\"),\n+        ),\n+    ]\n+\n+    def get_index_text(self, ifacename: str, name: str) -> str:\n+        return _(\"%s() (%s method)\") % (name, ifacename)\n+\n+    def handle_signature(self, sig: str, signode: desc_signature) -> str:\n+        params = addnodes.desc_parameterlist()\n+        returns = addnodes.desc_parameterlist()\n+\n+        contentnode = addnodes.desc_content()\n+        self.state.nested_parse(self.content, self.content_offset, contentnode)\n+        for child in contentnode:\n+            if isinstance(child, nodes.field_list):\n+                for field in child:\n+                    ty, sg, name = field[0].astext().split(None, 2)\n+                    param = addnodes.desc_parameter()\n+                    param += addnodes.desc_sig_keyword_type(sg, sg)\n+                    param += addnodes.desc_sig_space()\n+                    param += addnodes.desc_sig_name(name, name)\n+                    if ty == \"arg\":\n+                        params += param\n+                    elif ty == \"ret\":\n+                        returns += param\n+\n+        anno = \"signal \" if self.signal else \"method \"\n+        signode += addnodes.desc_annotation(anno, anno)\n+        signode += addnodes.desc_name(sig, sig)\n+        signode += params\n+        if not self.signal and \"noreply\" not in self.options:\n+            ret = addnodes.desc_returns()\n+            ret += returns\n+            signode += ret\n+\n+        return sig\n+\n+\n+class DBusSignal(DBusMethod):\n+    \"\"\"\n+    Implementation of ``dbus:signal``.\n+    \"\"\"\n+\n+    doc_field_types: List[Field] = [\n+        TypedField(\n+            \"arg\",\n+            label=_(\"Arguments\"),\n+            names=(\"arg\",),\n+            rolename=\"arg\",\n+            typerolename=None,\n+            typenames=(\"argtype\", \"type\"),\n+        ),\n+    ]\n+    signal = True\n+\n+    def get_index_text(self, ifacename: str, name: str) -> str:\n+        return _(\"%s() (%s signal)\") % (name, ifacename)\n+\n+\n+class DBusProperty(DBusMember):\n+    \"\"\"\n+    Implementation of ``dbus:property``.\n+    \"\"\"\n+\n+    option_spec: OptionSpec = DBusMember.option_spec.copy()\n+    option_spec.update(\n+        {\n+            \"type\": directives.unchanged,\n+            \"readonly\": directives.flag,\n+            \"writeonly\": directives.flag,\n+            \"readwrite\": directives.flag,\n+            \"emits-changed\": directives.unchanged,\n+        }\n+    )\n+\n+    doc_field_types: List[Field] = []\n+\n+    def get_index_text(self, ifacename: str, name: str) -> str:\n+        return _(\"%s (%s property)\") % (name, ifacename)\n+\n+    def transform_content(self, contentnode: addnodes.desc_content) -> None:\n+        fieldlist = nodes.field_list()\n+        access = None\n+        if \"readonly\" in self.options:\n+            access = _(\"read-only\")\n+        if \"writeonly\" in self.options:\n+            access = _(\"write-only\")\n+        if \"readwrite\" in self.options:\n+            access = _(\"read & write\")\n+        if access:\n+            content = nodes.Text(access)\n+            fieldname = nodes.field_name(\"\", _(\"Access\"))\n+            fieldbody = nodes.field_body(\"\", nodes.paragraph(\"\", \"\", content))\n+            field = nodes.field(\"\", fieldname, fieldbody)\n+            fieldlist += field\n+        emits = self.options.get(\"emits-changed\", None)\n+        if emits:\n+            content = nodes.Text(emits)\n+            fieldname = nodes.field_name(\"\", _(\"Emits Changed\"))\n+            fieldbody = nodes.field_body(\"\", nodes.paragraph(\"\", \"\", content))\n+            field = nodes.field(\"\", fieldname, fieldbody)\n+            fieldlist += field\n+        if len(fieldlist) > 0:\n+            contentnode.insert(0, fieldlist)\n+\n+    def handle_signature(self, sig: str, signode: desc_signature) -> str:\n+        contentnode = addnodes.desc_content()\n+        self.state.nested_parse(self.content, self.content_offset, contentnode)\n+        ty = self.options.get(\"type\")\n+\n+        signode += addnodes.desc_annotation(\"property \", \"property \")\n+        signode += addnodes.desc_name(sig, sig)\n+        signode += addnodes.desc_sig_punctuation(\"\", \":\")\n+        signode += addnodes.desc_sig_keyword_type(ty, ty)\n+        return sig\n+\n+    def run(self) -> List[Node]:\n+        self.name = \"dbus:member\"\n+        return super().run()\n+\n+\n+class DBusXRef(XRefRole):\n+    def process_link(self, env, refnode, has_explicit_title, title, target):\n+        refnode[\"dbus:interface\"] = env.ref_context.get(\"dbus:interface\")\n+        if not has_explicit_title:\n+            title = title.lstrip(\".\")  # only has a meaning for the target\n+            target = target.lstrip(\"~\")  # only has a meaning for the title\n+            # if the first character is a tilde, don't display the module/class\n+            # parts of the contents\n+            if title[0:1] == \"~\":\n+                title = title[1:]\n+                dot = title.rfind(\".\")\n+                if dot != -1:\n+                    title = title[dot + 1 :]\n+        # if the first character is a dot, search more specific namespaces first\n+        # else search builtins first\n+        if target[0:1] == \".\":\n+            target = target[1:]\n+            refnode[\"refspecific\"] = True\n+        return title, target\n+\n+\n+class DBusIndex(Index):\n+    \"\"\"\n+    Index subclass to provide a D-Bus interfaces index.\n+    \"\"\"\n+\n+    name = \"dbusindex\"\n+    localname = _(\"D-Bus Interfaces Index\")\n+    shortname = _(\"dbus\")\n+\n+    def generate(\n+        self, docnames: Iterable[str] = None\n+    ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:\n+        content: Dict[str, List[IndexEntry]] = {}\n+        # list of prefixes to ignore\n+        ignores: List[str] = self.domain.env.config[\"dbus_index_common_prefix\"]\n+        ignores = sorted(ignores, key=len, reverse=True)\n+\n+        ifaces = sorted(\n+            [\n+                x\n+                for x in self.domain.data[\"objects\"].items()\n+                if x[1].objtype == \"interface\"\n+            ],\n+            key=lambda x: x[0].lower(),\n+        )\n+        for name, (docname, node_id, _) in ifaces:\n+            if docnames and docname not in docnames:\n+                continue\n+\n+            for ignore in ignores:\n+                if name.startswith(ignore):\n+                    name = name[len(ignore) :]\n+                    stripped = ignore\n+                    break\n+            else:\n+                stripped = \"\"\n+\n+            entries = content.setdefault(name[0].lower(), [])\n+            entries.append(IndexEntry(stripped + name, 0, docname, node_id, \"\", \"\", \"\"))\n+\n+        # sort by first letter\n+        sorted_content = sorted(content.items())\n+\n+        return sorted_content, False\n+\n+\n+class ObjectEntry(NamedTuple):\n+    docname: str\n+    node_id: str\n+    objtype: str\n+\n+\n+class DBusDomain(Domain):\n+    \"\"\"\n+    Implementation of the D-Bus domain.\n+    \"\"\"\n+\n+    name = \"dbus\"\n+    label = \"D-Bus\"\n+    object_types: Dict[str, ObjType] = {\n+        \"interface\": ObjType(_(\"interface\"), \"iface\", \"obj\"),\n+        \"method\": ObjType(_(\"method\"), \"meth\", \"obj\"),\n+        \"signal\": ObjType(_(\"signal\"), \"sig\", \"obj\"),\n+        \"property\": ObjType(_(\"property\"), \"attr\", \"_prop\", \"obj\"),\n+    }\n+    directives = {\n+        \"interface\": DBusInterface,\n+        \"method\": DBusMethod,\n+        \"signal\": DBusSignal,\n+        \"property\": DBusProperty,\n+    }\n+    roles = {\n+        \"iface\": DBusXRef(),\n+        \"meth\": DBusXRef(),\n+        \"sig\": DBusXRef(),\n+        \"prop\": DBusXRef(),\n+    }\n+    initial_data: Dict[str, Dict[str, Tuple[Any]]] = {\n+        \"objects\": {},  # fullname -> ObjectEntry\n+    }\n+    indices = [\n+        DBusIndex,\n+    ]\n+\n+    @property\n+    def objects(self) -> Dict[str, ObjectEntry]:\n+        return self.data.setdefault(\"objects\", {})  # fullname -> ObjectEntry\n+\n+    def note_object(\n+        self, name: str, objtype: str, node_id: str, location: Any = None\n+    ) -> None:\n+        self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)\n+\n+    def clear_doc(self, docname: str) -> None:\n+        for fullname, obj in list(self.objects.items()):\n+            if obj.docname == docname:\n+                del self.objects[fullname]\n+\n+    def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:\n+        # skip parens\n+        if name[-2:] == \"()\":\n+            name = name[:-2]\n+        if typ in (\"meth\", \"sig\", \"prop\"):\n+            try:\n+                ifacename, name = name.rsplit(\".\", 1)\n+            except ValueError:\n+                pass\n+        return self.objects.get(name)\n+\n+    def resolve_xref(\n+        self,\n+        env: \"BuildEnvironment\",\n+        fromdocname: str,\n+        builder: \"Builder\",\n+        typ: str,\n+        target: str,\n+        node: pending_xref,\n+        contnode: Element,\n+    ) -> Optional[Element]:\n+        \"\"\"Resolve the pending_xref *node* with the given *typ* and *target*.\"\"\"\n+        objdef = self.find_obj(typ, target)\n+        if objdef:\n+            return node_utils.make_refnode(\n+                builder, fromdocname, objdef.docname, objdef.node_id, contnode\n+            )\n+\n+    def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:\n+        for refname, obj in self.objects.items():\n+            yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)\n+\n+\n+def setup(app):\n+    app.add_domain(DBusDomain)\n+    app.add_config_value(\"dbus_index_common_prefix\", [], \"env\")\ndiff --git a/docs/sphinx/dbusparser.py b/docs/sphinx/dbusparser.py\nnew file mode 100644\nindex 000000000000..024553eae7b5\n--- /dev/null\n+++ b/docs/sphinx/dbusparser.py\n@@ -0,0 +1,373 @@\n+# Based from \"GDBus - GLib D-Bus Library\":\n+#\n+# Copyright (C) 2008-2011 Red Hat, Inc.\n+#\n+# This library is free software; you can redistribute it and/or\n+# modify it under the terms of the GNU Lesser General Public\n+# License as published by the Free Software Foundation; either\n+# version 2.1 of the License, or (at your option) any later version.\n+#\n+# This library is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+# Lesser General Public License for more details.\n+#\n+# You should have received a copy of the GNU Lesser General\n+# Public License along with this library; if not, see <http://www.gnu.org/licenses/>.\n+#\n+# Author: David Zeuthen <davidz@redhat.com>\n+\n+import xml.parsers.expat\n+\n+\n+class Annotation:\n+    def __init__(self, key, value):\n+        self.key = key\n+        self.value = value\n+        self.annotations = []\n+        self.since = \"\"\n+\n+\n+class Arg:\n+    def __init__(self, name, signature):\n+        self.name = name\n+        self.signature = signature\n+        self.annotations = []\n+        self.doc_string = \"\"\n+        self.since = \"\"\n+\n+\n+class Method:\n+    def __init__(self, name, h_type_implies_unix_fd=True):\n+        self.name = name\n+        self.h_type_implies_unix_fd = h_type_implies_unix_fd\n+        self.in_args = []\n+        self.out_args = []\n+        self.annotations = []\n+        self.doc_string = \"\"\n+        self.since = \"\"\n+        self.deprecated = False\n+        self.unix_fd = False\n+\n+\n+class Signal:\n+    def __init__(self, name):\n+        self.name = name\n+        self.args = []\n+        self.annotations = []\n+        self.doc_string = \"\"\n+        self.since = \"\"\n+        self.deprecated = False\n+\n+\n+class Property:\n+    def __init__(self, name, signature, access):\n+        self.name = name\n+        self.signature = signature\n+        self.access = access\n+        self.annotations = []\n+        self.arg = Arg(\"value\", self.signature)\n+        self.arg.annotations = self.annotations\n+        self.readable = False\n+        self.writable = False\n+        if self.access == \"readwrite\":\n+            self.readable = True\n+            self.writable = True\n+        elif self.access == \"read\":\n+            self.readable = True\n+        elif self.access == \"write\":\n+            self.writable = True\n+        else:\n+            raise ValueError('Invalid access type \"{}\"'.format(self.access))\n+        self.doc_string = \"\"\n+        self.since = \"\"\n+        self.deprecated = False\n+        self.emits_changed_signal = True\n+\n+\n+class Interface:\n+    def __init__(self, name):\n+        self.name = name\n+        self.methods = []\n+        self.signals = []\n+        self.properties = []\n+        self.annotations = []\n+        self.doc_string = \"\"\n+        self.doc_string_brief = \"\"\n+        self.since = \"\"\n+        self.deprecated = False\n+\n+\n+class DBusXMLParser:\n+    STATE_TOP = \"top\"\n+    STATE_NODE = \"node\"\n+    STATE_INTERFACE = \"interface\"\n+    STATE_METHOD = \"method\"\n+    STATE_SIGNAL = \"signal\"\n+    STATE_PROPERTY = \"property\"\n+    STATE_ARG = \"arg\"\n+    STATE_ANNOTATION = \"annotation\"\n+    STATE_IGNORED = \"ignored\"\n+\n+    def __init__(self, xml_data, h_type_implies_unix_fd=True):\n+        self._parser = xml.parsers.expat.ParserCreate()\n+        self._parser.CommentHandler = self.handle_comment\n+        self._parser.CharacterDataHandler = self.handle_char_data\n+        self._parser.StartElementHandler = self.handle_start_element\n+        self._parser.EndElementHandler = self.handle_end_element\n+\n+        self.parsed_interfaces = []\n+        self._cur_object = None\n+\n+        self.state = DBusXMLParser.STATE_TOP\n+        self.state_stack = []\n+        self._cur_object = None\n+        self._cur_object_stack = []\n+\n+        self.doc_comment_last_symbol = \"\"\n+\n+        self._h_type_implies_unix_fd = h_type_implies_unix_fd\n+\n+        self._parser.Parse(xml_data)\n+\n+    COMMENT_STATE_BEGIN = \"begin\"\n+    COMMENT_STATE_PARAMS = \"params\"\n+    COMMENT_STATE_BODY = \"body\"\n+    COMMENT_STATE_SKIP = \"skip\"\n+\n+    def handle_comment(self, data):\n+        comment_state = DBusXMLParser.COMMENT_STATE_BEGIN\n+        lines = data.split(\"\\n\")\n+        symbol = \"\"\n+        body = \"\"\n+        in_para = False\n+        params = {}\n+        for line in lines:\n+            orig_line = line\n+            line = line.lstrip()\n+            if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN:\n+                if len(line) > 0:\n+                    colon_index = line.find(\": \")\n+                    if colon_index == -1:\n+                        if line.endswith(\":\"):\n+                            symbol = line[0 : len(line) - 1]\n+                            comment_state = DBusXMLParser.COMMENT_STATE_PARAMS\n+                        else:\n+                            comment_state = DBusXMLParser.COMMENT_STATE_SKIP\n+                    else:\n+                        symbol = line[0:colon_index]\n+                        rest_of_line = line[colon_index + 2 :].strip()\n+                        if len(rest_of_line) > 0:\n+                            body += rest_of_line + \"\\n\"\n+                        comment_state = DBusXMLParser.COMMENT_STATE_PARAMS\n+            elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS:\n+                if line.startswith(\"@\"):\n+                    colon_index = line.find(\": \")\n+                    if colon_index == -1:\n+                        comment_state = DBusXMLParser.COMMENT_STATE_BODY\n+                        if not in_para:\n+                            in_para = True\n+                        body += orig_line + \"\\n\"\n+                    else:\n+                        param = line[1:colon_index]\n+                        docs = line[colon_index + 2 :]\n+                        params[param] = docs\n+                else:\n+                    comment_state = DBusXMLParser.COMMENT_STATE_BODY\n+                    if len(line) > 0:\n+                        if not in_para:\n+                            in_para = True\n+                        body += orig_line + \"\\n\"\n+            elif comment_state == DBusXMLParser.COMMENT_STATE_BODY:\n+                if len(line) > 0:\n+                    if not in_para:\n+                        in_para = True\n+                    body += orig_line + \"\\n\"\n+                else:\n+                    if in_para:\n+                        body += \"\\n\"\n+                        in_para = False\n+        if in_para:\n+            body += \"\\n\"\n+\n+        if symbol != \"\":\n+            self.doc_comment_last_symbol = symbol\n+            self.doc_comment_params = params\n+            self.doc_comment_body = body\n+\n+    def handle_char_data(self, data):\n+        # print 'char_data=%s'%data\n+        pass\n+\n+    def handle_start_element(self, name, attrs):\n+        old_state = self.state\n+        old_cur_object = self._cur_object\n+        if self.state == DBusXMLParser.STATE_IGNORED:\n+            self.state = DBusXMLParser.STATE_IGNORED\n+        elif self.state == DBusXMLParser.STATE_TOP:\n+            if name == DBusXMLParser.STATE_NODE:\n+                self.state = DBusXMLParser.STATE_NODE\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+        elif self.state == DBusXMLParser.STATE_NODE:\n+            if name == DBusXMLParser.STATE_INTERFACE:\n+                self.state = DBusXMLParser.STATE_INTERFACE\n+                iface = Interface(attrs[\"name\"])\n+                self._cur_object = iface\n+                self.parsed_interfaces.append(iface)\n+            elif name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+            # assign docs, if any\n+            if \"name\" in attrs and self.doc_comment_last_symbol == attrs[\"name\"]:\n+                self._cur_object.doc_string = self.doc_comment_body\n+                if \"short_description\" in self.doc_comment_params:\n+                    short_description = self.doc_comment_params[\"short_description\"]\n+                    self._cur_object.doc_string_brief = short_description\n+                if \"since\" in self.doc_comment_params:\n+                    self._cur_object.since = self.doc_comment_params[\"since\"].strip()\n+\n+        elif self.state == DBusXMLParser.STATE_INTERFACE:\n+            if name == DBusXMLParser.STATE_METHOD:\n+                self.state = DBusXMLParser.STATE_METHOD\n+                method = Method(\n+                    attrs[\"name\"], h_type_implies_unix_fd=self._h_type_implies_unix_fd\n+                )\n+                self._cur_object.methods.append(method)\n+                self._cur_object = method\n+            elif name == DBusXMLParser.STATE_SIGNAL:\n+                self.state = DBusXMLParser.STATE_SIGNAL\n+                signal = Signal(attrs[\"name\"])\n+                self._cur_object.signals.append(signal)\n+                self._cur_object = signal\n+            elif name == DBusXMLParser.STATE_PROPERTY:\n+                self.state = DBusXMLParser.STATE_PROPERTY\n+                prop = Property(attrs[\"name\"], attrs[\"type\"], attrs[\"access\"])\n+                self._cur_object.properties.append(prop)\n+                self._cur_object = prop\n+            elif name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+            # assign docs, if any\n+            if \"name\" in attrs and self.doc_comment_last_symbol == attrs[\"name\"]:\n+                self._cur_object.doc_string = self.doc_comment_body\n+                if \"since\" in self.doc_comment_params:\n+                    self._cur_object.since = self.doc_comment_params[\"since\"].strip()\n+\n+        elif self.state == DBusXMLParser.STATE_METHOD:\n+            if name == DBusXMLParser.STATE_ARG:\n+                self.state = DBusXMLParser.STATE_ARG\n+                arg_name = None\n+                if \"name\" in attrs:\n+                    arg_name = attrs[\"name\"]\n+                arg = Arg(arg_name, attrs[\"type\"])\n+                direction = attrs.get(\"direction\", \"in\")\n+                if direction == \"in\":\n+                    self._cur_object.in_args.append(arg)\n+                elif direction == \"out\":\n+                    self._cur_object.out_args.append(arg)\n+                else:\n+                    raise ValueError('Invalid direction \"{}\"'.format(direction))\n+                self._cur_object = arg\n+            elif name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+            # assign docs, if any\n+            if self.doc_comment_last_symbol == old_cur_object.name:\n+                if \"name\" in attrs and attrs[\"name\"] in self.doc_comment_params:\n+                    doc_string = self.doc_comment_params[attrs[\"name\"]]\n+                    if doc_string is not None:\n+                        self._cur_object.doc_string = doc_string\n+                    if \"since\" in self.doc_comment_params:\n+                        self._cur_object.since = self.doc_comment_params[\n+                            \"since\"\n+                        ].strip()\n+\n+        elif self.state == DBusXMLParser.STATE_SIGNAL:\n+            if name == DBusXMLParser.STATE_ARG:\n+                self.state = DBusXMLParser.STATE_ARG\n+                arg_name = None\n+                if \"name\" in attrs:\n+                    arg_name = attrs[\"name\"]\n+                arg = Arg(arg_name, attrs[\"type\"])\n+                self._cur_object.args.append(arg)\n+                self._cur_object = arg\n+            elif name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+            # assign docs, if any\n+            if self.doc_comment_last_symbol == old_cur_object.name:\n+                if \"name\" in attrs and attrs[\"name\"] in self.doc_comment_params:\n+                    doc_string = self.doc_comment_params[attrs[\"name\"]]\n+                    if doc_string is not None:\n+                        self._cur_object.doc_string = doc_string\n+                    if \"since\" in self.doc_comment_params:\n+                        self._cur_object.since = self.doc_comment_params[\n+                            \"since\"\n+                        ].strip()\n+\n+        elif self.state == DBusXMLParser.STATE_PROPERTY:\n+            if name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+        elif self.state == DBusXMLParser.STATE_ARG:\n+            if name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+        elif self.state == DBusXMLParser.STATE_ANNOTATION:\n+            if name == DBusXMLParser.STATE_ANNOTATION:\n+                self.state = DBusXMLParser.STATE_ANNOTATION\n+                anno = Annotation(attrs[\"name\"], attrs[\"value\"])\n+                self._cur_object.annotations.append(anno)\n+                self._cur_object = anno\n+            else:\n+                self.state = DBusXMLParser.STATE_IGNORED\n+\n+        else:\n+            raise ValueError(\n+                'Unhandled state \"{}\" while entering element with name \"{}\"'.format(\n+                    self.state, name\n+                )\n+            )\n+\n+        self.state_stack.append(old_state)\n+        self._cur_object_stack.append(old_cur_object)\n+\n+    def handle_end_element(self, name):\n+        self.state = self.state_stack.pop()\n+        self._cur_object = self._cur_object_stack.pop()\n+\n+\n+def parse_dbus_xml(xml_data):\n+    parser = DBusXMLParser(xml_data, True)\n+    return parser.parsed_interfaces\ndiff --git a/docs/sphinx/fakedbusdoc.py b/docs/sphinx/fakedbusdoc.py\nnew file mode 100644\nindex 000000000000..a680b257547f\n--- /dev/null\n+++ b/docs/sphinx/fakedbusdoc.py\n@@ -0,0 +1,25 @@\n+# D-Bus XML documentation extension, compatibility gunk for <sphinx4\n+#\n+# Copyright (C) 2021, Red Hat Inc.\n+#\n+# SPDX-License-Identifier: LGPL-2.1-or-later\n+#\n+# Author: Marc-André Lureau <marcandre.lureau@redhat.com>\n+\"\"\"dbus-doc is a Sphinx extension that provides documentation from D-Bus XML.\"\"\"\n+\n+from sphinx.application import Sphinx\n+from sphinx.util.docutils import SphinxDirective\n+from typing import Any, Dict\n+\n+\n+class FakeDBusDocDirective(SphinxDirective):\n+    has_content = True\n+    required_arguments = 1\n+\n+    def run(self):\n+        return []\n+\n+\n+def setup(app: Sphinx) -> Dict[str, Any]:\n+    \"\"\"Register a fake dbus-doc directive with Sphinx\"\"\"\n+    app.add_directive(\"dbus-doc\", FakeDBusDocDirective)\n","prefixes":["PULL","v2","20/36"]}