From 11f2b612612748ee57728693c7745e3af92e9d54 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 5 Jan 2020 23:35:23 -0800 Subject: Use `spack commands --format=bash` to generate shell completion (#14393) This PR adds a `--format=bash` option to `spack commands` to auto-generate the Bash programmable tab completion script. It can be extended to work for other shells. Progress: - [x] Fix bug in superclass initialization in `ArgparseWriter` - [x] Refactor `ArgparseWriter` (see below) - [x] Ensure that output of old `--format` options remains the same - [x] Add `ArgparseCompletionWriter` and `BashCompletionWriter` - [x] Add `--aliases` option to add command aliases - [x] Standardize positional argument names - [x] Tests for `spack commands --format=bash` coverage - [x] Tests to make sure `spack-completion.bash` stays up-to-date - [x] Tests for `spack-completion.bash` coverage - [x] Speed up `spack-completion.bash` by caching subroutine calls This PR also necessitates a significant refactoring of `ArgparseWriter`. Previously, `ArgparseWriter` was mostly a single `_write` method which handled everything from extracting the information we care about from the parser to formatting the output. Now, `_write` only handles recursion, while the information extraction is split into a separate `parse` method, and the formatting is handled by `format`. This allows subclasses to completely redefine how the format will appear without overriding all of `_write`. Co-Authored-by: Todd Gamblin --- lib/spack/llnl/util/argparsewriter.py | 415 ++++++++++++++++------- lib/spack/spack/cmd/activate.py | 7 +- lib/spack/spack/cmd/add.py | 6 +- lib/spack/spack/cmd/blame.py | 8 +- lib/spack/spack/cmd/build_env.py | 2 +- lib/spack/spack/cmd/buildcache.py | 32 +- lib/spack/spack/cmd/checksum.py | 5 +- lib/spack/spack/cmd/clean.py | 7 +- lib/spack/spack/cmd/commands.py | 125 ++++++- lib/spack/spack/cmd/common/arguments.py | 46 ++- lib/spack/spack/cmd/config.py | 6 +- lib/spack/spack/cmd/configure.py | 13 +- lib/spack/spack/cmd/deactivate.py | 6 +- lib/spack/spack/cmd/dependencies.py | 5 +- lib/spack/spack/cmd/dependents.py | 8 +- lib/spack/spack/cmd/dev_build.py | 7 +- lib/spack/spack/cmd/edit.py | 5 +- lib/spack/spack/cmd/env.py | 4 +- lib/spack/spack/cmd/extensions.py | 2 +- lib/spack/spack/cmd/fetch.py | 12 +- lib/spack/spack/cmd/gpg.py | 15 +- lib/spack/spack/cmd/graph.py | 7 +- lib/spack/spack/cmd/info.py | 6 +- lib/spack/spack/cmd/install.py | 13 +- lib/spack/spack/cmd/load.py | 8 +- lib/spack/spack/cmd/location.py | 6 +- lib/spack/spack/cmd/maintainers.py | 14 +- lib/spack/spack/cmd/mirror.py | 14 +- lib/spack/spack/cmd/patch.py | 13 +- lib/spack/spack/cmd/pkg.py | 5 +- lib/spack/spack/cmd/remove.py | 6 +- lib/spack/spack/cmd/repo.py | 12 +- lib/spack/spack/cmd/restage.py | 10 +- lib/spack/spack/cmd/setup.py | 5 +- lib/spack/spack/cmd/spec.py | 4 +- lib/spack/spack/cmd/stage.py | 7 +- lib/spack/spack/cmd/uninstall.py | 18 +- lib/spack/spack/cmd/unload.py | 7 +- lib/spack/spack/cmd/verify.py | 12 +- lib/spack/spack/cmd/versions.py | 7 +- lib/spack/spack/reporters/cdash.py | 4 +- lib/spack/spack/test/cmd/commands.py | 170 ++++++++-- lib/spack/spack/test/llnl/util/argparsewriter.py | 37 ++ 43 files changed, 751 insertions(+), 370 deletions(-) create mode 100644 lib/spack/spack/test/llnl/util/argparsewriter.py (limited to 'lib') 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 '' diff --git a/lib/spack/spack/cmd/activate.py b/lib/spack/spack/cmd/activate.py index 718d30ce07..bfca9f5604 100644 --- a/lib/spack/spack/cmd/activate.py +++ b/lib/spack/spack/cmd/activate.py @@ -3,11 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment as ev from spack.filesystem_view import YamlFilesystemView @@ -23,9 +22,7 @@ def setup_parser(subparser): subparser.add_argument( '-v', '--view', metavar='VIEW', type=str, help="the view to operate on") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="spec of package extension to activate") + arguments.add_common_arguments(subparser, ['installed_spec']) def activate(parser, args): diff --git a/lib/spack/spack/cmd/add.py b/lib/spack/spack/cmd/add.py index efae7ffeb7..e08c2c5aac 100644 --- a/lib/spack/spack/cmd/add.py +++ b/lib/spack/spack/cmd/add.py @@ -3,11 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment as ev @@ -20,8 +19,7 @@ def setup_parser(subparser): subparser.add_argument('-l', '--list-name', dest='list_name', default='specs', help="name of the list to add specs to") - subparser.add_argument( - 'specs', nargs=argparse.REMAINDER, help="specs of packages to add") + arguments.add_common_arguments(subparser, ['specs']) def add(parser, args): diff --git a/lib/spack/spack/cmd/blame.py b/lib/spack/spack/cmd/blame.py index ea1a310476..b806058aec 100644 --- a/lib/spack/spack/cmd/blame.py +++ b/lib/spack/spack/cmd/blame.py @@ -35,7 +35,7 @@ def setup_parser(subparser): help='show git blame output instead of summary') subparser.add_argument( - 'package_name', help='name of package to show contributions for, ' + 'package_or_file', help='name of package to show contributions for, ' 'or path to a file in the spack repo') @@ -47,13 +47,13 @@ def blame(parser, args): # Get name of file to blame blame_file = None - if os.path.isfile(args.package_name): - path = os.path.realpath(args.package_name) + if os.path.isfile(args.package_or_file): + path = os.path.realpath(args.package_or_file) if path.startswith(spack.paths.prefix): blame_file = path if not blame_file: - pkg = spack.repo.get(args.package_name) + pkg = spack.repo.get(args.package_or_file) blame_file = pkg.module.__file__.rstrip('c') # .pyc -> .py # get git blame for the package diff --git a/lib/spack/spack/cmd/build_env.py b/lib/spack/spack/cmd/build_env.py index 7f9f08c01f..128d167a29 100644 --- a/lib/spack/spack/cmd/build_env.py +++ b/lib/spack/spack/cmd/build_env.py @@ -33,7 +33,7 @@ def setup_parser(subparser): subparser.add_argument( 'spec', nargs=argparse.REMAINDER, metavar='spec [--] [cmd]...', - help="specs of package environment to emulate") + help="spec of package environment to emulate") subparser.epilog\ = 'If a command is not specified, the environment will be printed ' \ 'to standard output (cf /usr/bin/env) unless --dump and/or --pickle ' \ diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py index 5baa63af85..cbcbc2c0cb 100644 --- a/lib/spack/spack/cmd/buildcache.py +++ b/lib/spack/spack/cmd/buildcache.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse import os import shutil import sys @@ -61,11 +60,9 @@ def setup_parser(subparser): "building package(s)") create.add_argument('-y', '--spec-yaml', default=None, help='Create buildcache entry for spec from yaml file') - create.add_argument( - 'packages', nargs=argparse.REMAINDER, - help="specs of packages to create buildcache for") create.add_argument('--no-deps', action='store_true', default='false', help='Create buildcache entry wo/ dependencies') + arguments.add_common_arguments(create, ['specs']) create.set_defaults(func=createtarball) install = subparsers.add_parser('install', help=installtarball.__doc__) @@ -79,9 +76,7 @@ def setup_parser(subparser): install.add_argument('-u', '--unsigned', action='store_true', help="install unsigned buildcache" + " tarballs for testing") - install.add_argument( - 'packages', nargs=argparse.REMAINDER, - help="specs of packages to install buildcache for") + arguments.add_common_arguments(install, ['specs']) install.set_defaults(func=installtarball) listcache = subparsers.add_parser('list', help=listspecs.__doc__) @@ -92,9 +87,7 @@ def setup_parser(subparser): help='show variants in output (can be long)') listcache.add_argument('-f', '--force', action='store_true', help="force new download of specs") - listcache.add_argument( - 'packages', nargs=argparse.REMAINDER, - help="specs of packages to search for") + arguments.add_common_arguments(listcache, ['specs']) listcache.set_defaults(func=listspecs) dlkeys = subparsers.add_parser('keys', help=getkeys.__doc__) @@ -113,10 +106,9 @@ def setup_parser(subparser): help='analyzes an installed spec and reports whether ' 'executables and libraries are relocatable' ) - preview_parser.add_argument( - 'packages', nargs='+', help='list of installed packages' - ) + arguments.add_common_arguments(preview_parser, ['installed_specs']) preview_parser.set_defaults(func=preview) + # Check if binaries need to be rebuilt on remote mirror check = subparsers.add_parser('check', help=check_binaries.__doc__) check.add_argument( @@ -313,8 +305,10 @@ def _createtarball(env, spec_yaml, packages, directory, key, no_deps, force, tty.debug(yaml_text) s = Spec.from_yaml(yaml_text) packages.add('/{0}'.format(s.dag_hash())) + elif packages: packages = packages + else: tty.die("build cache file creation requires at least one" + " installed package argument or else path to a" + @@ -378,17 +372,17 @@ def createtarball(args): # restrict matching to current environment if one is active env = ev.get_env(args, 'buildcache create') - _createtarball(env, args.spec_yaml, args.packages, args.directory, + _createtarball(env, args.spec_yaml, args.specs, args.directory, args.key, args.no_deps, args.force, args.rel, args.unsigned, args.allow_root, args.no_rebuild_index) def installtarball(args): """install from a binary package""" - if not args.packages: + if not args.specs: tty.die("build cache file installation requires" + " at least one package spec argument") - pkgs = set(args.packages) + pkgs = set(args.specs) matches = match_downloaded_specs(pkgs, args.multiple, args.force) for match in matches: @@ -422,8 +416,8 @@ def install_tarball(spec, args): def listspecs(args): """list binary packages available from mirrors""" specs = bindist.get_specs(args.force) - if args.packages: - constraints = set(args.packages) + if args.specs: + constraints = set(args.specs) specs = [s for s in specs if any(s.satisfies(c) for c in constraints)] display_specs(specs, args, all_headers=True) @@ -440,7 +434,7 @@ def preview(args): Args: args: command line arguments """ - specs = find_matching_specs(args.packages, allow_multiple_matches=True) + specs = find_matching_specs(args.specs, allow_multiple_matches=True) # Cycle over the specs that match for spec in specs: diff --git a/lib/spack/spack/cmd/checksum.py b/lib/spack/spack/cmd/checksum.py index c606cd3886..343915868c 100644 --- a/lib/spack/spack/cmd/checksum.py +++ b/lib/spack/spack/cmd/checksum.py @@ -10,6 +10,7 @@ import argparse import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.repo import spack.stage import spack.util.crypto @@ -22,12 +23,10 @@ level = "long" def setup_parser(subparser): - subparser.add_argument( - 'package', - help='package to checksum versions for') subparser.add_argument( '--keep-stage', action='store_true', help="don't clean up staging area when command completes") + arguments.add_common_arguments(subparser, ['package']) subparser.add_argument( 'versions', nargs=argparse.REMAINDER, help='versions to generate checksums for') diff --git a/lib/spack/spack/cmd/clean.py b/lib/spack/spack/cmd/clean.py index dc25857b51..791a1b7dc3 100644 --- a/lib/spack/spack/cmd/clean.py +++ b/lib/spack/spack/cmd/clean.py @@ -11,6 +11,7 @@ import llnl.util.tty as tty import spack.caches import spack.cmd +import spack.cmd.common.arguments as arguments import spack.repo import spack.stage from spack.paths import lib_path, var_path @@ -43,11 +44,7 @@ def setup_parser(subparser): subparser.add_argument( '-a', '--all', action=AllClean, help="equivalent to -sdmp", nargs=0 ) - subparser.add_argument( - 'specs', - nargs=argparse.REMAINDER, - help="removes the build stages and tarballs for specs" - ) + arguments.add_common_arguments(subparser, ['specs']) def clean(parser, args): diff --git a/lib/spack/spack/cmd/commands.py b/lib/spack/spack/cmd/commands.py index b4fde1aea4..4966bd7858 100644 --- a/lib/spack/spack/cmd/commands.py +++ b/lib/spack/spack/cmd/commands.py @@ -5,13 +5,16 @@ from __future__ import print_function -import sys +import argparse +import copy import os import re -import argparse +import sys import llnl.util.tty as tty -from llnl.util.argparsewriter import ArgparseWriter, ArgparseRstWriter +from llnl.util.argparsewriter import ( + ArgparseWriter, ArgparseRstWriter, ArgparseCompletionWriter +) from llnl.util.tty.colify import colify import spack.cmd @@ -35,6 +38,8 @@ def formatter(func): def setup_parser(subparser): + subparser.add_argument( + '-a', '--aliases', action='store_true', help='include command aliases') subparser.add_argument( '--format', default='names', choices=formatters, help='format to be used to print the output (default: names)') @@ -52,29 +57,97 @@ def setup_parser(subparser): class SpackArgparseRstWriter(ArgparseRstWriter): """RST writer tailored for spack documentation.""" - def __init__(self, documented_commands, out=sys.stdout): - super(SpackArgparseRstWriter, self).__init__(out) - self.documented = documented_commands if documented_commands else [] + def __init__(self, prog, out=sys.stdout, aliases=False, + documented_commands=[], + rst_levels=['-', '-', '^', '~', ':', '`']): + super(SpackArgparseRstWriter, self).__init__( + prog, out, aliases, rst_levels) + self.documented = documented_commands def usage(self, *args): - super(SpackArgparseRstWriter, self).usage(*args) - cmd = re.sub(' ', '-', self.parser.prog) + string = super(SpackArgparseRstWriter, self).usage(*args) + + cmd = self.parser.prog.replace(' ', '-') if cmd in self.documented: - self.line() - self.line(':ref:`More documentation `' % cmd) + string += '\n:ref:`More documentation `\n'.format(cmd) + + return string class SubcommandWriter(ArgparseWriter): - def begin_command(self, prog): - self.out.write(' ' * self.level + prog) - self.out.write('\n') + def format(self, cmd): + return ' ' * self.level + cmd.prog + '\n' + + +_positional_to_subroutine = { + 'package': '_all_packages', + 'spec': '_all_packages', + 'filter': '_all_packages', + 'installed': '_installed_packages', + 'compiler': '_installed_compilers', + 'section': '_config_sections', + 'env': '_environments', + 'extendable': '_extensions', + 'keys': '_keys', + 'help_command': '_subcommands', + 'mirror': '_mirrors', + 'virtual': '_providers', + 'namespace': '_repos', + 'hash': '_all_resource_hashes', + 'pytest': '_tests', +} + + +class BashCompletionWriter(ArgparseCompletionWriter): + """Write argparse output as bash programmable tab completion.""" + + def body(self, positionals, optionals, subcommands): + if positionals: + return """ + if $list_options + then + {0} + else + {1} + fi +""".format(self.optionals(optionals), self.positionals(positionals)) + elif subcommands: + return """ + if $list_options + then + {0} + else + {1} + fi +""".format(self.optionals(optionals), self.subcommands(subcommands)) + else: + return """ + {0} +""".format(self.optionals(optionals)) + + def positionals(self, positionals): + # If match found, return function name + for positional in positionals: + for key, value in _positional_to_subroutine.items(): + if positional.startswith(key): + return value + + # If no matches found, return empty list + return 'SPACK_COMPREPLY=""' + + def optionals(self, optionals): + return 'SPACK_COMPREPLY="{0}"'.format(' '.join(optionals)) + + def subcommands(self, subcommands): + return 'SPACK_COMPREPLY="{0}"'.format(' '.join(subcommands)) @formatter def subcommands(args, out): parser = spack.main.make_argument_parser() spack.main.add_all_commands(parser) - SubcommandWriter(out).write(parser) + writer = SubcommandWriter(parser.prog, out, args.aliases) + writer.write(parser) def rst_index(out): @@ -124,12 +197,28 @@ def rst(args, out): out.write('\n') # print sections for each command and subcommand - SpackArgparseRstWriter(documented_commands, out).write(parser, root=1) + writer = SpackArgparseRstWriter( + parser.prog, out, args.aliases, documented_commands) + writer.write(parser) @formatter def names(args, out): - colify(spack.cmd.all_commands(), output=out) + commands = copy.copy(spack.cmd.all_commands()) + + if args.aliases: + commands.extend(spack.main.aliases.keys()) + + colify(commands, output=out) + + +@formatter +def bash(args, out): + parser = spack.main.make_argument_parser() + spack.main.add_all_commands(parser) + + writer = BashCompletionWriter(parser.prog, out, args.aliases) + writer.write(parser) def prepend_header(args, out): @@ -148,12 +237,14 @@ def commands(parser, args): tty.die("No such file: '%s'" % args.header) # if we're updating an existing file, only write output if a command - # is newer than the file. + # or the header is newer than the file. if args.update: if os.path.exists(args.update): files = [ spack.cmd.get_module(command).__file__.rstrip('c') # pyc -> py for command in spack.cmd.all_commands()] + if args.header: + files.append(args.header) last_update = os.path.getmtime(args.update) if not any(os.path.getmtime(f) > last_update for f in files): tty.msg('File is up to date: %s' % args.update) diff --git a/lib/spack/spack/cmd/common/arguments.py b/lib/spack/spack/cmd/common/arguments.py index da8b0b0277..b93f265c7a 100644 --- a/lib/spack/spack/cmd/common/arguments.py +++ b/lib/spack/spack/cmd/common/arguments.py @@ -120,7 +120,7 @@ class SetParallelJobs(argparse.Action): class DeptypeAction(argparse.Action): - """Creates a tuple of valid dependency tpyes from a deptype argument.""" + """Creates a tuple of valid dependency types from a deptype argument.""" def __call__(self, parser, namespace, values, option_string=None): deptype = dep.all_deptypes if values: @@ -132,11 +132,53 @@ class DeptypeAction(argparse.Action): setattr(namespace, self.dest, deptype) +# TODO: merge constraint and installed_specs @arg def constraint(): return Args( 'constraint', nargs=argparse.REMAINDER, action=ConstraintAction, - help='constraint to select a subset of installed packages') + help='constraint to select a subset of installed packages', + metavar='installed_specs') + + +@arg +def package(): + return Args('package', help='package name') + + +@arg +def packages(): + return Args( + 'packages', nargs='+', help='one or more package names', + metavar='package') + + +# Specs must use `nargs=argparse.REMAINDER` because a single spec can +# contain spaces, and contain variants like '-mpi' that argparse thinks +# are a collection of optional flags. +@arg +def spec(): + return Args('spec', nargs=argparse.REMAINDER, help='package spec') + + +@arg +def specs(): + return Args( + 'specs', nargs=argparse.REMAINDER, help='one or more package specs') + + +@arg +def installed_spec(): + return Args( + 'spec', nargs=argparse.REMAINDER, help='installed package spec', + metavar='installed_spec') + + +@arg +def installed_specs(): + return Args( + 'specs', nargs=argparse.REMAINDER, + help='one or more installed package specs', metavar='installed_specs') @arg diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py index 9951f346cb..b6055a7f6b 100644 --- a/lib/spack/spack/cmd/config.py +++ b/lib/spack/spack/cmd/config.py @@ -34,7 +34,7 @@ def setup_parser(subparser): help="configuration section to print. " "options: %(choices)s", nargs='?', - metavar='SECTION', + metavar='section', choices=spack.config.section_schemas) blame_parser = sp.add_parser( @@ -42,14 +42,14 @@ def setup_parser(subparser): blame_parser.add_argument('section', help="configuration section to print. " "options: %(choices)s", - metavar='SECTION', + metavar='section', choices=spack.config.section_schemas) edit_parser = sp.add_parser('edit', help='edit configuration file') edit_parser.add_argument('section', help="configuration section to edit. " "options: %(choices)s", - metavar='SECTION', + metavar='section', nargs='?', choices=spack.config.section_schemas) edit_parser.add_argument( diff --git a/lib/spack/spack/cmd/configure.py b/lib/spack/spack/cmd/configure.py index d15d2be912..3df3c87413 100644 --- a/lib/spack/spack/cmd/configure.py +++ b/lib/spack/spack/cmd/configure.py @@ -7,6 +7,7 @@ import argparse import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.cmd.install as inst from spack.build_systems.autotools import AutotoolsPackage @@ -36,16 +37,12 @@ build_system_to_phase = { def setup_parser(subparser): - subparser.add_argument( - 'package', - nargs=argparse.REMAINDER, - help="spec of the package to install" - ) subparser.add_argument( '-v', '--verbose', action='store_true', help="print additional output during builds" ) + arguments.add_common_arguments(subparser, ['spec']) def _stop_at_phase_during_install(args, calling_fn, phase_mapping): @@ -64,15 +61,15 @@ def _stop_at_phase_during_install(args, calling_fn, phase_mapping): # Install package dependencies if needed parser = argparse.ArgumentParser() inst.setup_parser(parser) - tty.msg('Checking dependencies for {0}'.format(args.package[0])) + tty.msg('Checking dependencies for {0}'.format(args.spec[0])) cli_args = ['-v'] if args.verbose else [] install_args = parser.parse_args(cli_args + ['--only=dependencies']) - install_args.package = args.package + install_args.spec = args.spec inst.install(parser, install_args) # Install package and stop at the given phase cli_args = ['-v'] if args.verbose else [] install_args = parser.parse_args(cli_args + ['--only=package']) - install_args.package = args.package + install_args.spec = args.spec inst.install(parser, install_args, stop_at=phase) except IndexError: tty.error( diff --git a/lib/spack/spack/cmd/deactivate.py b/lib/spack/spack/cmd/deactivate.py index 43ef09a9b1..3c72531a9c 100644 --- a/lib/spack/spack/cmd/deactivate.py +++ b/lib/spack/spack/cmd/deactivate.py @@ -3,10 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment as ev import spack.store from spack.filesystem_view import YamlFilesystemView @@ -28,9 +28,7 @@ def setup_parser(subparser): '-a', '--all', action='store_true', help="deactivate all extensions of an extendable package, or " "deactivate an extension AND its dependencies") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="spec of package extension to deactivate") + arguments.add_common_arguments(subparser, ['installed_spec']) def deactivate(parser, args): diff --git a/lib/spack/spack/cmd/dependencies.py b/lib/spack/spack/cmd/dependencies.py index db8fbe4b48..e65e050bfa 100644 --- a/lib/spack/spack/cmd/dependencies.py +++ b/lib/spack/spack/cmd/dependencies.py @@ -3,8 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty from llnl.util.tty.colify import colify @@ -31,8 +29,7 @@ def setup_parser(subparser): subparser.add_argument( '-V', '--no-expand-virtuals', action='store_false', default=True, dest="expand_virtuals", help="do not expand virtual dependencies") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, help="spec or package name") + arguments.add_common_arguments(subparser, ['spec']) def dependencies(parser, args): diff --git a/lib/spack/spack/cmd/dependents.py b/lib/spack/spack/cmd/dependents.py index 78e862a982..e60733f589 100644 --- a/lib/spack/spack/cmd/dependents.py +++ b/lib/spack/spack/cmd/dependents.py @@ -3,15 +3,14 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty from llnl.util.tty.colify import colify +import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment as ev import spack.repo import spack.store -import spack.cmd description = "show packages that depend on another" section = "basic" @@ -26,8 +25,7 @@ def setup_parser(subparser): subparser.add_argument( '-t', '--transitive', action='store_true', default=False, help="Show all transitive dependents.") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, help="spec or package name") + arguments.add_common_arguments(subparser, ['spec']) def inverted_dependencies(): diff --git a/lib/spack/spack/cmd/dev_build.py b/lib/spack/spack/cmd/dev_build.py index 190720b05f..c1004f24b3 100644 --- a/lib/spack/spack/cmd/dev_build.py +++ b/lib/spack/spack/cmd/dev_build.py @@ -5,14 +5,13 @@ import sys import os -import argparse import llnl.util.tty as tty import spack.config import spack.cmd -import spack.repo import spack.cmd.common.arguments as arguments +import spack.repo from spack.stage import DIYStage description = "developer build: build from code in current working directory" @@ -41,9 +40,7 @@ def setup_parser(subparser): subparser.add_argument( '-u', '--until', type=str, dest='until', default=None, help="phase to stop after when installing (default None)") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="specs to use for install. must contain package AND version") + arguments.add_common_arguments(subparser, ['spec']) cd_group = subparser.add_mutually_exclusive_group() arguments.add_common_arguments(cd_group, ['clean', 'dirty']) diff --git a/lib/spack/spack/cmd/edit.py b/lib/spack/spack/cmd/edit.py index 1438383b2c..6cdc3b788d 100644 --- a/lib/spack/spack/cmd/edit.py +++ b/lib/spack/spack/cmd/edit.py @@ -84,12 +84,11 @@ def setup_parser(subparser): help="namespace of package to edit") subparser.add_argument( - 'name', nargs='?', default=None, - help="name of package to edit") + 'package', nargs='?', default=None, help="package name") def edit(parser, args): - name = args.name + name = args.package # By default, edit package files path = spack.paths.packages_path diff --git a/lib/spack/spack/cmd/env.py b/lib/spack/spack/cmd/env.py index 1d61dfc250..a8bc1e5bbe 100644 --- a/lib/spack/spack/cmd/env.py +++ b/lib/spack/spack/cmd/env.py @@ -157,7 +157,7 @@ def env_deactivate(args): def env_create_setup_parser(subparser): """create a new environment""" subparser.add_argument( - 'create_env', metavar='ENV', help='name of environment to create') + 'create_env', metavar='env', help='name of environment to create') subparser.add_argument( '-d', '--dir', action='store_true', help='create an environment in a specific directory') @@ -221,7 +221,7 @@ def _env_create(name_or_path, init_file=None, dir=False, with_view=None): def env_remove_setup_parser(subparser): """remove an existing environment""" subparser.add_argument( - 'rm_env', metavar='ENV', nargs='+', + 'rm_env', metavar='env', nargs='+', help='environment(s) to remove') arguments.add_common_arguments(subparser, ['yes_to_all']) diff --git a/lib/spack/spack/cmd/extensions.py b/lib/spack/spack/cmd/extensions.py index 7e3db66384..e834d7fd18 100644 --- a/lib/spack/spack/cmd/extensions.py +++ b/lib/spack/spack/cmd/extensions.py @@ -37,7 +37,7 @@ def setup_parser(subparser): subparser.add_argument( 'spec', nargs=argparse.REMAINDER, - help='spec of package to list extensions for') + help='spec of package to list extensions for', metavar='extendable') def extensions(parser, args): diff --git a/lib/spack/spack/cmd/fetch.py b/lib/spack/spack/cmd/fetch.py index 3004250f03..b91eb52ab8 100644 --- a/lib/spack/spack/cmd/fetch.py +++ b/lib/spack/spack/cmd/fetch.py @@ -3,14 +3,12 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.config import spack.repo -import spack.cmd.common.arguments as arguments description = "fetch archives for packages" section = "build" @@ -25,19 +23,17 @@ def setup_parser(subparser): subparser.add_argument( '-D', '--dependencies', action='store_true', help="also fetch all dependencies") - subparser.add_argument( - 'packages', nargs=argparse.REMAINDER, - help="specs of packages to fetch") + arguments.add_common_arguments(subparser, ['specs']) def fetch(parser, args): - if not args.packages: + if not args.specs: tty.die("fetch requires at least one package argument") if args.no_checksum: spack.config.set('config:checksum', False, scope='command_line') - specs = spack.cmd.parse_specs(args.packages, concretize=True) + specs = spack.cmd.parse_specs(args.specs, concretize=True) for spec in specs: if args.missing or args.dependencies: for s in spec.traverse(): diff --git a/lib/spack/spack/cmd/gpg.py b/lib/spack/spack/cmd/gpg.py index c1a0cafe45..0a77812c12 100644 --- a/lib/spack/spack/cmd/gpg.py +++ b/lib/spack/spack/cmd/gpg.py @@ -6,6 +6,7 @@ import os import argparse +import spack.cmd.common.arguments as arguments import spack.paths from spack.util.gpg import Gpg @@ -19,8 +20,7 @@ def setup_parser(subparser): subparsers = subparser.add_subparsers(help='GPG sub-commands') verify = subparsers.add_parser('verify', help=gpg_verify.__doc__) - verify.add_argument('package', type=str, - help='the package to verify') + arguments.add_common_arguments(verify, ['installed_spec']) verify.add_argument('signature', type=str, nargs='?', help='the signature file') verify.set_defaults(func=gpg_verify) @@ -44,8 +44,7 @@ def setup_parser(subparser): help='the key to use for signing') sign.add_argument('--clearsign', action='store_true', help='if specified, create a clearsign signature') - sign.add_argument('package', type=str, - help='the package to sign') + arguments.add_common_arguments(sign, ['installed_spec']) sign.set_defaults(func=gpg_sign) create = subparsers.add_parser('create', help=gpg_create.__doc__) @@ -122,9 +121,9 @@ def gpg_sign(args): 'please choose one') output = args.output if not output: - output = args.package + '.asc' + output = args.spec[0] + '.asc' # TODO: Support the package format Spack creates. - Gpg.sign(key, args.package, output, args.clearsign) + Gpg.sign(key, ' '.join(args.spec), output, args.clearsign) def gpg_trust(args): @@ -155,8 +154,8 @@ def gpg_verify(args): # TODO: Support the package format Spack creates. signature = args.signature if signature is None: - signature = args.package + '.asc' - Gpg.verify(signature, args.package) + signature = args.spec[0] + '.asc' + Gpg.verify(signature, ' '.join(args.spec)) def gpg(parser, args): diff --git a/lib/spack/spack/cmd/graph.py b/lib/spack/spack/cmd/graph.py index 94197283b8..d0fbf8e6c6 100644 --- a/lib/spack/spack/cmd/graph.py +++ b/lib/spack/spack/cmd/graph.py @@ -5,7 +5,6 @@ from __future__ import print_function -import argparse import llnl.util.tty as tty import spack.cmd @@ -38,11 +37,7 @@ def setup_parser(subparser): '-i', '--installed', action='store_true', help="graph all installed specs in dot format (implies --dot)") - arguments.add_common_arguments(subparser, ['deptype']) - - subparser.add_argument( - 'specs', nargs=argparse.REMAINDER, - help="specs of packages to graph") + arguments.add_common_arguments(subparser, ['deptype', 'specs']) def graph(parser, args): diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py index 413b96fe18..81a68dae96 100644 --- a/lib/spack/spack/cmd/info.py +++ b/lib/spack/spack/cmd/info.py @@ -11,6 +11,7 @@ from six.moves import zip_longest import llnl.util.tty.color as color from llnl.util.tty.colify import colify +import spack.cmd.common.arguments as arguments import spack.repo import spack.spec import spack.fetch_strategy as fs @@ -36,8 +37,7 @@ def padder(str_list, extra=0): def setup_parser(subparser): - subparser.add_argument( - 'name', metavar='PACKAGE', help='name of package to get info for') + arguments.add_common_arguments(subparser, ['package']) def section_title(s): @@ -237,5 +237,5 @@ def print_text_info(pkg): def info(parser, args): - pkg = spack.repo.get(args.name) + pkg = spack.repo.get(args.package) print_text_info(pkg) diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py index ace27577eb..18dad6108b 100644 --- a/lib/spack/spack/cmd/install.py +++ b/lib/spack/spack/cmd/install.py @@ -122,11 +122,6 @@ the dependencies""" cd_group = subparser.add_mutually_exclusive_group() arguments.add_common_arguments(cd_group, ['clean', 'dirty']) - subparser.add_argument( - 'package', - nargs=argparse.REMAINDER, - help="spec of the package to install" - ) testing = subparser.add_mutually_exclusive_group() testing.add_argument( '--test', default=None, @@ -157,7 +152,7 @@ packages. If neither are chosen, don't run tests for any packages.""" help="Show usage instructions for CDash reporting" ) add_cdash_args(subparser, False) - arguments.add_common_arguments(subparser, ['yes_to_all']) + arguments.add_common_arguments(subparser, ['yes_to_all', 'spec']) def add_cdash_args(subparser, add_help): @@ -258,7 +253,7 @@ environment variables: parser.print_help() return - if not args.package and not args.specfiles: + if not args.spec and not args.specfiles: # if there are no args but an active environment or spack.yaml file # then install the packages from it. env = ev.get_env(args, 'install') @@ -293,7 +288,7 @@ environment variables: if args.log_file: reporter.filename = args.log_file - abstract_specs = spack.cmd.parse_specs(args.package) + abstract_specs = spack.cmd.parse_specs(args.spec) tests = False if args.test == 'all' or args.run_tests: tests = True @@ -303,7 +298,7 @@ environment variables: try: specs = spack.cmd.parse_specs( - args.package, concretize=True, tests=tests) + args.spec, concretize=True, tests=tests) except SpackError as e: tty.debug(e) reporter.concretization_report(e.message) diff --git a/lib/spack/spack/cmd/load.py b/lib/spack/spack/cmd/load.py index 89dd61675e..9c48fe802a 100644 --- a/lib/spack/spack/cmd/load.py +++ b/lib/spack/spack/cmd/load.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse from spack.cmd.common import print_module_placeholder_help, arguments description = "add package to environment using `module load`" @@ -14,11 +13,8 @@ level = "short" def setup_parser(subparser): """Parser is only constructed so that this prints a nice help message with -h. """ - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="spec of package to load with modules " - ) - arguments.add_common_arguments(subparser, ['recurse_dependencies']) + arguments.add_common_arguments( + subparser, ['recurse_dependencies', 'installed_spec']) def load(parser, args): diff --git a/lib/spack/spack/cmd/location.py b/lib/spack/spack/cmd/location.py index a48ce85261..60978fe404 100644 --- a/lib/spack/spack/cmd/location.py +++ b/lib/spack/spack/cmd/location.py @@ -6,11 +6,11 @@ from __future__ import print_function import os -import argparse import llnl.util.tty as tty import spack.environment as ev import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment import spack.paths import spack.repo @@ -55,9 +55,7 @@ def setup_parser(subparser): '-e', '--env', action='store', help="location of an environment managed by spack") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="spec of package to fetch directory for") + arguments.add_common_arguments(subparser, ['spec']) def location(parser, args): diff --git a/lib/spack/spack/cmd/maintainers.py b/lib/spack/spack/cmd/maintainers.py index a1fb7716f9..a1cf477146 100644 --- a/lib/spack/spack/cmd/maintainers.py +++ b/lib/spack/spack/cmd/maintainers.py @@ -40,7 +40,7 @@ def setup_parser(subparser): # options for commands that take package arguments subparser.add_argument( - 'pkg_or_user', nargs=argparse.REMAINDER, + 'package_or_user', nargs=argparse.REMAINDER, help='names of packages or users to get info for') @@ -104,31 +104,31 @@ def maintainers(parser, args): if args.all: if args.by_user: - maintainers = maintainers_to_packages(args.pkg_or_user) + maintainers = maintainers_to_packages(args.package_or_user) for user, packages in sorted(maintainers.items()): color.cprint('@c{%s}: %s' % (user, ', '.join(sorted(packages)))) return 0 if maintainers else 1 else: - packages = packages_to_maintainers(args.pkg_or_user) + packages = packages_to_maintainers(args.package_or_user) for pkg, maintainers in sorted(packages.items()): color.cprint('@c{%s}: %s' % (pkg, ', '.join(sorted(maintainers)))) return 0 if packages else 1 if args.by_user: - if not args.pkg_or_user: + if not args.package_or_user: tty.die('spack maintainers --by-user requires a user or --all') - packages = union_values(maintainers_to_packages(args.pkg_or_user)) + packages = union_values(maintainers_to_packages(args.package_or_user)) colify(packages) return 0 if packages else 1 else: - if not args.pkg_or_user: + if not args.package_or_user: tty.die('spack maintainers requires a package or --all') - users = union_values(packages_to_maintainers(args.pkg_or_user)) + users = union_values(packages_to_maintainers(args.package_or_user)) colify(users) return 0 if users else 1 diff --git a/lib/spack/spack/cmd/mirror.py b/lib/spack/spack/cmd/mirror.py index 10f01fd363..5206927895 100644 --- a/lib/spack/spack/cmd/mirror.py +++ b/lib/spack/spack/cmd/mirror.py @@ -5,7 +5,6 @@ import sys -import argparse import llnl.util.tty as tty from llnl.util.tty.colify import colify @@ -39,9 +38,6 @@ def setup_parser(subparser): create_parser.add_argument('-d', '--directory', default=None, help="directory in which to create mirror") - create_parser.add_argument( - 'specs', nargs=argparse.REMAINDER, - help="specs of packages to put in mirror") create_parser.add_argument( '-a', '--all', action='store_true', help="mirror all versions of all packages in Spack, or all packages" @@ -57,6 +53,7 @@ def setup_parser(subparser): '-n', '--versions-per-spec', help="the number of versions to fetch for each spec, choose 'all' to" " retrieve all versions of each package") + arguments.add_common_arguments(create_parser, ['specs']) # used to construct scope arguments below scopes = spack.config.scopes() @@ -64,7 +61,8 @@ def setup_parser(subparser): # Add add_parser = sp.add_parser('add', help=mirror_add.__doc__) - add_parser.add_argument('name', help="mnemonic name for mirror") + add_parser.add_argument( + 'name', help="mnemonic name for mirror", metavar="mirror") add_parser.add_argument( 'url', help="url of mirror directory from 'spack mirror create'") add_parser.add_argument( @@ -75,7 +73,8 @@ def setup_parser(subparser): # Remove remove_parser = sp.add_parser('remove', aliases=['rm'], help=mirror_remove.__doc__) - remove_parser.add_argument('name') + remove_parser.add_argument( + 'name', help="mnemonic name for mirror", metavar="mirror") remove_parser.add_argument( '--scope', choices=scopes, metavar=scopes_metavar, default=spack.config.default_modify_scope(), @@ -83,7 +82,8 @@ def setup_parser(subparser): # Set-Url set_url_parser = sp.add_parser('set-url', help=mirror_set_url.__doc__) - set_url_parser.add_argument('name', help="mnemonic name for mirror") + set_url_parser.add_argument( + 'name', help="mnemonic name for mirror", metavar="mirror") set_url_parser.add_argument( 'url', help="url of mirror directory from 'spack mirror create'") set_url_parser.add_argument( diff --git a/lib/spack/spack/cmd/patch.py b/lib/spack/spack/cmd/patch.py index 9e7cc4164b..8f91edb8f1 100644 --- a/lib/spack/spack/cmd/patch.py +++ b/lib/spack/spack/cmd/patch.py @@ -3,8 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.repo @@ -18,20 +16,17 @@ level = "long" def setup_parser(subparser): - arguments.add_common_arguments(subparser, ['no_checksum']) - subparser.add_argument( - 'packages', nargs=argparse.REMAINDER, - help="specs of packages to stage") + arguments.add_common_arguments(subparser, ['no_checksum', 'specs']) def patch(parser, args): - if not args.packages: - tty.die("patch requires at least one package argument") + if not args.specs: + tty.die("patch requires at least one spec argument") if args.no_checksum: spack.config.set('config:checksum', False, scope='command_line') - specs = spack.cmd.parse_specs(args.packages, concretize=True) + specs = spack.cmd.parse_specs(args.specs, concretize=True) for spec in specs: package = spack.repo.get(spec) package.do_patch() diff --git a/lib/spack/spack/cmd/pkg.py b/lib/spack/spack/cmd/pkg.py index 86ff535d5e..b988d6a848 100644 --- a/lib/spack/spack/cmd/pkg.py +++ b/lib/spack/spack/cmd/pkg.py @@ -6,7 +6,6 @@ from __future__ import print_function import os -import argparse import re import llnl.util.tty as tty @@ -14,6 +13,7 @@ from llnl.util.tty.colify import colify from llnl.util.filesystem import working_dir import spack.cmd +import spack.cmd.common.arguments as arguments import spack.paths import spack.repo from spack.util.executable import which @@ -28,8 +28,7 @@ def setup_parser(subparser): metavar='SUBCOMMAND', dest='pkg_command') add_parser = sp.add_parser('add', help=pkg_add.__doc__) - add_parser.add_argument('packages', nargs=argparse.REMAINDER, - help="names of packages to add to git repo") + arguments.add_common_arguments(add_parser, ['packages']) list_parser = sp.add_parser('list', help=pkg_list.__doc__) list_parser.add_argument('rev', default='HEAD', nargs='?', diff --git a/lib/spack/spack/cmd/remove.py b/lib/spack/spack/cmd/remove.py index cce197af2e..049041ce83 100644 --- a/lib/spack/spack/cmd/remove.py +++ b/lib/spack/spack/cmd/remove.py @@ -3,11 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.environment as ev @@ -26,8 +25,7 @@ def setup_parser(subparser): subparser.add_argument( '-f', '--force', action='store_true', help="remove concretized spec (if any) immediately") - subparser.add_argument( - 'specs', nargs=argparse.REMAINDER, help="specs to be removed") + arguments.add_common_arguments(subparser, ['specs']) def remove(parser, args): diff --git a/lib/spack/spack/cmd/repo.py b/lib/spack/spack/cmd/repo.py index 019813fc9f..83acf796a2 100644 --- a/lib/spack/spack/cmd/repo.py +++ b/lib/spack/spack/cmd/repo.py @@ -51,8 +51,8 @@ def setup_parser(subparser): remove_parser = sp.add_parser( 'remove', help=repo_remove.__doc__, aliases=['rm']) remove_parser.add_argument( - 'path_or_namespace', - help="path or namespace of a Spack package repository") + 'namespace_or_path', + help="namespace or path of a Spack package repository") remove_parser.add_argument( '--scope', choices=scopes, metavar=scopes_metavar, default=spack.config.default_modify_scope(), @@ -101,10 +101,10 @@ def repo_add(args): def repo_remove(args): """Remove a repository from Spack's configuration.""" repos = spack.config.get('repos', scope=args.scope) - path_or_namespace = args.path_or_namespace + namespace_or_path = args.namespace_or_path # If the argument is a path, remove that repository from config. - canon_path = canonicalize_path(path_or_namespace) + canon_path = canonicalize_path(namespace_or_path) for repo_path in repos: repo_canon_path = canonicalize_path(repo_path) if canon_path == repo_canon_path: @@ -117,7 +117,7 @@ def repo_remove(args): for path in repos: try: repo = Repo(path) - if repo.namespace == path_or_namespace: + if repo.namespace == namespace_or_path: repos.remove(path) spack.config.set('repos', repos, args.scope) tty.msg("Removed repository %s with namespace '%s'." @@ -127,7 +127,7 @@ def repo_remove(args): continue tty.die("No repository with path or namespace: %s" - % path_or_namespace) + % namespace_or_path) def repo_list(args): diff --git a/lib/spack/spack/cmd/restage.py b/lib/spack/spack/cmd/restage.py index f74ef09a12..0f55884bfe 100644 --- a/lib/spack/spack/cmd/restage.py +++ b/lib/spack/spack/cmd/restage.py @@ -3,11 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.cmd +import spack.cmd.common.arguments as arguments import spack.repo description = "revert checked out package source code" @@ -16,15 +15,14 @@ level = "long" def setup_parser(subparser): - subparser.add_argument('packages', nargs=argparse.REMAINDER, - help="specs of packages to restage") + arguments.add_common_arguments(subparser, ['specs']) def restage(parser, args): - if not args.packages: + if not args.specs: tty.die("spack restage requires at least one package spec.") - specs = spack.cmd.parse_specs(args.packages, concretize=True) + specs = spack.cmd.parse_specs(args.specs, concretize=True) for spec in specs: package = spack.repo.get(spec) package.do_restage() diff --git a/lib/spack/spack/cmd/setup.py b/lib/spack/spack/cmd/setup.py index 3e4f9135d7..246e3b4275 100644 --- a/lib/spack/spack/cmd/setup.py +++ b/lib/spack/spack/cmd/setup.py @@ -30,13 +30,10 @@ def setup_parser(subparser): subparser.add_argument( '-i', '--ignore-dependencies', action='store_true', dest='ignore_deps', help="do not try to install dependencies of requested packages") - arguments.add_common_arguments(subparser, ['no_checksum']) + arguments.add_common_arguments(subparser, ['no_checksum', 'spec']) subparser.add_argument( '-v', '--verbose', action='store_true', dest='verbose', help="display verbose build output while installing") - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help="specs to use for install. must contain package AND version") cd_group = subparser.add_mutually_exclusive_group() arguments.add_common_arguments(cd_group, ['clean', 'dirty']) diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py index 85fe5a1a9e..fd03f09e57 100644 --- a/lib/spack/spack/cmd/spec.py +++ b/lib/spack/spack/cmd/spec.py @@ -5,7 +5,6 @@ from __future__ import print_function -import argparse import contextlib import sys @@ -47,8 +46,7 @@ for further documentation regarding the spec syntax, see: subparser.add_argument( '-t', '--types', action='store_true', default=False, help='show dependency types') - subparser.add_argument( - 'specs', nargs=argparse.REMAINDER, help="specs of packages") + arguments.add_common_arguments(subparser, ['specs']) @contextlib.contextmanager diff --git a/lib/spack/spack/cmd/stage.py b/lib/spack/spack/cmd/stage.py index 9c0d3ad63c..1acefb723c 100644 --- a/lib/spack/spack/cmd/stage.py +++ b/lib/spack/spack/cmd/stage.py @@ -3,8 +3,6 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse - import llnl.util.tty as tty import spack.environment as ev @@ -18,14 +16,11 @@ level = "long" def setup_parser(subparser): - arguments.add_common_arguments(subparser, ['no_checksum']) + arguments.add_common_arguments(subparser, ['no_checksum', 'specs']) subparser.add_argument( '-p', '--path', dest='path', help="path to stage package, does not add to spack tree") - subparser.add_argument( - 'specs', nargs=argparse.REMAINDER, help="specs of packages to stage") - def stage(parser, args): if not args.specs: diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py index 906da8d3b3..0ad42f4dfb 100644 --- a/lib/spack/spack/cmd/uninstall.py +++ b/lib/spack/spack/cmd/uninstall.py @@ -5,7 +5,6 @@ from __future__ import print_function -import argparse import sys import spack.cmd @@ -38,17 +37,13 @@ display_args = { } -def add_common_arguments(subparser): +def setup_parser(subparser): subparser.add_argument( '-f', '--force', action='store_true', dest='force', help="remove regardless of whether other packages or environments " "depend on this one") arguments.add_common_arguments( - subparser, ['recurse_dependents', 'yes_to_all']) - - -def setup_parser(subparser): - add_common_arguments(subparser) + subparser, ['recurse_dependents', 'yes_to_all', 'installed_specs']) subparser.add_argument( '-a', '--all', action='store_true', dest='all', help="USE CAREFULLY. Remove ALL installed packages that match each " @@ -58,11 +53,6 @@ def setup_parser(subparser): "If used in an environment, all packages in the environment " "will be uninstalled.") - subparser.add_argument( - 'packages', - nargs=argparse.REMAINDER, - help="specs of packages to uninstall") - def find_matching_specs(env, specs, allow_multiple_matches=False, force=False): """Returns a list of specs matching the not necessarily @@ -351,10 +341,10 @@ def confirm_removal(specs): def uninstall(parser, args): - if not args.packages and not args.all: + if not args.specs and not args.all: tty.die('uninstall requires at least one package argument.', ' Use `spack uninstall --all` to uninstall ALL packages.') # [any] here handles the --all case by forcing all specs to be returned - specs = spack.cmd.parse_specs(args.packages) if args.packages else [any] + specs = spack.cmd.parse_specs(args.specs) if args.specs else [any] uninstall_specs(args, specs) diff --git a/lib/spack/spack/cmd/unload.py b/lib/spack/spack/cmd/unload.py index 581b37a013..92a25478b6 100644 --- a/lib/spack/spack/cmd/unload.py +++ b/lib/spack/spack/cmd/unload.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import argparse -from spack.cmd.common import print_module_placeholder_help +from spack.cmd.common import print_module_placeholder_help, arguments description = "remove package from environment using `module unload`" section = "modules" @@ -14,9 +13,7 @@ level = "short" def setup_parser(subparser): """Parser is only constructed so that this prints a nice help message with -h. """ - subparser.add_argument( - 'spec', nargs=argparse.REMAINDER, - help='spec of package to unload with modules') + arguments.add_common_arguments(subparser, ['installed_spec']) def unload(parser, args): diff --git a/lib/spack/spack/cmd/verify.py b/lib/spack/spack/cmd/verify.py index 9a38284691..b20d795ce5 100644 --- a/lib/spack/spack/cmd/verify.py +++ b/lib/spack/spack/cmd/verify.py @@ -25,8 +25,8 @@ def setup_parser(subparser): help="Ouptut json-formatted errors") subparser.add_argument('-a', '--all', action='store_true', help="Verify all packages") - subparser.add_argument('files_or_specs', nargs=argparse.REMAINDER, - help="Files or specs to verify") + subparser.add_argument('specs_or_files', nargs=argparse.REMAINDER, + help="Specs or files to verify") type = subparser.add_mutually_exclusive_group() type.add_argument( @@ -47,7 +47,7 @@ def verify(parser, args): setup_parser.parser.print_help() return 1 - for file in args.files_or_specs: + for file in args.specs_or_files: results = spack.verify.check_file_manifest(file) if results.has_errors(): if args.json: @@ -57,21 +57,21 @@ def verify(parser, args): return 0 else: - spec_args = spack.cmd.parse_specs(args.files_or_specs) + spec_args = spack.cmd.parse_specs(args.specs_or_files) if args.all: query = spack.store.db.query_local if local else spack.store.db.query # construct spec list if spec_args: - spec_list = spack.cmd.parse_specs(args.files_or_specs) + spec_list = spack.cmd.parse_specs(args.specs_or_files) specs = [] for spec in spec_list: specs += query(spec, installed=True) else: specs = query(installed=True) - elif args.files_or_specs: + elif args.specs_or_files: # construct disambiguated spec list env = ev.get_env(args, 'verify') specs = list(map(lambda x: spack.cmd.disambiguate_spec(x, env, diff --git a/lib/spack/spack/cmd/versions.py b/lib/spack/spack/cmd/versions.py index c12e7d6290..723f89ce08 100644 --- a/lib/spack/spack/cmd/versions.py +++ b/lib/spack/spack/cmd/versions.py @@ -5,11 +5,13 @@ from __future__ import print_function +import sys + from llnl.util.tty.colify import colify import llnl.util.tty as tty +import spack.cmd.common.arguments as arguments import spack.repo -import sys description = "list available versions of a package" section = "packaging" @@ -17,10 +19,9 @@ level = "long" def setup_parser(subparser): - subparser.add_argument('package', metavar='PACKAGE', - help='package to list versions for') subparser.add_argument('-s', '--safe-only', action='store_true', help='only list safe versions of the package') + arguments.add_common_arguments(subparser, ['package']) def versions(parser, args): diff --git a/lib/spack/spack/reporters/cdash.py b/lib/spack/spack/reporters/cdash.py index 178747706a..580df7866f 100644 --- a/lib/spack/spack/reporters/cdash.py +++ b/lib/spack/spack/reporters/cdash.py @@ -72,8 +72,8 @@ class CDash(Reporter): tty.verbose("Using CDash auth token from environment") self.authtoken = os.environ.get('SPACK_CDASH_AUTH_TOKEN') - if args.package: - packages = args.package + if args.spec: + packages = args.spec else: packages = [] for file in args.specfiles: diff --git a/lib/spack/spack/test/cmd/commands.py b/lib/spack/spack/test/cmd/commands.py index c8ec60d823..2fe62b9bba 100644 --- a/lib/spack/spack/test/cmd/commands.py +++ b/lib/spack/spack/test/cmd/commands.py @@ -3,13 +3,17 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) -import re -import pytest +import filecmp +import os +import subprocess -from llnl.util.argparsewriter import ArgparseWriter +import pytest import spack.cmd +from spack.cmd.commands import _positional_to_subroutine import spack.main +import spack.paths + commands = spack.main.SpackCommand('commands') @@ -17,38 +21,64 @@ parser = spack.main.make_argument_parser() spack.main.add_all_commands(parser) -def test_commands_by_name(): +def test_names(): """Test default output of spack commands.""" - out = commands() - assert out.strip().split('\n') == sorted(spack.cmd.all_commands()) + out1 = commands().strip().split('\n') + assert out1 == spack.cmd.all_commands() + assert 'rm' not in out1 + out2 = commands('--aliases').strip().split('\n') + assert out1 != out2 + assert 'rm' in out2 -def test_subcommands(): - """Test subcommand traversal.""" - out = commands('--format=subcommands') - assert 'spack mirror create' in out - assert 'spack buildcache list' in out - assert 'spack repo add' in out - assert 'spack pkg diff' in out - assert 'spack url parse' in out - assert 'spack view symlink' in out + out3 = commands('--format=names').strip().split('\n') + assert out1 == out3 - class Subcommands(ArgparseWriter): - def begin_command(self, prog): - assert prog in out - Subcommands().write(parser) +def test_subcommands(): + """Test subcommand traversal.""" + out1 = commands('--format=subcommands') + assert 'spack mirror create' in out1 + assert 'spack buildcache list' in out1 + assert 'spack repo add' in out1 + assert 'spack pkg diff' in out1 + assert 'spack url parse' in out1 + assert 'spack view symlink' in out1 + assert 'spack rm' not in out1 + assert 'spack compiler add' not in out1 + + out2 = commands('--aliases', '--format=subcommands') + assert 'spack mirror create' in out2 + assert 'spack buildcache list' in out2 + assert 'spack repo add' in out2 + assert 'spack pkg diff' in out2 + assert 'spack url parse' in out2 + assert 'spack view symlink' in out2 + assert 'spack rm' in out2 + assert 'spack compiler add' in out2 def test_rst(): """Do some simple sanity checks of the rst writer.""" - out = commands('--format=rst') - - class Subcommands(ArgparseWriter): - def begin_command(self, prog): - assert prog in out - assert re.sub(r' ', '-', prog) in out - Subcommands().write(parser) + out1 = commands('--format=rst') + assert 'spack mirror create' in out1 + assert 'spack buildcache list' in out1 + assert 'spack repo add' in out1 + assert 'spack pkg diff' in out1 + assert 'spack url parse' in out1 + assert 'spack view symlink' in out1 + assert 'spack rm' not in out1 + assert 'spack compiler add' not in out1 + + out2 = commands('--aliases', '--format=rst') + assert 'spack mirror create' in out2 + assert 'spack buildcache list' in out2 + assert 'spack repo add' in out2 + assert 'spack pkg diff' in out2 + assert 'spack url parse' in out2 + assert 'spack view symlink' in out2 + assert 'spack rm' in out2 + assert 'spack compiler add' in out2 def test_rst_with_input_files(tmpdir): @@ -109,3 +139,91 @@ def test_rst_update(tmpdir): assert update_file.exists() with update_file.open() as f: assert f.read() == 'empty\n' + + +def test_update_with_header(tmpdir): + update_file = tmpdir.join('output') + + # not yet created when commands is run + commands('--update', str(update_file)) + assert update_file.exists() + with update_file.open() as f: + assert f.read() + fake_header = 'this is a header!\n\n' + + filename = tmpdir.join('header.txt') + with filename.open('w') as f: + f.write(fake_header) + + # created, newer than commands, but older than header + commands('--update', str(update_file), '--header', str(filename)) + + # newer than commands and header + commands('--update', str(update_file), '--header', str(filename)) + + +@pytest.mark.xfail +def test_no_pipe_error(): + """Make sure we don't see any pipe errors when piping output.""" + + proc = subprocess.Popen( + ['spack', 'commands', '--format=rst'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Call close() on stdout to cause a broken pipe + proc.stdout.close() + proc.wait() + stderr = proc.stderr.read().decode('utf-8') + + assert 'Broken pipe' not in stderr + + +def test_bash_completion(): + """Test the bash completion writer.""" + out1 = commands('--format=bash') + + # Make sure header not included + assert '_bash_completion_spack() {' not in out1 + assert '_all_packages() {' not in out1 + + # Make sure subcommands appear + assert '_spack_remove() {' in out1 + assert '_spack_compiler_find() {' in out1 + + # Make sure aliases don't appear + assert '_spack_rm() {' not in out1 + assert '_spack_compiler_add() {' not in out1 + + # Make sure options appear + assert '-h --help' in out1 + + # Make sure subcommands are called + for function in _positional_to_subroutine.values(): + assert function in out1 + + out2 = commands('--aliases', '--format=bash') + + # Make sure aliases appear + assert '_spack_rm() {' in out2 + assert '_spack_compiler_add() {' in out2 + + +def test_updated_completion_scripts(tmpdir): + """Make sure our shell tab completion scripts remain up-to-date.""" + + msg = ("It looks like Spack's command-line interface has been modified. " + "Please update Spack's shell tab completion scripts by running:\n\n" + " share/spack/qa/update-completion-scripts.sh\n\n" + "and adding the changed files to your pull request.") + + for shell in ['bash']: # 'zsh', 'fish']: + header = os.path.join( + spack.paths.share_path, shell, 'spack-completion.in') + script = 'spack-completion.{0}'.format(shell) + old_script = os.path.join(spack.paths.share_path, script) + new_script = str(tmpdir.join(script)) + + commands('--aliases', '--format', shell, + '--header', header, '--update', new_script) + + assert filecmp.cmp(old_script, new_script), msg diff --git a/lib/spack/spack/test/llnl/util/argparsewriter.py b/lib/spack/spack/test/llnl/util/argparsewriter.py new file mode 100644 index 0000000000..127149bbaa --- /dev/null +++ b/lib/spack/spack/test/llnl/util/argparsewriter.py @@ -0,0 +1,37 @@ +# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +"""Tests for ``llnl/util/argparsewriter.py`` + +These tests are fairly minimal, and ArgparseWriter is more extensively +tested in ``cmd/commands.py``. +""" + +import pytest + +import llnl.util.argparsewriter as aw + +import spack.main + + +parser = spack.main.make_argument_parser() +spack.main.add_all_commands(parser) + + +def test_format_not_overridden(): + writer = aw.ArgparseWriter('spack') + + with pytest.raises(NotImplementedError): + writer.write(parser) + + +def test_completion_format_not_overridden(): + writer = aw.ArgparseCompletionWriter('spack') + + assert writer.positionals([]) == '' + assert writer.optionals([]) == '' + assert writer.subcommands([]) == '' + + writer.write(parser) -- cgit v1.2.3-70-g09d2