Browse Source
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>pull/202/head
5 changed files with 978 additions and 0 deletions
@ -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