mirror of https://gitee.com/openkylin/qemu.git
docs/sphinx: add sphinx modules to include D-Bus documentation
Add a new dbus-doc directive to import D-Bus interfaces documentation from the introspection XML. The comments annotations follow the gtkdoc/kerneldoc style, and should be formatted with reST. Note: I realize after the fact that I was implementing those modules with sphinx 4, and that we have much lower requirements. Instead of lowering the features and code (removing type annotations etc), let's have a warning in the documentation when the D-Bus modules can't be used, and point to the source XML file in that case. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> Acked-by: Gerd Hoffmann <kraxel@redhat.com>
This commit is contained in:
parent
20f19713ef
commit
2668dc7b5d
|
@ -73,6 +73,12 @@
|
|||
# ones.
|
||||
extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
|
||||
|
||||
if sphinx.version_info[:3] > (4, 0, 0):
|
||||
tags.add('sphinx4')
|
||||
extensions += ['dbusdoc']
|
||||
else:
|
||||
extensions += ['fakedbusdoc']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = [os.path.join(qemu_docdir, '_templates')]
|
||||
|
||||
|
@ -311,3 +317,5 @@
|
|||
kerneldoc_srctree = os.path.join(qemu_docdir, '..')
|
||||
hxtool_srctree = os.path.join(qemu_docdir, '..')
|
||||
qapidoc_srctree = os.path.join(qemu_docdir, '..')
|
||||
dbusdoc_srctree = os.path.join(qemu_docdir, '..')
|
||||
dbus_index_common_prefix = ["org.qemu."]
|
||||
|
|
|
@ -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é Lureau <marcandre.lureau@redhat.com>
|
||||
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||
|
||||
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 = logging.getLogger(__name__)
|
||||
|
||||
__version__ = "1.0"
|
||||
|
||||
|
||||
class DBusDoc:
|
||||
def __init__(self, sphinx_directive, dbusfile):
|
||||
self._cur_doc = None
|
||||
self._sphinx_directive = sphinx_directive
|
||||
self._dbusfile = dbusfile
|
||||
self._top_node = nodes.section()
|
||||
self.result = StringList()
|
||||
self.indent = ""
|
||||
|
||||
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 += " "
|
||||
for arg in method.in_args:
|
||||
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
for arg in method.out_args:
|
||||
self.add_line(f":ret {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
self.add_line("")
|
||||
for line in prepare_docstring("\n" + method.doc_string):
|
||||
self.add_line(line)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
def add_signal(self, signal):
|
||||
self.add_line(f".. dbus:signal:: {signal.name}")
|
||||
self.add_line("")
|
||||
self.indent += " "
|
||||
for arg in signal.args:
|
||||
self.add_line(f":arg {arg.signature} {arg.name}: {arg.doc_string}")
|
||||
self.add_line("")
|
||||
for line in prepare_docstring("\n" + signal.doc_string):
|
||||
self.add_line(line)
|
||||
self.indent = self.indent[:-3]
|
||||
|
||||
def add_property(self, prop):
|
||||
self.add_line(f".. dbus:property:: {prop.name}")
|
||||
self.indent += " "
|
||||
self.add_line(f":type: {prop.signature}")
|
||||
access = {"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 = self.indent[:-3]
|
||||
|
||||
def add_interface(self, iface):
|
||||
self.add_line(f".. dbus:interface:: {iface.name}")
|
||||
self.add_line("")
|
||||
self.indent += " "
|
||||
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 = 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 = nodes.paragraph()
|
||||
node.document = 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 = True
|
||||
required_arguments = 1
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = True
|
||||
|
||||
def run(self):
|
||||
reporter = self.state.document.reporter
|
||||
|
||||
try:
|
||||
source, lineno = reporter.get_source_and_line(self.lineno) # type: ignore
|
||||
except AttributeError:
|
||||
source, lineno = (None, None)
|
||||
|
||||
logger.debug("[dbusdoc] %s:%s: input:\n%s", source, lineno, self.block_text)
|
||||
|
||||
env = self.state.document.settings.env
|
||||
dbusfile = env.config.qapidoc_srctree + "/" + self.arguments[0]
|
||||
with open(dbusfile, "rb") as f:
|
||||
xml_data = f.read()
|
||||
xml = parse_dbus_xml(xml_data)
|
||||
doc = DBusDoc(self, dbusfile)
|
||||
for iface in xml:
|
||||
doc.add_interface(iface)
|
||||
|
||||
result = 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=__version__, parallel_read_safe=True, parallel_write_safe=True)
|
|
@ -0,0 +1,406 @@
|
|||
# D-Bus sphinx domain extension
|
||||
#
|
||||
# Copyright (C) 2021, Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.nodes import Element, Node
|
||||
from docutils.parsers.rst import directives
|
||||
from sphinx import addnodes
|
||||
from sphinx.addnodes import desc_signature, pending_xref
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.domains import Domain, Index, IndexEntry, ObjType
|
||||
from sphinx.locale import _
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.util import nodes as node_utils
|
||||
from sphinx.util.docfields import Field, TypedField
|
||||
from sphinx.util.typing import OptionSpec
|
||||
|
||||
|
||||
class DBusDescription(ObjectDescription[str]):
|
||||
"""Base class for DBus objects"""
|
||||
|
||||
option_spec: OptionSpec = ObjectDescription.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"deprecated": directives.flag,
|
||||
}
|
||||
)
|
||||
|
||||
def get_index_text(self, modname: str, name: str) -> str:
|
||||
"""Return the text for the index entry of the object."""
|
||||
raise NotImplementedError("must be implemented in subclasses")
|
||||
|
||||
def add_target_and_index(
|
||||
self, name: str, sig: str, signode: desc_signature
|
||||
) -> None:
|
||||
ifacename = self.env.ref_context.get("dbus:interface")
|
||||
node_id = name
|
||||
if ifacename:
|
||||
node_id = f"{ifacename}.{node_id}"
|
||||
|
||||
signode["names"].append(name)
|
||||
signode["ids"].append(node_id)
|
||||
|
||||
if "noindexentry" not in self.options:
|
||||
indextext = self.get_index_text(ifacename, name)
|
||||
if indextext:
|
||||
self.indexnode["entries"].append(
|
||||
("single", indextext, node_id, "", None)
|
||||
)
|
||||
|
||||
domain = cast(DBusDomain, self.env.get_domain("dbus"))
|
||||
domain.note_object(name, self.objtype, node_id, location=signode)
|
||||
|
||||
|
||||
class DBusInterface(DBusDescription):
|
||||
"""
|
||||
Implementation of ``dbus:interface``.
|
||||
"""
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return ifacename
|
||||
|
||||
def before_content(self) -> None:
|
||||
self.env.ref_context["dbus:interface"] = self.arguments[0]
|
||||
|
||||
def after_content(self) -> None:
|
||||
self.env.ref_context.pop("dbus:interface")
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
signode += addnodes.desc_annotation("interface ", "interface ")
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
return sig
|
||||
|
||||
def run(self) -> List[Node]:
|
||||
_, node = super().run()
|
||||
name = self.arguments[0]
|
||||
section = nodes.section(ids=[name + "-section"])
|
||||
section += nodes.title(name, "%s interface" % name)
|
||||
section += node
|
||||
return [self.indexnode, section]
|
||||
|
||||
|
||||
class DBusMember(DBusDescription):
|
||||
|
||||
signal = False
|
||||
|
||||
|
||||
class DBusMethod(DBusMember):
|
||||
"""
|
||||
Implementation of ``dbus:method``.
|
||||
"""
|
||||
|
||||
option_spec: OptionSpec = DBusMember.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"noreply": directives.flag,
|
||||
}
|
||||
)
|
||||
|
||||
doc_field_types: List[Field] = [
|
||||
TypedField(
|
||||
"arg",
|
||||
label=_("Arguments"),
|
||||
names=("arg",),
|
||||
rolename="arg",
|
||||
typerolename=None,
|
||||
typenames=("argtype", "type"),
|
||||
),
|
||||
TypedField(
|
||||
"ret",
|
||||
label=_("Returns"),
|
||||
names=("ret",),
|
||||
rolename="ret",
|
||||
typerolename=None,
|
||||
typenames=("rettype", "type"),
|
||||
),
|
||||
]
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s() (%s method)") % (name, ifacename)
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
params = addnodes.desc_parameterlist()
|
||||
returns = addnodes.desc_parameterlist()
|
||||
|
||||
contentnode = addnodes.desc_content()
|
||||
self.state.nested_parse(self.content, self.content_offset, contentnode)
|
||||
for child in contentnode:
|
||||
if isinstance(child, nodes.field_list):
|
||||
for field in child:
|
||||
ty, sg, name = field[0].astext().split(None, 2)
|
||||
param = addnodes.desc_parameter()
|
||||
param += addnodes.desc_sig_keyword_type(sg, sg)
|
||||
param += addnodes.desc_sig_space()
|
||||
param += addnodes.desc_sig_name(name, name)
|
||||
if ty == "arg":
|
||||
params += param
|
||||
elif ty == "ret":
|
||||
returns += param
|
||||
|
||||
anno = "signal " if self.signal else "method "
|
||||
signode += addnodes.desc_annotation(anno, anno)
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
signode += params
|
||||
if not self.signal and "noreply" not in self.options:
|
||||
ret = addnodes.desc_returns()
|
||||
ret += returns
|
||||
signode += ret
|
||||
|
||||
return sig
|
||||
|
||||
|
||||
class DBusSignal(DBusMethod):
|
||||
"""
|
||||
Implementation of ``dbus:signal``.
|
||||
"""
|
||||
|
||||
doc_field_types: List[Field] = [
|
||||
TypedField(
|
||||
"arg",
|
||||
label=_("Arguments"),
|
||||
names=("arg",),
|
||||
rolename="arg",
|
||||
typerolename=None,
|
||||
typenames=("argtype", "type"),
|
||||
),
|
||||
]
|
||||
signal = True
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s() (%s signal)") % (name, ifacename)
|
||||
|
||||
|
||||
class DBusProperty(DBusMember):
|
||||
"""
|
||||
Implementation of ``dbus:property``.
|
||||
"""
|
||||
|
||||
option_spec: OptionSpec = DBusMember.option_spec.copy()
|
||||
option_spec.update(
|
||||
{
|
||||
"type": directives.unchanged,
|
||||
"readonly": directives.flag,
|
||||
"writeonly": directives.flag,
|
||||
"readwrite": directives.flag,
|
||||
"emits-changed": directives.unchanged,
|
||||
}
|
||||
)
|
||||
|
||||
doc_field_types: List[Field] = []
|
||||
|
||||
def get_index_text(self, ifacename: str, name: str) -> str:
|
||||
return _("%s (%s property)") % (name, ifacename)
|
||||
|
||||
def transform_content(self, contentnode: addnodes.desc_content) -> None:
|
||||
fieldlist = nodes.field_list()
|
||||
access = None
|
||||
if "readonly" in self.options:
|
||||
access = _("read-only")
|
||||
if "writeonly" in self.options:
|
||||
access = _("write-only")
|
||||
if "readwrite" in self.options:
|
||||
access = _("read & write")
|
||||
if access:
|
||||
content = nodes.Text(access)
|
||||
fieldname = nodes.field_name("", _("Access"))
|
||||
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
|
||||
field = nodes.field("", fieldname, fieldbody)
|
||||
fieldlist += field
|
||||
emits = self.options.get("emits-changed", None)
|
||||
if emits:
|
||||
content = nodes.Text(emits)
|
||||
fieldname = nodes.field_name("", _("Emits Changed"))
|
||||
fieldbody = nodes.field_body("", nodes.paragraph("", "", content))
|
||||
field = nodes.field("", fieldname, fieldbody)
|
||||
fieldlist += field
|
||||
if len(fieldlist) > 0:
|
||||
contentnode.insert(0, fieldlist)
|
||||
|
||||
def handle_signature(self, sig: str, signode: desc_signature) -> str:
|
||||
contentnode = addnodes.desc_content()
|
||||
self.state.nested_parse(self.content, self.content_offset, contentnode)
|
||||
ty = self.options.get("type")
|
||||
|
||||
signode += addnodes.desc_annotation("property ", "property ")
|
||||
signode += addnodes.desc_name(sig, sig)
|
||||
signode += addnodes.desc_sig_punctuation("", ":")
|
||||
signode += addnodes.desc_sig_keyword_type(ty, ty)
|
||||
return sig
|
||||
|
||||
def run(self) -> List[Node]:
|
||||
self.name = "dbus:member"
|
||||
return super().run()
|
||||
|
||||
|
||||
class DBusXRef(XRefRole):
|
||||
def process_link(self, env, refnode, has_explicit_title, title, target):
|
||||
refnode["dbus:interface"] = env.ref_context.get("dbus:interface")
|
||||
if not has_explicit_title:
|
||||
title = title.lstrip(".") # only has a meaning for the target
|
||||
target = target.lstrip("~") # only has a meaning for the title
|
||||
# if the first character is a tilde, don't display the module/class
|
||||
# parts of the contents
|
||||
if title[0:1] == "~":
|
||||
title = title[1:]
|
||||
dot = title.rfind(".")
|
||||
if dot != -1:
|
||||
title = title[dot + 1 :]
|
||||
# if the first character is a dot, search more specific namespaces first
|
||||
# else search builtins first
|
||||
if target[0:1] == ".":
|
||||
target = target[1:]
|
||||
refnode["refspecific"] = True
|
||||
return title, target
|
||||
|
||||
|
||||
class DBusIndex(Index):
|
||||
"""
|
||||
Index subclass to provide a D-Bus interfaces index.
|
||||
"""
|
||||
|
||||
name = "dbusindex"
|
||||
localname = _("D-Bus Interfaces Index")
|
||||
shortname = _("dbus")
|
||||
|
||||
def generate(
|
||||
self, docnames: Iterable[str] = None
|
||||
) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
|
||||
content: Dict[str, List[IndexEntry]] = {}
|
||||
# list of prefixes to ignore
|
||||
ignores: List[str] = self.domain.env.config["dbus_index_common_prefix"]
|
||||
ignores = sorted(ignores, key=len, reverse=True)
|
||||
|
||||
ifaces = sorted(
|
||||
[
|
||||
x
|
||||
for x in self.domain.data["objects"].items()
|
||||
if x[1].objtype == "interface"
|
||||
],
|
||||
key=lambda x: x[0].lower(),
|
||||
)
|
||||
for name, (docname, node_id, _) in ifaces:
|
||||
if docnames and docname not in docnames:
|
||||
continue
|
||||
|
||||
for ignore in ignores:
|
||||
if name.startswith(ignore):
|
||||
name = name[len(ignore) :]
|
||||
stripped = ignore
|
||||
break
|
||||
else:
|
||||
stripped = ""
|
||||
|
||||
entries = content.setdefault(name[0].lower(), [])
|
||||
entries.append(IndexEntry(stripped + name, 0, docname, node_id, "", "", ""))
|
||||
|
||||
# sort by first letter
|
||||
sorted_content = sorted(content.items())
|
||||
|
||||
return sorted_content, False
|
||||
|
||||
|
||||
class ObjectEntry(NamedTuple):
|
||||
docname: str
|
||||
node_id: str
|
||||
objtype: str
|
||||
|
||||
|
||||
class DBusDomain(Domain):
|
||||
"""
|
||||
Implementation of the D-Bus domain.
|
||||
"""
|
||||
|
||||
name = "dbus"
|
||||
label = "D-Bus"
|
||||
object_types: Dict[str, ObjType] = {
|
||||
"interface": ObjType(_("interface"), "iface", "obj"),
|
||||
"method": ObjType(_("method"), "meth", "obj"),
|
||||
"signal": ObjType(_("signal"), "sig", "obj"),
|
||||
"property": ObjType(_("property"), "attr", "_prop", "obj"),
|
||||
}
|
||||
directives = {
|
||||
"interface": DBusInterface,
|
||||
"method": DBusMethod,
|
||||
"signal": DBusSignal,
|
||||
"property": DBusProperty,
|
||||
}
|
||||
roles = {
|
||||
"iface": DBusXRef(),
|
||||
"meth": DBusXRef(),
|
||||
"sig": DBusXRef(),
|
||||
"prop": DBusXRef(),
|
||||
}
|
||||
initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
|
||||
"objects": {}, # fullname -> ObjectEntry
|
||||
}
|
||||
indices = [
|
||||
DBusIndex,
|
||||
]
|
||||
|
||||
@property
|
||||
def objects(self) -> Dict[str, ObjectEntry]:
|
||||
return self.data.setdefault("objects", {}) # fullname -> ObjectEntry
|
||||
|
||||
def note_object(
|
||||
self, name: str, objtype: str, node_id: str, location: Any = None
|
||||
) -> None:
|
||||
self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype)
|
||||
|
||||
def clear_doc(self, docname: str) -> None:
|
||||
for fullname, obj in list(self.objects.items()):
|
||||
if obj.docname == docname:
|
||||
del self.objects[fullname]
|
||||
|
||||
def find_obj(self, typ: str, name: str) -> Optional[Tuple[str, ObjectEntry]]:
|
||||
# skip parens
|
||||
if name[-2:] == "()":
|
||||
name = name[:-2]
|
||||
if typ in ("meth", "sig", "prop"):
|
||||
try:
|
||||
ifacename, name = name.rsplit(".", 1)
|
||||
except ValueError:
|
||||
pass
|
||||
return self.objects.get(name)
|
||||
|
||||
def resolve_xref(
|
||||
self,
|
||||
env: "BuildEnvironment",
|
||||
fromdocname: str,
|
||||
builder: "Builder",
|
||||
typ: str,
|
||||
target: str,
|
||||
node: pending_xref,
|
||||
contnode: Element,
|
||||
) -> Optional[Element]:
|
||||
"""Resolve the pending_xref *node* with the given *typ* and *target*."""
|
||||
objdef = self.find_obj(typ, target)
|
||||
if objdef:
|
||||
return node_utils.make_refnode(
|
||||
builder, fromdocname, objdef.docname, objdef.node_id, contnode
|
||||
)
|
||||
|
||||
def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
|
||||
for refname, obj in self.objects.items():
|
||||
yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_domain(DBusDomain)
|
||||
app.add_config_value("dbus_index_common_prefix", [], "env")
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Author: David Zeuthen <davidz@redhat.com>
|
||||
|
||||
import xml.parsers.expat
|
||||
|
||||
|
||||
class Annotation:
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.annotations = []
|
||||
self.since = ""
|
||||
|
||||
|
||||
class Arg:
|
||||
def __init__(self, name, signature):
|
||||
self.name = name
|
||||
self.signature = signature
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
|
||||
|
||||
class Method:
|
||||
def __init__(self, name, h_type_implies_unix_fd=True):
|
||||
self.name = name
|
||||
self.h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||
self.in_args = []
|
||||
self.out_args = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
self.unix_fd = False
|
||||
|
||||
|
||||
class Signal:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.args = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
|
||||
|
||||
class Property:
|
||||
def __init__(self, name, signature, access):
|
||||
self.name = name
|
||||
self.signature = signature
|
||||
self.access = access
|
||||
self.annotations = []
|
||||
self.arg = Arg("value", self.signature)
|
||||
self.arg.annotations = self.annotations
|
||||
self.readable = False
|
||||
self.writable = False
|
||||
if self.access == "readwrite":
|
||||
self.readable = True
|
||||
self.writable = True
|
||||
elif self.access == "read":
|
||||
self.readable = True
|
||||
elif self.access == "write":
|
||||
self.writable = True
|
||||
else:
|
||||
raise ValueError('Invalid access type "{}"'.format(self.access))
|
||||
self.doc_string = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
self.emits_changed_signal = True
|
||||
|
||||
|
||||
class Interface:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.methods = []
|
||||
self.signals = []
|
||||
self.properties = []
|
||||
self.annotations = []
|
||||
self.doc_string = ""
|
||||
self.doc_string_brief = ""
|
||||
self.since = ""
|
||||
self.deprecated = False
|
||||
|
||||
|
||||
class DBusXMLParser:
|
||||
STATE_TOP = "top"
|
||||
STATE_NODE = "node"
|
||||
STATE_INTERFACE = "interface"
|
||||
STATE_METHOD = "method"
|
||||
STATE_SIGNAL = "signal"
|
||||
STATE_PROPERTY = "property"
|
||||
STATE_ARG = "arg"
|
||||
STATE_ANNOTATION = "annotation"
|
||||
STATE_IGNORED = "ignored"
|
||||
|
||||
def __init__(self, xml_data, h_type_implies_unix_fd=True):
|
||||
self._parser = xml.parsers.expat.ParserCreate()
|
||||
self._parser.CommentHandler = self.handle_comment
|
||||
self._parser.CharacterDataHandler = self.handle_char_data
|
||||
self._parser.StartElementHandler = self.handle_start_element
|
||||
self._parser.EndElementHandler = self.handle_end_element
|
||||
|
||||
self.parsed_interfaces = []
|
||||
self._cur_object = None
|
||||
|
||||
self.state = DBusXMLParser.STATE_TOP
|
||||
self.state_stack = []
|
||||
self._cur_object = None
|
||||
self._cur_object_stack = []
|
||||
|
||||
self.doc_comment_last_symbol = ""
|
||||
|
||||
self._h_type_implies_unix_fd = h_type_implies_unix_fd
|
||||
|
||||
self._parser.Parse(xml_data)
|
||||
|
||||
COMMENT_STATE_BEGIN = "begin"
|
||||
COMMENT_STATE_PARAMS = "params"
|
||||
COMMENT_STATE_BODY = "body"
|
||||
COMMENT_STATE_SKIP = "skip"
|
||||
|
||||
def handle_comment(self, data):
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BEGIN
|
||||
lines = data.split("\n")
|
||||
symbol = ""
|
||||
body = ""
|
||||
in_para = False
|
||||
params = {}
|
||||
for line in lines:
|
||||
orig_line = line
|
||||
line = line.lstrip()
|
||||
if comment_state == DBusXMLParser.COMMENT_STATE_BEGIN:
|
||||
if len(line) > 0:
|
||||
colon_index = line.find(": ")
|
||||
if colon_index == -1:
|
||||
if line.endswith(":"):
|
||||
symbol = line[0 : len(line) - 1]
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||
else:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_SKIP
|
||||
else:
|
||||
symbol = line[0:colon_index]
|
||||
rest_of_line = line[colon_index + 2 :].strip()
|
||||
if len(rest_of_line) > 0:
|
||||
body += rest_of_line + "\n"
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_PARAMS
|
||||
elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS:
|
||||
if line.startswith("@"):
|
||||
colon_index = line.find(": ")
|
||||
if colon_index == -1:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
else:
|
||||
param = line[1:colon_index]
|
||||
docs = line[colon_index + 2 :]
|
||||
params[param] = docs
|
||||
else:
|
||||
comment_state = DBusXMLParser.COMMENT_STATE_BODY
|
||||
if len(line) > 0:
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
elif comment_state == DBusXMLParser.COMMENT_STATE_BODY:
|
||||
if len(line) > 0:
|
||||
if not in_para:
|
||||
in_para = True
|
||||
body += orig_line + "\n"
|
||||
else:
|
||||
if in_para:
|
||||
body += "\n"
|
||||
in_para = False
|
||||
if in_para:
|
||||
body += "\n"
|
||||
|
||||
if symbol != "":
|
||||
self.doc_comment_last_symbol = symbol
|
||||
self.doc_comment_params = params
|
||||
self.doc_comment_body = body
|
||||
|
||||
def handle_char_data(self, data):
|
||||
# print 'char_data=%s'%data
|
||||
pass
|
||||
|
||||
def handle_start_element(self, name, attrs):
|
||||
old_state = self.state
|
||||
old_cur_object = self._cur_object
|
||||
if self.state == DBusXMLParser.STATE_IGNORED:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
elif self.state == DBusXMLParser.STATE_TOP:
|
||||
if name == DBusXMLParser.STATE_NODE:
|
||||
self.state = DBusXMLParser.STATE_NODE
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
elif self.state == DBusXMLParser.STATE_NODE:
|
||||
if name == DBusXMLParser.STATE_INTERFACE:
|
||||
self.state = DBusXMLParser.STATE_INTERFACE
|
||||
iface = Interface(attrs["name"])
|
||||
self._cur_object = iface
|
||||
self.parsed_interfaces.append(iface)
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||
self._cur_object.doc_string = self.doc_comment_body
|
||||
if "short_description" in self.doc_comment_params:
|
||||
short_description = self.doc_comment_params["short_description"]
|
||||
self._cur_object.doc_string_brief = short_description
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_INTERFACE:
|
||||
if name == DBusXMLParser.STATE_METHOD:
|
||||
self.state = DBusXMLParser.STATE_METHOD
|
||||
method = Method(
|
||||
attrs["name"], h_type_implies_unix_fd=self._h_type_implies_unix_fd
|
||||
)
|
||||
self._cur_object.methods.append(method)
|
||||
self._cur_object = method
|
||||
elif name == DBusXMLParser.STATE_SIGNAL:
|
||||
self.state = DBusXMLParser.STATE_SIGNAL
|
||||
signal = Signal(attrs["name"])
|
||||
self._cur_object.signals.append(signal)
|
||||
self._cur_object = signal
|
||||
elif name == DBusXMLParser.STATE_PROPERTY:
|
||||
self.state = DBusXMLParser.STATE_PROPERTY
|
||||
prop = Property(attrs["name"], attrs["type"], attrs["access"])
|
||||
self._cur_object.properties.append(prop)
|
||||
self._cur_object = prop
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if "name" in attrs and self.doc_comment_last_symbol == attrs["name"]:
|
||||
self._cur_object.doc_string = self.doc_comment_body
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params["since"].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_METHOD:
|
||||
if name == DBusXMLParser.STATE_ARG:
|
||||
self.state = DBusXMLParser.STATE_ARG
|
||||
arg_name = None
|
||||
if "name" in attrs:
|
||||
arg_name = attrs["name"]
|
||||
arg = Arg(arg_name, attrs["type"])
|
||||
direction = attrs.get("direction", "in")
|
||||
if direction == "in":
|
||||
self._cur_object.in_args.append(arg)
|
||||
elif direction == "out":
|
||||
self._cur_object.out_args.append(arg)
|
||||
else:
|
||||
raise ValueError('Invalid direction "{}"'.format(direction))
|
||||
self._cur_object = arg
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||
doc_string = self.doc_comment_params[attrs["name"]]
|
||||
if doc_string is not None:
|
||||
self._cur_object.doc_string = doc_string
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params[
|
||||
"since"
|
||||
].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_SIGNAL:
|
||||
if name == DBusXMLParser.STATE_ARG:
|
||||
self.state = DBusXMLParser.STATE_ARG
|
||||
arg_name = None
|
||||
if "name" in attrs:
|
||||
arg_name = attrs["name"]
|
||||
arg = Arg(arg_name, attrs["type"])
|
||||
self._cur_object.args.append(arg)
|
||||
self._cur_object = arg
|
||||
elif name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
# assign docs, if any
|
||||
if self.doc_comment_last_symbol == old_cur_object.name:
|
||||
if "name" in attrs and attrs["name"] in self.doc_comment_params:
|
||||
doc_string = self.doc_comment_params[attrs["name"]]
|
||||
if doc_string is not None:
|
||||
self._cur_object.doc_string = doc_string
|
||||
if "since" in self.doc_comment_params:
|
||||
self._cur_object.since = self.doc_comment_params[
|
||||
"since"
|
||||
].strip()
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_PROPERTY:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_ARG:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = DBusXMLParser.STATE_IGNORED
|
||||
|
||||
elif self.state == DBusXMLParser.STATE_ANNOTATION:
|
||||
if name == DBusXMLParser.STATE_ANNOTATION:
|
||||
self.state = DBusXMLParser.STATE_ANNOTATION
|
||||
anno = Annotation(attrs["name"], attrs["value"])
|
||||
self._cur_object.annotations.append(anno)
|
||||
self._cur_object = anno
|
||||
else:
|
||||
self.state = 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 = self.state_stack.pop()
|
||||
self._cur_object = self._cur_object_stack.pop()
|
||||
|
||||
|
||||
def parse_dbus_xml(xml_data):
|
||||
parser = DBusXMLParser(xml_data, True)
|
||||
return parser.parsed_interfaces
|
|
@ -0,0 +1,25 @@
|
|||
# D-Bus XML documentation extension, compatibility gunk for <sphinx4
|
||||
#
|
||||
# Copyright (C) 2021, Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#
|
||||
# Author: Marc-André Lureau <marcandre.lureau@redhat.com>
|
||||
"""dbus-doc is a Sphinx extension that provides documentation from D-Bus XML."""
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class FakeDBusDocDirective(SphinxDirective):
|
||||
has_content = True
|
||||
required_arguments = 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)
|
Loading…
Reference in New Issue