diff --git a/docs/reference/gio/gdbus-codegen.xml b/docs/reference/gio/gdbus-codegen.xml index 960b5ffa0..5860fed97 100644 --- a/docs/reference/gio/gdbus-codegen.xml +++ b/docs/reference/gio/gdbus-codegen.xml @@ -35,6 +35,7 @@ none|objects|all OUTDIR OUTFILES + OUTFILES FILE @@ -168,6 +169,16 @@ + + Generating reStructuredText documentation + + Each generated reStructuredText file (see the + option for details) is a plain text + reStructuredText + document describing the D-Bus interface. + + + Options @@ -212,8 +223,25 @@ Generate Docbook Documentation for each D-Bus interface and - put it in OUTFILES-NAME.xml where - NAME is a place-holder for the interface + put it in OUTFILES-NAME.xml + where NAME is a place-holder for the interface + name, e.g. net.Corp.FooBar and so on. + + + Pass to specify the directory + to put the output files in. By default the current directory + will be used. + + + + + + OUTFILES + + + Generate reStructuredText Documentation for each D-Bus interface and + put it in OUTFILES-NAME.rst + where NAME is a place-holder for the interface name, e.g. net.Corp.FooBar and so on. diff --git a/gio/gdbus-2.0/codegen/codegen_docbook.py b/gio/gdbus-2.0/codegen/codegen_docbook.py index 4b69e2927..b8d683408 100644 --- a/gio/gdbus-2.0/codegen/codegen_docbook.py +++ b/gio/gdbus-2.0/codegen/codegen_docbook.py @@ -345,9 +345,17 @@ class DocbookCodeGenerator: def expand_paras(self, s, expandParamsAndConstants): s = self.expand(s, expandParamsAndConstants).strip() + res = [] if not s.startswith("") + for line in s.split("\n"): + line = line.strip() + if not line: + line = "" + res.append(line) + if not s.endswith(""): + res.append("") + return "\n".join(res) def generate_expand_dicts(self): self.expand_member_dict = {} diff --git a/gio/gdbus-2.0/codegen/codegen_main.py b/gio/gdbus-2.0/codegen/codegen_main.py index 238d7dd12..194800c78 100644 --- a/gio/gdbus-2.0/codegen/codegen_main.py +++ b/gio/gdbus-2.0/codegen/codegen_main.py @@ -30,6 +30,7 @@ from . import dbustypes from . import parser from . import codegen from . import codegen_docbook +from . import codegen_rst from .utils import print_error, print_warning @@ -211,6 +212,11 @@ def codegen_main(): metavar="OUTFILES", help="Generate Docbook in OUTFILES-org.Project.IFace.xml", ) + arg_parser.add_argument( + "--generate-rst", + metavar="OUTFILES", + help="Generate reStructuredText in OUTFILES-org.Project.IFace.rst", + ) arg_parser.add_argument( "--pragma-once", action="store_true", @@ -287,10 +293,12 @@ def codegen_main(): ) if ( - args.generate_c_code is not None or args.generate_docbook is not None + args.generate_c_code is not None + or args.generate_docbook is not None + or args.generate_rst is not None ) and args.output is not None: print_error( - "Using --generate-c-code or --generate-docbook and " + "Using --generate-c-code or --generate-docbook or --generate-rst and " "--output at the same time is not allowed" ) @@ -420,6 +428,11 @@ def codegen_main(): if docbook: docbook_gen.generate(docbook, args.output_directory) + rst = args.generate_rst + rst_gen = codegen_rst.RstCodeGenerator(all_ifaces) + if rst: + rst_gen.generate(rst, args.output_directory) + if args.header: with open(h_file, "w") as outfile: gen = codegen.HeaderCodeGenerator( diff --git a/gio/gdbus-2.0/codegen/codegen_rst.py b/gio/gdbus-2.0/codegen/codegen_rst.py new file mode 100644 index 000000000..51da2d572 --- /dev/null +++ b/gio/gdbus-2.0/codegen/codegen_rst.py @@ -0,0 +1,332 @@ +# SPDX-FileCopyrightText: 2022 Emmanuele Bassi +# +# SPDX-License-Identifier: LGPL-2.1-or-later + +import os +import re + +from . import utils + +# Disable line length warnings as wrapping the templates would be hard +# flake8: noqa: E501 + + +class RstCodeGenerator: + """Generates documentation in reStructuredText format.""" + + def __init__(self, ifaces): + self.ifaces = ifaces + self._generate_expand_dicts() + + def _expand(self, s, expandParamsAndConstants): + """Expands parameters and constant literals.""" + res = [] + for line in s.split("\n"): + line = line.strip() + if line == "": + res.append("") + continue + for key in self._expand_member_dict_keys: + line = line.replace(key, self._expand_member_dict[key]) + for key in self._expand_iface_dict_keys: + line = line.replace(key, self._expand_iface_dict[key]) + if expandParamsAndConstants: + # replace @foo with ``foo`` + line = re.sub( + "@[a-zA-Z0-9_]*", + lambda m: "``" + m.group(0)[1:] + "``", + line, + ) + # replace e.g. %TRUE with ``TRUE`` + line = re.sub( + "%[a-zA-Z0-9_]*", + lambda m: "``" + m.group(0)[1:] + "``", + line, + ) + res.append(line) + return "\n".join(res) + + def _generate_expand_dicts(self): + """Generates the dictionaries used to expand gtk-doc sigils.""" + self._expand_member_dict = {} + self._expand_iface_dict = {} + for i in self.ifaces: + key = f"#{i.name}" + value = f"`{i.name}`_" + self._expand_iface_dict[key] = value + + for m in i.methods: + key = "%s.%s()" % (i.name, m.name) + value = f"`{i.name}.{m.name}`_" + self._expand_member_dict[key] = value + + for s in i.signals: + key = "#%s::%s" % (i.name, s.name) + value = f"`{i.name}::{s.name}`_" + self._expand_member_dict[key] = value + + for p in i.properties: + key = "#%s:%s" % (i.name, p.name) + value = f"`{i.name}:{p.name}`_" + self._expand_member_dict[key] = value + + # Make sure to expand the keys in reverse order so e.g. #org.foo.Iface:MediaCompat + # is evaluated before #org.foo.Iface:Media ... + self._expand_member_dict_keys = sorted( + self._expand_member_dict.keys(), reverse=True + ) + self._expand_iface_dict_keys = sorted( + self._expand_iface_dict.keys(), reverse=True + ) + + def _generate_header(self, iface): + """Generates the header and preamble of the document.""" + header_len = len(iface.name) + res = [ + f".. _{iface.name}:", + "", + "=" * header_len, + iface.name, + "=" * header_len, + "", + "-----------", + "Description", + "-----------", + "", + f".. _{iface.name} Description:", + "", + iface.doc_string_brief.strip(), + "", + self._expand(iface.doc_string, True), + "", + ] + if iface.since: + res += [ + f"Interface available since: {iface.since}.", + "", + ] + if iface.deprecated: + res += [ + ".. warning::", + "", + " This interface is deprecated.", + "", + "", + ] + res += [""] + return "\n".join(res) + + def _generate_section(self, title, name): + """Generates a section with the given title.""" + res = [ + "-" * len(title), + title, + "-" * len(title), + "", + f".. {name} {title}:", + "", + "", + ] + return "\n".join(res) + + def _generate_properties(self, iface): + """Generates the properties section.""" + res = [] + for p in iface.properties: + title = f"{iface.name}:{p.name}" + if p.readable and p.writable: + access = "readwrite" + elif p.writable: + access = "writable" + else: + access = "readable" + res += [ + title, + "^" * len(title), + "", + "::", + "", + f" {p.name} {access} {p.signature}", + "", + "", + self._expand(p.doc_string, True), + "", + ] + if p.since: + res += [ + f"Property available since: {p.since}.", + "", + ] + if p.deprecated: + res += [ + ".. warning::", + "", + " This property is deprecated.", + "", + "", + ] + res += [""] + return "\n".join(res) + + def _generate_method_signature(self, method): + """Generates the method signature as a code block.""" + res = [ + "::", + "", + ] + n_in_args = len(method.in_args) + n_out_args = len(method.out_args) + if n_in_args == 0 and n_out_args == 0: + res += [ + f" {method.name} ()", + ] + else: + res += [ + f" {method.name} (", + ] + for idx, arg in enumerate(method.in_args): + if idx == n_in_args - 1 and n_out_args == 0: + res += [ + f" IN {arg.name} {arg.signature}", + ] + else: + res += [ + f" IN {arg.name} {arg.signature},", + ] + for idx, arg in enumerate(method.out_args): + if idx == n_out_args - 1: + res += [ + f" OUT {arg.name} {arg.signature}", + ] + else: + res += [ + f" OUT {arg.name} {arg.signature},", + ] + res += [ + " )", + "", + ] + res += [""] + return "\n".join(res) + + def _generate_methods(self, iface): + """Generates the methods section.""" + res = [] + for m in iface.methods: + title = f"{iface.name}.{m.name}" + res += [ + title, + "^" * len(title), + "", + self._generate_method_signature(m), + "", + self._expand(m.doc_string, True), + "", + ] + for a in m.in_args: + arg_desc = self._expand(a.doc_string, True) + res += [ + f"{a.name}", + f" {arg_desc}", + "", + ] + res += [""] + if m.since: + res += [ + f"Method available since: {m.since}.", + "", + ] + if m.deprecated: + res += [ + ".. warning::", + "", + " This method is deprecated.", + "", + "", + ] + res += [""] + return "\n".join(res) + + def _generate_signal_signature(self, signal): + """Generates the signal signature.""" + res = [ + "::", + "", + ] + n_args = len(signal.args) + if n_args == 0: + res += [ + f" {signal.name} ()", + ] + else: + res += [ + f" {signal.name} (", + ] + for idx, arg in enumerate(signal.args): + if idx == n_args - 1: + res += [ + f" {arg.name} {arg.signature}", + ] + else: + res += [ + f" {arg.name} {arg.signature},", + ] + res += [ + " )", + "", + ] + res += [""] + return "\n".join(res) + + def _generate_signals(self, iface): + """Generates the signals section.""" + res = [] + for s in iface.signals: + title = f"{iface.name}::{s.name}" + res += [ + title, + "^" * len(title), + "", + self._generate_signal_signature(s), + "", + self._expand(s.doc_string, True), + "", + ] + for a in s.args: + arg_desc = self._expand(a.doc_string, True) + res += [ + f"{a.name}", + f" {arg_desc}", + "", + ] + res += [""] + if s.since: + res += [ + f"Signal available since: {s.since}.", + "", + ] + if s.deprecated: + res += [ + ".. warning::", + "", + " This signal is deprecated.", + "", + "", + ] + res += [""] + return "\n".join(res) + + def generate(self, rst, outdir): + """Generates the reStructuredText file for each interface.""" + for i in self.ifaces: + with open(os.path.join(outdir, f"{rst}-{i.name}.rst"), "w") as outfile: + outfile.write(self._generate_header(i)) + if len(i.properties) > 0: + outfile.write(self._generate_section("Properties", i.name)) + outfile.write(self._generate_properties(i)) + if len(i.methods) > 0: + outfile.write(self._generate_section("Methods", i.name)) + outfile.write(self._generate_methods(i)) + if len(i.signals) > 0: + outfile.write(self._generate_section("Signals", i.name)) + outfile.write(self._generate_signals(i)) diff --git a/gio/gdbus-2.0/codegen/meson.build b/gio/gdbus-2.0/codegen/meson.build index c0caf0e50..bf25cdaeb 100644 --- a/gio/gdbus-2.0/codegen/meson.build +++ b/gio/gdbus-2.0/codegen/meson.build @@ -3,6 +3,7 @@ gdbus_codegen_files = [ 'codegen.py', 'codegen_main.py', 'codegen_docbook.py', + 'codegen_rst.py', 'dbustypes.py', 'parser.py', 'utils.py', diff --git a/gio/gdbus-2.0/codegen/parser.py b/gio/gdbus-2.0/codegen/parser.py index 45226d540..cf8ea5229 100644 --- a/gio/gdbus-2.0/codegen/parser.py +++ b/gio/gdbus-2.0/codegen/parser.py @@ -85,7 +85,7 @@ class DBusXMLParser: symbol = line[0:colon_index] rest_of_line = line[colon_index + 2 :].strip() if len(rest_of_line) > 0: - body += "" + rest_of_line + "" + body += f"{rest_of_line}\n" comment_state = DBusXMLParser.COMMENT_STATE_PARAMS elif comment_state == DBusXMLParser.COMMENT_STATE_PARAMS: if line.startswith("@"): @@ -93,9 +93,9 @@ class DBusXMLParser: if colon_index == -1: comment_state = DBusXMLParser.COMMENT_STATE_BODY if not in_para: - body += "" + body += "\n" in_para = True - body += orig_line + "\n" + body += f"{orig_line}\n" else: param = line[1:colon_index] docs = line[colon_index + 2 :] @@ -104,21 +104,20 @@ class DBusXMLParser: comment_state = DBusXMLParser.COMMENT_STATE_BODY if len(line) > 0: if not in_para: - body += "" + body += "\n" in_para = True body += orig_line + "\n" elif comment_state == DBusXMLParser.COMMENT_STATE_BODY: if len(line) > 0: if not in_para: - body += "" in_para = True body += orig_line + "\n" else: if in_para: - body += "" + body += "\n" in_para = False if in_para: - body += "" + body += "\n" if symbol != "": self.doc_comment_last_symbol = symbol diff --git a/gio/tests/codegen.py b/gio/tests/codegen.py index 031776537..c95736e39 100644 --- a/gio/tests/codegen.py +++ b/gio/tests/codegen.py @@ -382,6 +382,46 @@ G_END_DECLS # The output should be the same. self.assertEqual(result1.out, result2.out) + def test_generate_docbook(self): + """Test the basic functionality of the docbook generator.""" + xml_contents = """ + + + + + + """ + res = self.runCodegenWithInterface( + xml_contents, + "--generate-docbook", + "test", + ) + self.assertEqual("", res.err) + self.assertEqual("", res.out) + with open("test-org.project.Bar.Frobnicator.xml", "r") as f: + xml_data = f.readlines() + self.assertTrue(len(xml_data) != 0) + + def test_generate_rst(self): + """Test the basic functionality of the rst generator.""" + xml_contents = """ + + + + + + """ + res = self.runCodegenWithInterface( + xml_contents, + "--generate-rst", + "test", + ) + self.assertEqual("", res.err) + self.assertEqual("", res.out) + with open("test-org.project.Bar.Frobnicator.rst", "r") as f: + rst = f.readlines() + self.assertTrue(len(rst) != 0) + def test_glib_min_required_invalid(self): """Test running with an invalid --glib-min-required.""" with self.assertRaises(subprocess.CalledProcessError): diff --git a/gio/tests/gdbus-object-manager-example/meson.build b/gio/tests/gdbus-object-manager-example/meson.build index f9c3bce26..ce0335e11 100644 --- a/gio/tests/gdbus-object-manager-example/meson.build +++ b/gio/tests/gdbus-object-manager-example/meson.build @@ -17,6 +17,22 @@ gdbus_example_objectmanager_generated = custom_target('objectmanager-gen', '--symbol-decorator-define', 'HAVE_CONFIG_H', '@INPUT@']) +gdbus_example_objectmanager_rst_gen = custom_target('objectmanager-rst-gen', + input: gdbus_example_objectmanager_xml, + output: [ + 'objectmanager-rst-gen-org.gtk.GDBus.Example.ObjectManager.Animal.rst', + 'objectmanager-rst-gen-org.gtk.GDBus.Example.ObjectManager.Cat.rst', + ], + command: [ + python, + gdbus_codegen, + '--interface-prefix', 'org.gtk.GDBus.Example.ObjectManager.', + '--generate-rst', 'objectmanager-rst-gen', + '--output-directory', '@OUTDIR@', + '@INPUT@', + ], +) + libgdbus_example_objectmanager = library('gdbus-example-objectmanager', gdbus_example_objectmanager_generated, c_args : test_c_args, @@ -25,6 +41,9 @@ libgdbus_example_objectmanager = library('gdbus-example-objectmanager', install_dir : installed_tests_execdir) libgdbus_example_objectmanager_dep = declare_dependency( - sources : gdbus_example_objectmanager_generated[0], + sources : [ + gdbus_example_objectmanager_generated[0], + gdbus_example_objectmanager_rst_gen[0], + ], link_with : libgdbus_example_objectmanager, dependencies : [libgio_dep])