spack commands: add type hints and docstrings (#38705)

This commit is contained in:
Adam J. Stewart
2023-07-04 15:43:02 -05:00
committed by GitHub
parent d35149d174
commit 2978911520
3 changed files with 371 additions and 159 deletions

View File

@@ -3,31 +3,42 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import abc
import argparse
import errno
import io
import re
import sys
from argparse import ArgumentParser
from typing import IO, Optional, Sequence, Tuple
class Command(object):
class Command:
"""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)
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.
"""
def __init__(self, prog, description, usage, positionals, optionals, subcommands):
def __init__(
self,
prog: str,
description: Optional[str],
usage: str,
positionals: Sequence[Tuple[str, str]],
optionals: Sequence[Tuple[Sequence[str], str, str]],
subcommands: Sequence[Tuple[ArgumentParser, str]],
) -> None:
"""Initialize a new Command instance.
Args:
prog: Program name.
description: Command description.
usage: Command usage.
positionals: List of positional arguments.
optionals: List of optional arguments.
subcommands: List of subcommand parsers.
"""
self.prog = prog
self.description = description
self.usage = usage
@@ -36,35 +47,34 @@ def __init__(self, prog, description, usage, positionals, optionals, subcommands
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."""
# 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, abc.ABC):
"""Analyze an argparse ArgumentParser for easy generation of help."""
def __init__(self, prog, out=None, aliases=False):
"""Initializes a new ArgparseWriter instance.
def __init__(self, prog: str, out: IO = sys.stdout, aliases: bool = False) -> None:
"""Initialize a new ArgparseWriter instance.
Parameters:
prog (str): the program name
out (file object): the file to write to (default sys.stdout)
aliases (bool): whether or not to include subparsers for aliases
Args:
prog: Program name.
out: File object to write to.
aliases: Whether or not to include subparsers for aliases.
"""
super(ArgparseWriter, self).__init__(prog)
super().__init__(prog)
self.level = 0
self.prog = prog
self.out = sys.stdout if out is None else out
self.out = out
self.aliases = aliases
def parse(self, parser, prog):
"""Parses the parser object and returns the relavent components.
def parse(self, parser: ArgumentParser, prog: str) -> Command:
"""Parse the parser object and return the relavent components.
Parameters:
parser (argparse.ArgumentParser): the parser
prog (str): the command name
Args:
parser: Command parser.
prog: Program name.
Returns:
(Command) information about the command from the parser
Information about the command from the parser.
"""
self.parser = parser
@@ -78,8 +88,7 @@ def parse(self, parser, prog):
groups = parser._mutually_exclusive_groups
usage = fmt._format_usage(None, actions, groups, "").strip()
# Go through actions and split them into optionals, positionals,
# and subcommands
# Go through actions and split them into optionals, positionals, and subcommands
optionals = []
positionals = []
subcommands = []
@@ -96,7 +105,7 @@ def parse(self, parser, prog):
subcommands.append((subparser, subaction.dest))
# Look for aliases of the form 'name (alias, ...)'
if self.aliases:
if self.aliases and isinstance(subaction.metavar, str):
match = re.match(r"(.*) \((.*)\)", subaction.metavar)
if match:
aliases = match.group(2).split(", ")
@@ -111,28 +120,26 @@ def parse(self, parser, prog):
return Command(prog, description, usage, positionals, optionals, subcommands)
def format(self, cmd):
"""Returns the string representation of a single node in the
parser tree.
@abc.abstractmethod
def format(self, cmd: Command) -> str:
"""Return the string representation of a single node in the parser tree.
Override this in subclasses to define how each subcommand
should be displayed.
Override this in subclasses to define how each subcommand should be displayed.
Parameters:
(Command): parsed information about a command or subcommand
Args:
cmd: Parsed information about a command or subcommand.
Returns:
str: the string representation of this subcommand
String representation of this subcommand.
"""
raise NotImplementedError
def _write(self, parser, prog, level=0):
"""Recursively writes a parser.
def _write(self, parser: ArgumentParser, prog: str, level: int = 0) -> None:
"""Recursively write a parser.
Parameters:
parser (argparse.ArgumentParser): the parser
prog (str): the command name
level (int): the current level
Args:
parser: Command parser.
prog: Program name.
level: Current level.
"""
self.level = level
@@ -142,19 +149,17 @@ def _write(self, parser, prog, level=0):
for subparser, prog in cmd.subcommands:
self._write(subparser, prog, level=level + 1)
def write(self, parser):
def write(self, parser: ArgumentParser) -> None:
"""Write out details about an ArgumentParser.
Args:
parser (argparse.ArgumentParser): the parser
parser: Command parser.
"""
try:
self._write(parser, self.prog)
except IOError as e:
except BrokenPipeError:
# Swallow pipe errors
# Raises IOError in Python 2 and BrokenPipeError in Python 3
if e.errno != errno.EPIPE:
raise
pass
_rst_levels = ["=", "-", "^", "~", ":", "`"]
@@ -163,21 +168,33 @@ def write(self, parser):
class ArgparseRstWriter(ArgparseWriter):
"""Write argparse output as rst sections."""
def __init__(self, prog, out=None, aliases=False, rst_levels=_rst_levels):
"""Create a new ArgparseRstWriter.
def __init__(
self,
prog: str,
out: IO = sys.stdout,
aliases: bool = False,
rst_levels: Sequence[str] = _rst_levels,
) -> None:
"""Initialize a new ArgparseRstWriter instance.
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
Args:
prog: Program name.
out: File object to write to.
aliases: Whether or not to include subparsers for aliases.
rst_levels: List of characters for rst section headings.
"""
out = sys.stdout if out is None else out
super(ArgparseRstWriter, self).__init__(prog, out, aliases)
super().__init__(prog, out, aliases)
self.rst_levels = rst_levels
def format(self, cmd):
def format(self, cmd: Command) -> str:
"""Return the string representation of a single node in the parser tree.
Args:
cmd: Parsed information about a command or subcommand.
Returns:
String representation of a node.
"""
string = io.StringIO()
string.write(self.begin_command(cmd.prog))
@@ -203,7 +220,15 @@ def format(self, cmd):
return string.getvalue()
def begin_command(self, prog):
def begin_command(self, prog: str) -> str:
"""Text to print before a command.
Args:
prog: Program name.
Returns:
Text before a command.
"""
return """
----
@@ -216,10 +241,26 @@ def begin_command(self, prog):
prog.replace(" ", "-"), prog, self.rst_levels[self.level] * len(prog)
)
def description(self, description):
def description(self, description: str) -> str:
"""Description of a command.
Args:
description: Command description.
Returns:
Description of a command.
"""
return description + "\n\n"
def usage(self, usage):
def usage(self, usage: str) -> str:
"""Example usage of a command.
Args:
usage: Command usage.
Returns:
Usage of a command.
"""
return """\
.. code-block:: console
@@ -229,10 +270,24 @@ def usage(self, usage):
usage
)
def begin_positionals(self):
def begin_positionals(self) -> str:
"""Text to print before positional arguments.
Returns:
Positional arguments header.
"""
return "\n**Positional arguments**\n\n"
def positional(self, name, help):
def positional(self, name: str, help: str) -> str:
"""Description of a positional argument.
Args:
name: Argument name.
help: Help text.
Returns:
Positional argument description.
"""
return """\
{0}
{1}
@@ -241,13 +296,32 @@ def positional(self, name, help):
name, help
)
def end_positionals(self):
def end_positionals(self) -> str:
"""Text to print after positional arguments.
Returns:
Positional arguments footer.
"""
return ""
def begin_optionals(self):
def begin_optionals(self) -> str:
"""Text to print before optional arguments.
Returns:
Optional arguments header.
"""
return "\n**Optional arguments**\n\n"
def optional(self, opts, help):
def optional(self, opts: str, help: str) -> str:
"""Description of an optional argument.
Args:
opts: Optional argument.
help: Help text.
Returns:
Optional argument description.
"""
return """\
``{0}``
{1}
@@ -256,10 +330,23 @@ def optional(self, opts, help):
opts, help
)
def end_optionals(self):
def end_optionals(self) -> str:
"""Text to print after optional arguments.
Returns:
Optional arguments footer.
"""
return ""
def begin_subcommands(self, subcommands):
def begin_subcommands(self, subcommands: Sequence[Tuple[ArgumentParser, str]]) -> str:
"""Table with links to other subcommands.
Arguments:
subcommands: List of subcommands.
Returns:
Subcommand linking text.
"""
string = """
**Subcommands**
@@ -278,29 +365,25 @@ def begin_subcommands(self, subcommands):
class ArgparseCompletionWriter(ArgparseWriter):
"""Write argparse output as shell programmable tab completion functions."""
def format(self, cmd):
"""Returns the string representation of a single node in the
parser tree.
def format(self, cmd: Command) -> str:
"""Return 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
Args:
cmd: Parsed information about a command or subcommand.
Returns:
str: the string representation of this subcommand
String representation of this subcommand.
"""
assert cmd.optionals # we should always at least have -h, --help
assert not (cmd.positionals and cmd.subcommands) # one or the other
# We only care about the arguments/flags, not the help messages
positionals = []
positionals: Tuple[str, ...] = ()
if cmd.positionals:
positionals, _ = zip(*cmd.positionals)
optionals, _, _ = zip(*cmd.optionals)
subcommands = []
subcommands: Tuple[str, ...] = ()
if cmd.subcommands:
_, subcommands = zip(*cmd.subcommands)
@@ -313,71 +396,73 @@ def format(self, cmd):
+ self.end_function(cmd.prog)
)
def start_function(self, prog):
"""Returns the syntax needed to begin a function definition.
def start_function(self, prog: str) -> str:
"""Return the syntax needed to begin a function definition.
Parameters:
prog (str): the command name
Args:
prog: Program name.
Returns:
str: the function definition beginning
Function definition beginning.
"""
name = prog.replace("-", "_").replace(" ", "_")
return "\n_{0}() {{".format(name)
def end_function(self, prog=None):
"""Returns the syntax needed to end a function definition.
def end_function(self, prog: str) -> str:
"""Return the syntax needed to end a function definition.
Parameters:
prog (str or None): the command name
Args:
prog: Program name
Returns:
str: the function definition ending
Function definition ending.
"""
return "}\n"
def body(self, positionals, optionals, subcommands):
"""Returns the body of the function.
def body(
self, positionals: Sequence[str], optionals: Sequence[str], subcommands: Sequence[str]
) -> str:
"""Return the body of the function.
Parameters:
positionals (list): list of positional arguments
optionals (list): list of optional arguments
subcommands (list): list of subcommand parsers
Args:
positionals: List of positional arguments.
optionals: List of optional arguments.
subcommands: List of subcommand parsers.
Returns:
str: the function body
Function body.
"""
return ""
def positionals(self, positionals):
"""Returns the syntax for reporting positional arguments.
def positionals(self, positionals: Sequence[str]) -> str:
"""Return the syntax for reporting positional arguments.
Parameters:
positionals (list): list of positional arguments
Args:
positionals: List of positional arguments.
Returns:
str: the syntax for positional arguments
Syntax for positional arguments.
"""
return ""
def optionals(self, optionals):
"""Returns the syntax for reporting optional flags.
def optionals(self, optionals: Sequence[str]) -> str:
"""Return the syntax for reporting optional flags.
Parameters:
optionals (list): list of optional arguments
Args:
optionals: List of optional arguments.
Returns:
str: the syntax for optional flags
Syntax for optional flags.
"""
return ""
def subcommands(self, subcommands):
"""Returns the syntax for reporting subcommands.
def subcommands(self, subcommands: Sequence[str]) -> str:
"""Return the syntax for reporting subcommands.
Parameters:
subcommands (list): list of subcommand parsers
Args:
subcommands: List of subcommand parsers.
Returns:
str: the syntax for subcommand parsers
Syntax for subcommand parsers
"""
return ""