Merge remote-tracking branch 'origin/pr/104'

* origin/pr/104:
  dochelpers: make PEP8 happier
  dochelpers: handle legacy sphinx library
  Fix Sphinx 2 new API for Fedora 31+
This commit is contained in:
Marek Marczykowski-Górecki 2019-09-21 03:34:27 +02:00
commit 556cec4e3f
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
2 changed files with 63 additions and 45 deletions

View File

@ -2,6 +2,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath('../'))
import argparse
@ -9,12 +10,14 @@ import qubesadmin.tools.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 '
'qubesadmin.tools.<command_name>')
help='program\'s command name; this should translate to '
'qubesadmin.tools.<command_name>')
def main():
args = parser.parse_args()
sys.stdout.write(qubesadmin.tools.dochelpers.prepare_manpage(args.command))
if __name__ == '__main__':
main()

View File

@ -19,11 +19,11 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
'''Documentation helpers.
"""Documentation helpers.
This module contains classes and functions which help to maintain documentation,
particularly our custom Sphinx extension.
'''
"""
import argparse
import io
@ -39,33 +39,39 @@ import sphinx
import sphinx.errors
import sphinx.locale
import sphinx.util.docfields
from sphinx.util import logging
import qubesadmin.tools
try:
log = logging.getLogger(__name__)
except AttributeError:
log = None
SUBCOMMANDS_TITLE = 'COMMANDS'
OPTIONS_TITLE = 'OPTIONS'
def make_rst_section(heading, char):
'''Format a section header in rst'''
"""Format a section header in rst"""
return '{}\n{}\n\n'.format(heading, char[0] * len(heading))
def prepare_manpage(command):
'''Build a man page skeleton'''
"""Build a man page skeleton"""
parser = qubesadmin.tools.get_parser_for_command(command)
stream = io.StringIO()
stream.write('.. program:: {}\n\n'.format(command))
stream.write(make_rst_section(
':program:`{}` -- {}'.format(command, parser.description), '='))
stream.write('''.. warning::
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''')
After rewrite, please remove this admonition.\n\n""")
stream.write(make_rst_section('Synopsis', '-'))
usage = ' '.join(parser.format_usage().strip().split())
@ -79,34 +85,34 @@ def prepare_manpage(command):
stream.write(make_rst_section('Options', '-'))
for action in parser._actions: # pylint: disable=protected-access
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)))
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('''\
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 OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
''' Checks if the visited option nodes and the specified args are in sync.
'''
""" Checks if the visited option nodes and the specified args are in sync.
"""
def __init__(self, command, args, document):
assert isinstance(args, set)
docutils.nodes.SparseNodeVisitor.__init__(self, document)
@ -114,14 +120,13 @@ class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
self.args = args
def visit_desc(self, node):
''' Skips all but 'option' elements '''
""" Skips all but 'option' elements """
# pylint: disable=no-self-use
if not node.get('desctype', None) == 'option':
raise docutils.nodes.SkipChildren
def visit_desc_name(self, node):
''' Checks if the option is defined `self.args` '''
""" Checks if the option is defined `self.args` """
if not isinstance(node[0], docutils.nodes.Text):
raise sphinx.errors.SphinxError('first child should be Text')
@ -133,14 +138,14 @@ class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
'No such argument for {!r}: {!r}'.format(self.command, arg))
def check_undocumented_arguments(self, ignored_options=None):
''' Call this to check if any undocumented arguments are left.
""" Call this to check if any undocumented arguments are left.
While the documentation talks about a
'SparseNodeVisitor.depart_document()' function, this function does
not exists. (For details see implementation of
:py:meth:`NodeVisitor.dispatch_departure()`) So we need to
manually call this.
'''
"""
if ignored_options is None:
ignored_options = set()
left_over_args = self.args - ignored_options
@ -151,9 +156,9 @@ class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
''' Checks if the visited sub command section nodes and the specified sub
""" Checks if the visited sub command section nodes and the specified sub
command args are in sync.
'''
"""
def __init__(self, command, sub_commands, document):
docutils.nodes.SparseNodeVisitor.__init__(self, document)
@ -161,12 +166,12 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
self.sub_commands = sub_commands
def visit_section(self, node):
''' Checks if the visited sub-command section nodes exists and it
""" Checks if the visited sub-command section nodes exists and it
options are in sync.
Uses :py:class:`OptionsCheckVisitor` for checking
sub-commands options
'''
"""
# pylint: disable=no-self-use
title = str(node[0][0])
if title.upper() == SUBCOMMANDS_TITLE:
@ -186,10 +191,10 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
'No such sub-command {!r}'.format(sub_cmd))
def visit_Text(self, node):
''' If the visited text node starts with 'alias: ', all the provided
""" If the visited text node starts with 'alias: ', all the provided
comma separted alias in this node, are removed from
`self.sub_commands`
'''
"""
# pylint: disable=invalid-name
text = str(node).strip()
if text.startswith('aliases:'):
@ -198,16 +203,15 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
assert alias in self.sub_commands
del self.sub_commands[alias]
def check_undocumented_sub_commands(self):
''' Call this to check if any undocumented sub_commands are left.
""" Call this to check if any undocumented sub_commands are left.
While the documentation talks about a
'SparseNodeVisitor.depart_document()' function, this function does
not exists. (For details see implementation of
:py:meth:`NodeVisitor.dispatch_departure()`) So we need to
manually call this.
'''
"""
if self.sub_commands:
raise sphinx.errors.SphinxError(
'Undocumented commands for {!r}: {!r}'.format(
@ -215,15 +219,22 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
class ManpageCheckVisitor(docutils.nodes.SparseNodeVisitor):
''' Checks if the sub-commands and options specified in the 'COMMAND' and
""" Checks if the sub-commands and options specified in the 'COMMAND' and
'OPTIONS' (case insensitve) sections in sync the command parser.
'''
"""
def __init__(self, app, command, document):
docutils.nodes.SparseNodeVisitor.__init__(self, document)
try:
parser = qubesadmin.tools.get_parser_for_command(command)
except ImportError:
app.warn('cannot import module for command')
msg = 'cannot import module for command'
if log:
log.warning(msg)
else:
# Handle legacy
app.warn(msg)
self.parser = None
return
except AttributeError:
@ -252,16 +263,16 @@ class ManpageCheckVisitor(docutils.nodes.SparseNodeVisitor):
self.options.update(action.option_strings)
def visit_section(self, node):
''' If section title is OPTIONS or COMMANDS dispatch the apropriate
""" If section title is OPTIONS or COMMANDS dispatch the apropriate
`NodeVisitor`.
'''
"""
if self.parser is None:
return
section_title = str(node[0][0]).upper()
if section_title == OPTIONS_TITLE:
options_visitor = OptionsCheckVisitor(self.command, self.options,
self.document)
self.document)
node.walkabout(options_visitor)
options_visitor.check_undocumented_arguments()
elif section_title == SUBCOMMANDS_TITLE:
@ -270,21 +281,26 @@ class ManpageCheckVisitor(docutils.nodes.SparseNodeVisitor):
node.walkabout(sub_cmd_visitor)
sub_cmd_visitor.check_undocumented_sub_commands()
def check_man_args(app, doctree, docname):
''' Checks the manpage for undocumented or obsolete sub-commands and
""" Checks the manpage for undocumented or obsolete sub-commands and
options.
'''
"""
dirname, command = os.path.split(docname)
if os.path.basename(dirname) != 'manpages':
return
app.info('Checking arguments for {!r}'.format(command))
msg = 'Checking arguments for {!r}'.format(command)
if log:
log.info(msg)
else:
# Handle legacy
app.info(msg)
doctree.walk(ManpageCheckVisitor(app, command, doctree))
def break_to_pdb(app, *_dummy):
'''DEBUG'''
"""DEBUG"""
if not app.config.break_to_pdb:
return
import pdb
@ -292,11 +308,10 @@ def break_to_pdb(app, *_dummy):
def setup(app):
'''Setup Sphinx extension'''
"""Setup Sphinx extension"""
app.add_config_value('break_to_pdb', False, 'env')
app.connect('doctree-resolved', break_to_pdb)
app.connect('doctree-resolved', check_man_args)
# vim: ts=4 sw=4 et