summaryrefslogtreecommitdiff
path: root/lib/spack/llnl/util/argparsewriter.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/llnl/util/argparsewriter.py')
-rw-r--r--lib/spack/llnl/util/argparsewriter.py415
1 files changed, 295 insertions, 120 deletions
diff --git a/lib/spack/llnl/util/argparsewriter.py b/lib/spack/llnl/util/argparsewriter.py
index ec6ea30df9..f43595145e 100644
--- a/lib/spack/llnl/util/argparsewriter.py
+++ b/lib/spack/llnl/util/argparsewriter.py
@@ -4,201 +4,376 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from __future__ import print_function
+
import re
import argparse
import errno
import sys
-
+from six import StringIO
+
+
+class Command(object):
+ """Parsed representation of a command from argparse.
+
+ This is a single command from an argparse parser. ``ArgparseWriter``
+ creates these and returns them from ``parse()``, and it passes one of
+ these to each call to ``format()`` so that we can take an action for
+ a single command.
+
+ Parts of a Command:
+ - prog: command name (str)
+ - description: command description (str)
+ - usage: command usage (str)
+ - positionals: list of positional arguments (list)
+ - optionals: list of optional arguments (list)
+ - subcommands: list of subcommand parsers (list)
+ """
+ def __init__(self, prog, description, usage,
+ positionals, optionals, subcommands):
+ self.prog = prog
+ self.description = description
+ self.usage = usage
+ self.positionals = positionals
+ self.optionals = optionals
+ self.subcommands = subcommands
+
+
+# NOTE: The only reason we subclass argparse.HelpFormatter is to get access
+# to self._expand_help(), ArgparseWriter is not intended to be used as a
+# formatter_class.
class ArgparseWriter(argparse.HelpFormatter):
"""Analyzes an argparse ArgumentParser for easy generation of help."""
- def __init__(self, out=sys.stdout):
- super(ArgparseWriter, self).__init__(out)
+
+ def __init__(self, prog, out=sys.stdout, aliases=False):
+ """Initializes a new ArgparseWriter instance.
+
+ Parameters:
+ prog (str): the program name
+ out (file object): the file to write to
+ aliases (bool): whether or not to include subparsers for aliases
+ """
+ super(ArgparseWriter, self).__init__(prog)
self.level = 0
+ self.prog = prog
self.out = out
+ self.aliases = aliases
+
+ def parse(self, parser, prog):
+ """Parses the parser object and returns the relavent components.
- def _write(self, parser, root=True, level=0):
+ Parameters:
+ parser (argparse.ArgumentParser): the parser
+ prog (str): the command name
+
+ Returns:
+ (Command) information about the command from the parser
+ """
self.parser = parser
- self.level = level
- actions = parser._actions
- # allow root level to be flattened with rest of commands
- if type(root) == int:
- self.level = root
- root = True
+ split_prog = parser.prog.split(' ')
+ split_prog[-1] = prog
+ prog = ' '.join(split_prog)
+ description = parser.description
+
+ fmt = parser._get_formatter()
+ actions = parser._actions
+ groups = parser._mutually_exclusive_groups
+ usage = fmt._format_usage(None, actions, groups, '').strip()
- # go through actions and split them into optionals, positionals,
+ # Go through actions and split them into optionals, positionals,
# and subcommands
optionals = []
positionals = []
subcommands = []
for action in actions:
if action.option_strings:
- optionals.append(action)
+ flags = action.option_strings
+ dest_flags = fmt._format_action_invocation(action)
+ help = self._expand_help(action) if action.help else ''
+ help = help.replace('\n', ' ')
+ optionals.append((flags, dest_flags, help))
elif isinstance(action, argparse._SubParsersAction):
for subaction in action._choices_actions:
subparser = action._name_parser_map[subaction.dest]
- subcommands.append(subparser)
+ subcommands.append((subparser, subaction.dest))
+
+ # Look for aliases of the form 'name (alias, ...)'
+ if self.aliases:
+ match = re.match(r'(.*) \((.*)\)', subaction.metavar)
+ if match:
+ aliases = match.group(2).split(', ')
+ for alias in aliases:
+ subparser = action._name_parser_map[alias]
+ subcommands.append((subparser, alias))
else:
- positionals.append(action)
+ args = fmt._format_action_invocation(action)
+ help = self._expand_help(action) if action.help else ''
+ help = help.replace('\n', ' ')
+ positionals.append((args, help))
- groups = parser._mutually_exclusive_groups
- fmt = parser._get_formatter()
- description = parser.description
+ return Command(
+ prog, description, usage, positionals, optionals, subcommands)
- def action_group(function, actions):
- for action in actions:
- arg = fmt._format_action_invocation(action)
- help = self._expand_help(action) if action.help else ''
- function(arg, re.sub('\n', ' ', help))
+ def format(self, cmd):
+ """Returns the string representation of a single node in the
+ parser tree.
- if root:
- self.begin_command(parser.prog)
+ Override this in subclasses to define how each subcommand
+ should be displayed.
- if description:
- self.description(parser.description)
+ Parameters:
+ (Command): parsed information about a command or subcommand
- usage = fmt._format_usage(None, actions, groups, '').strip()
- self.usage(usage)
+ Returns:
+ str: the string representation of this subcommand
+ """
+ raise NotImplementedError
- if positionals:
- self.begin_positionals()
- action_group(self.positional, positionals)
- self.end_positionals()
+ def _write(self, parser, prog, level=0):
+ """Recursively writes a parser.
- if optionals:
- self.begin_optionals()
- action_group(self.optional, optionals)
- self.end_optionals()
+ Parameters:
+ parser (argparse.ArgumentParser): the parser
+ prog (str): the command name
+ level (int): the current level
+ """
+ self.level = level
- if subcommands:
- self.begin_subcommands(subcommands)
- for subparser in subcommands:
- self._write(subparser, root=True, level=level + 1)
- self.end_subcommands(subcommands)
+ cmd = self.parse(parser, prog)
+ self.out.write(self.format(cmd))
- if root:
- self.end_command(parser.prog)
+ for subparser, prog in cmd.subcommands:
+ self._write(subparser, prog, level=level + 1)
- def write(self, parser, root=True):
+ def write(self, parser):
"""Write out details about an ArgumentParser.
Args:
- parser (ArgumentParser): an ``argparse`` parser
- root (bool or int): if bool, whether to include the root parser;
- or ``1`` to flatten the root parser with first-level
- subcommands
+ parser (argparse.ArgumentParser): the parser
"""
try:
- self._write(parser, root, level=0)
+ self._write(parser, self.prog)
except IOError as e:
- # swallow pipe errors
+ # Swallow pipe errors
+ # Raises IOError in Python 2 and BrokenPipeError in Python 3
if e.errno != errno.EPIPE:
raise
+
+_rst_levels = ['=', '-', '^', '~', ':', '`']
+
+
+class ArgparseRstWriter(ArgparseWriter):
+ """Write argparse output as rst sections."""
+
+ def __init__(self, prog, out=sys.stdout, aliases=False,
+ rst_levels=_rst_levels):
+ """Create a new ArgparseRstWriter.
+
+ Parameters:
+ prog (str): program name
+ out (file object): file to write to
+ aliases (bool): whether or not to include subparsers for aliases
+ rst_levels (list of str): list of characters
+ for rst section headings
+ """
+ super(ArgparseRstWriter, self).__init__(prog, out, aliases)
+ self.rst_levels = rst_levels
+
+ def format(self, cmd):
+ string = StringIO()
+ string.write(self.begin_command(cmd.prog))
+
+ if cmd.description:
+ string.write(self.description(cmd.description))
+
+ string.write(self.usage(cmd.usage))
+
+ if cmd.positionals:
+ string.write(self.begin_positionals())
+ for args, help in cmd.positionals:
+ string.write(self.positional(args, help))
+ string.write(self.end_positionals())
+
+ if cmd.optionals:
+ string.write(self.begin_optionals())
+ for flags, dest_flags, help in cmd.optionals:
+ string.write(self.optional(dest_flags, help))
+ string.write(self.end_optionals())
+
+ if cmd.subcommands:
+ string.write(self.begin_subcommands(cmd.subcommands))
+
+ return string.getvalue()
+
def begin_command(self, prog):
- pass
+ return """
+----
+
+.. _{0}:
- def end_command(self, prog):
- pass
+{1}
+{2}
+
+""".format(prog.replace(' ', '-'), prog,
+ self.rst_levels[self.level] * len(prog))
def description(self, description):
- pass
+ return description + '\n\n'
def usage(self, usage):
- pass
+ return """\
+.. code-block:: console
+
+ {0}
+
+""".format(usage)
def begin_positionals(self):
- pass
+ return '\n**Positional arguments**\n\n'
def positional(self, name, help):
- pass
+ return """\
+{0}
+ {1}
+
+""".format(name, help)
def end_positionals(self):
- pass
+ return ''
def begin_optionals(self):
- pass
+ return '\n**Optional arguments**\n\n'
+
+ def optional(self, opts, help):
+ return """\
+``{0}``
+ {1}
- def optional(self, option, help):
- pass
+""".format(opts, help)
def end_optionals(self):
- pass
+ return ''
def begin_subcommands(self, subcommands):
- pass
+ string = """
+**Subcommands**
- def end_subcommands(self, subcommands):
- pass
+.. hlist::
+ :columns: 4
+"""
-_rst_levels = ['=', '-', '^', '~', ':', '`']
+ for cmd, _ in subcommands:
+ prog = re.sub(r'^[^ ]* ', '', cmd.prog)
+ string += ' * :ref:`{0} <{1}>`\n'.format(
+ prog, cmd.prog.replace(' ', '-'))
+ return string + '\n'
-class ArgparseRstWriter(ArgparseWriter):
- """Write argparse output as rst sections."""
- def __init__(self, out=sys.stdout, rst_levels=_rst_levels,
- strip_root_prog=True):
- """Create a new ArgparseRstWriter.
+class ArgparseCompletionWriter(ArgparseWriter):
+ """Write argparse output as shell programmable tab completion functions."""
- Args:
- out (file object): file to write to
- rst_levels (list of str): list of characters
- for rst section headings
- strip_root_prog (bool): if ``True``, strip the base command name
- from subcommands in output
+ def format(self, cmd):
+ """Returns the string representation of a single node in the
+ parser tree.
+
+ Override this in subclasses to define how each subcommand
+ should be displayed.
+
+ Parameters:
+ (Command): parsed information about a command or subcommand
+
+ Returns:
+ str: the string representation of this subcommand
"""
- super(ArgparseRstWriter, self).__init__(out)
- self.rst_levels = rst_levels
- self.strip_root_prog = strip_root_prog
- def line(self, string=''):
- self.out.write('%s\n' % string)
+ assert cmd.optionals # we should always at least have -h, --help
+ assert not (cmd.positionals and cmd.subcommands) # one or the other
- def begin_command(self, prog):
- self.line()
- self.line('----')
- self.line()
- self.line('.. _%s:\n' % prog.replace(' ', '-'))
- self.line('%s' % prog)
- self.line(self.rst_levels[self.level] * len(prog) + '\n')
+ # We only care about the arguments/flags, not the help messages
+ positionals = []
+ if cmd.positionals:
+ positionals, _ = zip(*cmd.positionals)
+ optionals, _, _ = zip(*cmd.optionals)
+ subcommands = []
+ if cmd.subcommands:
+ _, subcommands = zip(*cmd.subcommands)
- def description(self, description):
- self.line('%s\n' % description)
+ # Flatten lists of lists
+ optionals = [x for xx in optionals for x in xx]
- def usage(self, usage):
- self.line('.. code-block:: console\n')
- self.line(' %s\n' % usage)
+ return (self.start_function(cmd.prog) +
+ self.body(positionals, optionals, subcommands) +
+ self.end_function(cmd.prog))
- def begin_positionals(self):
- self.line()
- self.line('**Positional arguments**\n')
+ def start_function(self, prog):
+ """Returns the syntax needed to begin a function definition.
- def positional(self, name, help):
- self.line(name)
- self.line(' %s\n' % help)
+ Parameters:
+ prog (str): the command name
- def begin_optionals(self):
- self.line()
- self.line('**Optional arguments**\n')
+ Returns:
+ str: the function definition beginning
+ """
+ name = prog.replace('-', '_').replace(' ', '_')
+ return '\n_{0}() {{'.format(name)
- def optional(self, opts, help):
- self.line('``%s``' % opts)
- self.line(' %s\n' % help)
+ def end_function(self, prog=None):
+ """Returns the syntax needed to end a function definition.
- def begin_subcommands(self, subcommands):
- self.line()
- self.line('**Subcommands**\n')
- self.line('.. hlist::')
- self.line(' :columns: 4\n')
-
- for cmd in subcommands:
- prog = cmd.prog
- if self.strip_root_prog:
- prog = re.sub(r'^[^ ]* ', '', prog)
-
- self.line(' * :ref:`%s <%s>`'
- % (prog, cmd.prog.replace(' ', '-')))
- self.line()
+ Parameters:
+ prog (str, optional): the command name
+
+ Returns:
+ str: the function definition ending
+ """
+ return '}\n'
+
+ def body(self, positionals, optionals, subcommands):
+ """Returns the body of the function.
+
+ Parameters:
+ positionals (list): list of positional arguments
+ optionals (list): list of optional arguments
+ subcommands (list): list of subcommand parsers
+
+ Returns:
+ str: the function body
+ """
+ return ''
+
+ def positionals(self, positionals):
+ """Returns the syntax for reporting positional arguments.
+
+ Parameters:
+ positionals (list): list of positional arguments
+
+ Returns:
+ str: the syntax for positional arguments
+ """
+ return ''
+
+ def optionals(self, optionals):
+ """Returns the syntax for reporting optional flags.
+
+ Parameters:
+ optionals (list): list of optional arguments
+
+ Returns:
+ str: the syntax for optional flags
+ """
+ return ''
+
+ def subcommands(self, subcommands):
+ """Returns the syntax for reporting subcommands.
+
+ Parameters:
+ subcommands (list): list of subcommand parsers
+
+ Returns:
+ str: the syntax for subcommand parsers
+ """
+ return ''