From 42d8e67556f6b7eacf711be439b1992b3df07269 Mon Sep 17 00:00:00 2001 From: Wojciech Zygmunt Porczyk Date: Mon, 12 Jan 2015 13:22:04 +0100 Subject: [PATCH] doc: Add autogenerated qubes.xml documentation --- doc/.gitignore | 2 + doc/Makefile | 45 ++++++----- doc/example.xml | 45 +++++++++++ doc/index.rst | 5 ++ qubes/rngdoc.py | 182 ++++++++++++++++++++++++++++++++++++++++++++ qubes/tests/init.py | 5 +- relaxng/qubes.rng | 3 - 7 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 doc/example.xml create mode 100755 qubes/rngdoc.py diff --git a/doc/.gitignore b/doc/.gitignore index 512245fb..63b63ff9 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -1 +1,3 @@ +_build sandbox.rst +autoxml.rst diff --git a/doc/Makefile b/doc/Makefile index 95b72af8..dae418dd 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -14,6 +14,8 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +DEPEND = autoxml.rst + .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @@ -41,40 +43,40 @@ help: @echo " install to generate manpages and copy them to \$$(DESTDIR)/usr/share/man" clean: - -rm -rf $(BUILDDIR)/* + -rm -rf $(BUILDDIR)/* $(DEPEND) -html: +html: $(DEPEND) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -dirhtml: +dirhtml: $(DEPEND) $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." -singlehtml: +singlehtml: $(DEPEND) $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." -pickle: +pickle: $(DEPEND) $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." -json: +json: $(DEPEND) $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." -htmlhelp: +htmlhelp: $(DEPEND) $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." -qthelp: +qthelp: $(DEPEND) $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -83,7 +85,7 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/core-admin.qhc" -devhelp: +devhelp: $(DEPEND) $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @@ -92,30 +94,30 @@ devhelp: @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/core-admin" @echo "# devhelp" -epub: +epub: $(DEPEND) $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." -latex: +latex: $(DEPEND) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." -latexpdf: +latexpdf: $(DEPEND) $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." -text: +text: $(DEPEND) $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." -man: +man: $(DEPEND) $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man for file in $(BUILDDIR)/man/*.[12345678]; do \ gzip -f $$file; \ @@ -123,41 +125,44 @@ man: @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." -texinfo: +texinfo: $(DEPEND) $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." -info: +info: $(DEPEND) $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." -gettext: +gettext: $(DEPEND) $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." -changes: +changes: $(DEPEND) $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." -linkcheck: +linkcheck: $(DEPEND) $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." -doctest: +doctest: $(DEPEND) $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +autoxml.rst: ../relaxng/qubes.rng example.xml + ../qubes/rngdoc.py $+ > $@ + .PHONY: install install: man mkdir -p $(DESTDIR)/usr/share/man/man1 diff --git a/doc/example.xml b/doc/example.xml new file mode 100644 index 00000000..0d1b5756 --- /dev/null +++ b/doc/example.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + 1 + netvm + + + + + meminfo-writer + qubes-firewall + + + + 01:23.45 + + + + + + 2 + appvm + + + + + qwe123 + + + + + + diff --git a/doc/index.rst b/doc/index.rst index 9552e67e..cb3de45a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -35,6 +35,11 @@ Developer documentation qubes-tests qubes-dochelpers +.. toctree:: + :maxdepth: 1 + + autoxml + Indices and tables ================== diff --git a/qubes/rngdoc.py b/qubes/rngdoc.py new file mode 100755 index 00000000..8467d68a --- /dev/null +++ b/qubes/rngdoc.py @@ -0,0 +1,182 @@ +#!/usr/bin/python2 -O + +from __future__ import print_function + +import sys +import textwrap + +import lxml.etree + +class Element(object): + def __init__(self, schema, xml): + self.schema = schema + self.xml = xml + + + @property + def nsmap(self): + return self.schema.nsmap + + + @property + def name(self): + return self.xml.get('name') + + + def get_description(self, xml=None, wrap=True): + if xml is None: xml = self.xml + + xml = xml.xpath('./doc:description', namespaces=self.nsmap) + if not xml: return '' + xml = xml[0] + + if wrap: + return ''.join(self.schema.wrapper.fill(p) + '\n\n' + for p in textwrap.dedent(xml.text.strip('\n')).split('\n\n')) + else: + return ' '.join(xml.text.strip().split()) + + + def get_data_type(self, xml=None): + if xml is None: xml = self.xml + + value = xml.xpath('./rng:value', namespaces=self.nsmap) + if value: + value = '``{}``'.format(value[0].text.strip()) + else: + metavar = xml.xpath('./doc:metavar', namespaces=self.nsmap) + if metavar: + value = '``{}``'.format(metavar[0].text.strip()) + else: + value = '' + + xml = xml.xpath('./rng:data', namespaces=self.nsmap) + if not xml: + return ('', value) + + xml = xml[0] + type_ = xml.get('type', '') + + if not value: + pattern = xml.xpath('./rng:param[@name="pattern"]', + namespaces=self.nsmap) + if pattern: + value = '``{}``'.format(pattern[0].text.strip()) + + return type_, value + + + def get_attributes(self): + for xml in self.xml.xpath('''./rng:attribute | + ./rng:optional/rng:attribute | + ./rng:choice/rng:attribute''', namespaces=self.nsmap): + required = xml.getparent() == self.xml and 'yes' or 'no' + yield (xml, required) + + + def resolve_ref(self, ref): + refs = self.xml.xpath('//rng:define[name="{}"]/rng:element'.format(ref['name'])) + return refs[0] if refs else None + + + def get_child_elements(self): + for xml in self.xml.xpath('''./rng:element | ./rng:ref | + ./rng:optional/rng:element | ./rng:optional/rng:ref | + ./rng:zeroOrMore/rng:element | ./rng:zeroOrMore/rng:ref | + ./rng:oneOrMore/rng:element | ./rng:oneOrMore/rng:ref''', namespaces=self.nsmap): + parent = xml.getparent() + qname = lxml.etree.QName(parent) + if parent == self.xml: + n = '1' + elif qname.localname == 'optional': + n = '?' + elif qname.localname == 'zeroOrMore': + n = '\\*' + elif qname.localname == 'oneOrMore': + n = '\\+' + else: + print(parent.tag) + + if xml.tag == 'ref': + xml = self.resolve_ref(xml) + if xml is None: continue + + yield (self.schema.elements[xml.get('name')], n) + + + def write_rst(self, stream): + stream.write('.. _qubesxml-element-{}:\n\n'.format(self.name)) + stream.write(make_rst_section('Element: **{}**'.format(self.name), '-')) + stream.write(self.get_description()) + + attrtable = [] + for attr, required in self.get_attributes(): + type_, value = self.get_data_type(attr) + attrtable.append(( + attr.get('name'), + required, + type_, + value, + self.get_description(attr, wrap=False))) + + if attrtable: + stream.write(make_rst_section('Attributes', '^')) + write_rst_table(stream, attrtable, + ('attribute', 'req.', 'type', 'value', 'description')) + + childtable = [(':ref:`{0} `'.format(child.xml.get('name')), n) + for child, n in self.get_child_elements()] + if childtable: + stream.write(make_rst_section('Child elements', '^')) + write_rst_table(stream, childtable, ('element', 'n')) + + +class Schema(object): + nsmap = { + 'rng': 'http://relaxng.org/ns/structure/1.0', + 'q': 'http://qubes-os.org/qubes/3', + 'doc': 'http://qubes-os.org/qubes-doc/1'} + + def __init__(self, xml): + self.xml = xml + + self.wrapper = textwrap.TextWrapper(width=80, + break_long_words=False, break_on_hyphens=False) + + self.elements = {} + for x in self.xml.xpath('//rng:element', namespaces=self.nsmap): + element = Element(self, x) + self.elements[element.name] = element + + +def make_rst_section(heading, c): + return '{}\n{}\n\n'.format(heading, c[0] * len(heading)) + + +def write_rst_table(stream, it, heads): + stream.write('.. csv-table::\n') + stream.write(' :header: {}\n'.format(', '.join('"{}"'.format(c) for c in heads))) + stream.write(' :widths: {}\n\n'.format(', '.join('1' for c in heads))) + + for row in it: + stream.write(' {}\n'.format(', '.join('"{}"'.format(i) for i in row))) + + stream.write('\n') + + +def main(filename, example): + schema = Schema(lxml.etree.parse(open(filename, 'rb'))) + + sys.stdout.write(make_rst_section('Qubes XML specification', '=')) + sys.stdout.write('This is the documentation of qubes.xml autogenerated from RelaxNG source.\n\n') + sys.stdout.write('Quick example, worth thousands lines of specification:\n\n') + sys.stdout.write('.. literalinclude:: {}\n :language: xml\n\n'.format(example)) + + for name in sorted(schema.elements): + schema.elements[name].write_rst(sys.stdout) + + +if __name__ == '__main__': + main(*sys.argv[1:]) + +# vim: ts=4 sw=4 et diff --git a/qubes/tests/init.py b/qubes/tests/init.py index 45a990b3..dc4131b3 100644 --- a/qubes/tests/init.py +++ b/qubes/tests/init.py @@ -320,4 +320,7 @@ class TC_30_VMCollection(qubes.tests.QubesTestCase): class TC_90_Qubes(qubes.tests.QubesTestCase): - pass + def test_900_example_xml_in_doc(self): + self.assertXMLIsValid( + lxml.etree.parse(open('../../doc/example.xml', 'rb')), + '../../relaxng/qubes.rng') diff --git a/relaxng/qubes.rng b/relaxng/qubes.rng index 82a38a1c..95fafc51 100644 --- a/relaxng/qubes.rng +++ b/relaxng/qubes.rng @@ -170,9 +170,6 @@ the parser will complain about missing combine= attribute on the second . Whether service is enabled or disabled. Default is ``true``. - - true|false -