Browse Source

qubes: Documentation for qubes.tools

And first documentation for qubes.tools.qvm_ls.
Wojtek Porczyk 9 years ago
parent
commit
142521317c
7 changed files with 246 additions and 2 deletions
  1. 1 0
      doc/index.rst
  2. 32 0
      doc/qubes-tools/index.rst
  3. 8 0
      doc/qubes-tools/qvm_ls.rst
  4. 20 0
      doc/skel-manpage.py
  5. 3 0
      qubes/__init__.py
  6. 132 1
      qubes/dochelpers.py
  7. 50 1
      qubes/tools/__init__.py

+ 1 - 0
doc/index.rst

@@ -19,6 +19,7 @@ manpages and API documentation. For primary user documentation, see
    qubes-plugins
    qubes-ext
    qubes-log
+   qubes-tools/index
    qubes-tests
    qubes-dochelpers
 

+ 32 - 0
doc/qubes-tools/index.rst

@@ -0,0 +1,32 @@
+:py:mod:`qubes.tools` -- Command line utilities
+===============================================
+
+Those are Python modules that house actual functionality of CLI tools -- the
+files installed in :file:`/usr/bin` only import these modules and run ``main()``
+function.
+
+The modules should make available for import theirs command line parsers
+(instances of :py:class:`argparse.ArgumentParser`) as either ``.parser``
+attribute or function ``get_parser()``, which returns parser. Manual page will
+be automatically checked during generation if its "Options" section contains all
+options from this parser (and only those).
+
+
+Module contents
+---------------
+
+.. automodule:: qubes.tools
+   :members:
+   :show-inheritance:
+
+
+All CLI tools
+-------------
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   *
+
+.. vim: ts=3 sw=3 et tw=80

+ 8 - 0
doc/qubes-tools/qvm_ls.rst

@@ -0,0 +1,8 @@
+:py:mod:`qubes.tools.qvm_ls` -- VM listing
+==========================================
+
+.. automodule:: qubes.tools.qvm_ls
+   :members:
+   :show-inheritance:
+
+.. vim: ts=3 sw=3 et

+ 20 - 0
doc/skel-manpage.py

@@ -0,0 +1,20 @@
+#!/usr/bin/python
+
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../'))
+
+import argparse
+import qubes.dochelpers
+
+parser = argparse.ArgumentParser(description='prepare new manpage for command')
+parser.add_argument('command', metavar='COMMAND',
+    help='program\'s command name; this should translate to '
+        'qubes.tools.<command_name>')
+
+def main():
+    args = parser.parse_args()
+    sys.stdout.write(qubes.dochelpers.prepare_manpage(args.command))
+
+if __name__ == '__main__':
+    main()

+ 3 - 0
qubes/__init__.py

@@ -81,6 +81,9 @@ class QubesException(Exception):
     pass
 
 
+import qubes.events
+
+
 class VMMConnection(object):
     '''Connection to Virtual Machine Manager (libvirt)'''
 

+ 132 - 1
qubes/dochelpers.py

@@ -29,8 +29,10 @@ particularly our custom Sphinx extension.
 '''
 
 import csv
+import os
 import posixpath
 import re
+import StringIO
 import urllib2
 
 import docutils
@@ -39,9 +41,13 @@ import docutils.parsers.rst
 import docutils.parsers.rst.roles
 import docutils.statemachine
 import sphinx
+import sphinx.errors
 import sphinx.locale
 import sphinx.util.docfields
 
+import qubes.tools
+
+
 def fetch_ticket_info(uri):
     '''Fetch info about particular trac ticket given
 
@@ -75,7 +81,7 @@ def ticket(name, rawtext, text, lineno, inliner, options=None, content=None):
         options = {}
 
     ticketno = text.lstrip('#')
-    if not ticket.isdigit():
+    if not ticketno.isdigit():
         msg = inliner.reporter.error(
             'Invalid ticket identificator: {!r}'.format(text), line=lineno)
         prb = inliner.problematic(rawtext, rawtext, msg)
@@ -147,6 +153,119 @@ class VersionCheck(docutils.parsers.rst.Directive):
         return [node]
 
 
+def make_rst_section(heading, char):
+    return '{}\n{}\n\n'.format(heading, char[0] * len(heading))
+
+
+def prepare_manpage(command):
+    parser = qubes.tools.get_parser_for_command(command)
+    stream = StringIO.StringIO()
+    stream.write('.. program:: {}\n\n'.format(command))
+    stream.write(make_rst_section(
+        ':program:`{}` -- {}'.format(command, parser.description), '='))
+    stream.write('''.. warning::
+
+   This page was autogenerated from command-line parser. It shouldn't be 1:1
+   conversion, because it would add little value. Please revise it and add
+   more descriptive help, which normally won't fit in standard ``--help``
+   option.
+
+   After rewrite, please remove this admonition.\n\n''')
+
+    stream.write(make_rst_section('Synopsis', '-'))
+    usage = ' '.join(parser.format_usage().strip().split())
+    if usage.startswith('usage: '):
+        usage = usage[len('usage: '):]
+
+    # replace METAVARS with *METAVARS*
+    usage = re.sub(r'\b([A-Z]{2,})\b', r'*\1*', usage)
+
+    stream.write(':command:`{}` {}\n\n'.format(command, usage))
+
+    stream.write(make_rst_section('Options', '-'))
+
+    for action in parser._actions: # pylint: disable=protected-access
+        stream.write('.. option:: ')
+        if action.metavar:
+            stream.write(', '.join('{}{}{}'.format(
+                    option,
+                    '=' if option.startswith('--') else ' ',
+                    action.metavar)
+                for option in sorted(action.option_strings)))
+        else:
+            stream.write(', '.join(sorted(action.option_strings)))
+        stream.write('\n\n   {}\n\n'.format(action.help))
+
+    stream.write(make_rst_section('Authors', '-'))
+    stream.write('''\
+| Joanna Rutkowska <joanna at invisiblethingslab dot com>
+| Rafal Wojtczuk <rafal at invisiblethingslab dot com>
+| Marek Marczykowski <marmarek at invisiblethingslab dot com>
+| Wojtek Porczyk <woju at invisiblethingslab dot com>
+
+.. vim: ts=3 sw=3 et tw=80
+''')
+
+    return stream.getvalue()
+
+
+class ArgumentCheckVisitor(docutils.nodes.SparseNodeVisitor):
+    def __init__(self, app, command, document):
+        docutils.nodes.SparseNodeVisitor.__init__(self, document)
+
+        self.app = app
+        self.command = command
+        self.args = set()
+
+        try:
+            parser = qubes.tools.get_parser_for_command(command)
+        except ImportError:
+            self.app.warn('cannot import module for command')
+            self.command = None
+            return
+        except AttributeError:
+            raise sphinx.errors.SphinxError('cannot find parser in module')
+
+        # pylint: disable=protected-access
+        for action in parser._actions:
+            self.args.update(action.option_strings)
+
+
+    # pylint: disable=no-self-use,unused-argument
+
+    def visit_desc(self, node):
+        if not node.get('desctype', None) == 'option':
+            raise docutils.nodes.SkipChildren
+
+
+    def visit_desc_name(self, node):
+        if self.command is None:
+            return
+
+        if not isinstance(node[0], docutils.nodes.Text):
+            raise sphinx.errors.SphinxError('first child should be Text')
+
+        arg = str(node[0])
+        try:
+            self.args.remove(arg)
+        except KeyError:
+            raise sphinx.errors.SphinxError(
+                'No such argument for {!r}: {!r}'.format(self.command, arg))
+
+
+    def depart_document(self, node):
+        if self.args:
+            raise sphinx.errors.SphinxError(
+                'Undocumented arguments: {!r}'.format(
+                    ', '.join(sorted(self.args))))
+
+
+def check_man_args(app, doctree, docname):
+    command = os.path.split(docname)[1]
+    app.info('Checking arguments for {!r}'.format(command))
+    doctree.walk(ArgumentCheckVisitor(app, command, doctree))
+
+
 #
 # this is lifted from sphinx' own conf.py
 #
@@ -172,10 +291,19 @@ def parse_event(env, sig, signode):
 # end of codelifting
 #
 
+
+def break_to_pdb(app, *dummy):
+    if not app.config.break_to_pdb:
+        return
+    import pdb
+    pdb.set_trace()
+
+
 def setup(app):
     app.add_role('ticket', ticket)
     app.add_config_value('ticket_base_uri',
         'https://wiki.qubes-os.org/ticket/', 'env')
+    app.add_config_value('break_to_pdb', False, 'env')
     app.add_node(versioncheck,
         html=(visit, depart),
         man=(visit, depart))
@@ -186,5 +314,8 @@ def setup(app):
     app.add_object_type('event', 'event', 'pair: %s; event', parse_event,
                         doc_field_types=[fdesc])
 
+    app.connect('doctree-resolved', break_to_pdb)
+    app.connect('doctree-resolved', check_man_args)
+
 
 # vim: ts=4 sw=4 et

+ 50 - 1
qubes/tools/__init__.py

@@ -1 +1,50 @@
-# pylint: skip-file
+#!/usr/bin/python2 -O
+# vim: fileencoding=utf-8
+
+#
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2015  Joanna Rutkowska <joanna@invisiblethingslab.com>
+# Copyright (C) 2015  Wojtek Porczyk <woju@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+'''Qubes' command line tools
+'''
+
+import importlib
+
+def get_parser_for_command(command):
+    '''Get parser for given qvm-tool.
+
+    :param str command: command name
+    :rtype: argparse.ArgumentParser
+    :raises ImportError: when command's module is not found
+    :raises AttributeError: when parser was not found
+    '''
+
+    module = importlib.import_module(
+        '.' + command.replace('-', '_'), 'qubes.tools')
+
+    try:
+        parser = module.parser
+    except AttributeError:
+        try:
+            parser = module.get_parser()
+        except AttributeError:
+            raise AttributeError('cannot find parser in module')
+
+    return parser