refactor: clean up spack find, make spack find -dp work properly

Refactor `spack.cmd.display_specs()` and `spack find` so that any options
can be used together with -d. This cleans up the display logic
considerably, as there are no longer multiple "modes".
This commit is contained in:
Todd Gamblin 2019-09-01 22:36:54 -07:00
parent 987d8cbaaa
commit d4bad06202
4 changed files with 161 additions and 182 deletions

View File

@ -201,115 +201,31 @@ def gray_hash(spec, length):
return colorize('@K{%s}' % h) return colorize('@K{%s}' % h)
def display_formatted_specs(specs, format_string, deps=False):
"""Print a list of specs formatted with the provided string.
Arguments:
specs (list): list of specs to display.
deps (bool): whether to also print dependencies of specs.
"""
for spec in specs:
print(spec.format(format_string))
if deps:
for depth, dep in spec.traverse(depth=True, root=False):
print(" " * depth, dep.format(format_string))
def display_specs_as_json(specs, deps=False): def display_specs_as_json(specs, deps=False):
"""Convert specs to a list of json records.""" """Convert specs to a list of json records."""
seen = set() seen = set()
records = [] records = []
for spec in specs: for spec in specs:
if spec.dag_hash() not in seen: if spec.dag_hash() in seen:
seen.add(spec.dag_hash()) continue
records.append(spec.to_record_dict()) seen.add(spec.dag_hash())
records.append(spec.to_record_dict())
if deps: if deps:
for dep in spec.traverse(): for dep in spec.traverse():
if dep.dag_hash() not in seen: if dep.dag_hash() in seen:
seen.add(spec.dag_hash()) continue
records.append(dep.to_record_dict()) seen.add(dep.dag_hash())
records.append(dep.to_record_dict())
sjson.dump(records, sys.stdout) sjson.dump(records, sys.stdout)
def display_specs(specs, args=None, **kwargs): def iter_sections(specs, indent, all_headers):
"""Display human readable specs with customizable formatting. """Break a list of specs into sections indexed by arch/compiler."""
Prints the supplied specs to the screen, formatted according to the
arguments provided.
Specs are grouped by architecture and compiler, and columnized if
possible. There are three possible "modes":
* ``short`` (default): short specs with name and version, columnized
* ``paths``: Two columns: one for specs, one for paths
* ``deps``: Dependency-tree style, like ``spack spec``; can get long
Options can add more information to the default display. Options can
be provided either as keyword arguments or as an argparse namespace.
Keyword arguments take precedence over settings in the argparse
namespace.
Args:
specs (list of spack.spec.Spec): the specs to display
args (optional argparse.Namespace): namespace containing
formatting arguments
Keyword Args:
mode (str): Either 'short', 'paths', or 'deps'
long (bool): Display short hashes with specs
very_long (bool): Display full hashes with specs (supersedes ``long``)
namespace (bool): Print namespaces along with names
show_flags (bool): Show compiler flags with specs
variants (bool): Show variants with specs
indent (int): indent each line this much
decorators (dict): dictionary mappng specs to decorators
header_callback (function): called at start of arch/compiler sections
all_headers (bool): show headers even when arch/compiler aren't defined
"""
def get_arg(name, default=None):
"""Prefer kwargs, then args, then default."""
if name in kwargs:
return kwargs.get(name)
elif args is not None:
return getattr(args, name, default)
else:
return default
mode = get_arg('mode', 'short')
hashes = get_arg('long', False)
namespace = get_arg('namespace', False)
flags = get_arg('show_flags', False)
full_compiler = get_arg('show_full_compiler', False)
variants = get_arg('variants', False)
all_headers = get_arg('all_headers', False)
decorator = get_arg('decorator', None)
if decorator is None:
decorator = lambda s, f: f
indent = get_arg('indent', 0)
ispace = indent * ' '
hlen = 7
if get_arg('very_long', False):
hashes = True
hlen = None
nfmt = '{namespace}.{name}' if namespace else '{name}'
ffmt = ''
if full_compiler or flags:
ffmt += '{%compiler.name}'
if full_compiler:
ffmt += '{@compiler.version}'
ffmt += ' {compiler_flags}'
vfmt = '{variants}' if variants else ''
format_string = nfmt + '{@version}' + ffmt + vfmt
# Make a dict with specs keyed by architecture and compiler. # Make a dict with specs keyed by architecture and compiler.
index = index_by(specs, ('architecture', 'compiler')) index = index_by(specs, ('architecture', 'compiler'))
transform = {'package': decorator, 'fullpackage': decorator} ispace = indent * ' '
# Traverse the index and print out each package # Traverse the index and print out each package
for i, (architecture, compiler) in enumerate(sorted(index)): for i, (architecture, compiler) in enumerate(sorted(index)):
@ -331,57 +247,131 @@ def get_arg(name, default=None):
specs = index[(architecture, compiler)] specs = index[(architecture, compiler)]
specs.sort() specs.sort()
yield specs
if mode == 'paths':
# Print one spec per line along with prefix path
abbreviated = [s.cformat(format_string, transform=transform)
for s in specs]
width = max(len(s) for s in abbreviated)
width += 2
for abbrv, spec in zip(abbreviated, specs): def display_specs(specs, args=None, **kwargs):
# optional hash prefix for paths """Display human readable specs with customizable formatting.
h = gray_hash(spec, hlen) if hashes else ''
# only show prefix for concrete specs Prints the supplied specs to the screen, formatted according to the
prefix = spec.prefix if spec.concrete else '' arguments provided.
# print it all out at once Specs are grouped by architecture and compiler, and columnized if
fmt = "%%s%%s %%-%ds%%s" % width possible.
print(fmt % (ispace, h, abbrv, prefix))
elif mode == 'deps': Options can add more information to the default display. Options can
for spec in specs: be provided either as keyword arguments or as an argparse namespace.
print(spec.tree( Keyword arguments take precedence over settings in the argparse
format=format_string, namespace.
indent=4,
prefix=(lambda s: gray_hash(s, hlen)) if hashes else None))
elif mode == 'short': Args:
def fmt(s): specs (list of spack.spec.Spec): the specs to display
string = "" args (optional argparse.Namespace): namespace containing
if hashes: formatting arguments
string += gray_hash(s, hlen) + ' '
string += s.cformat(
nfmt + '{@version}' + vfmt, transform=transform)
return string
if not flags and not full_compiler: Keyword Args:
# Print columns of output if not printing flags paths (bool): Show paths with each displayed spec
colify((fmt(s) for s in specs), indent=indent) deps (bool): Display dependencies with specs
long (bool): Display short hashes with specs
else: very_long (bool): Display full hashes with specs (supersedes ``long``)
# Print one entry per line if including flags namespace (bool): Print namespaces along with names
for spec in specs: show_flags (bool): Show compiler flags with specs
# Print the hash if necessary variants (bool): Show variants with specs
hsh = gray_hash(spec, hlen) + ' ' if hashes else '' indent (int): indent each line this much
print(ispace + hsh + spec.cformat( sections (bool): display specs grouped by arch/compiler (default True)
format_string, transform=transform)) decorators (dict): dictionary mappng specs to decorators
header_callback (function): called at start of arch/compiler sections
all_headers (bool): show headers even when arch/compiler aren't defined
"""
def get_arg(name, default=None):
"""Prefer kwargs, then args, then default."""
if name in kwargs:
return kwargs.get(name)
elif args is not None:
return getattr(args, name, default)
else: else:
raise ValueError( return default
"Invalid mode for display_specs: %s. Must be one of (paths,"
"deps, short)." % mode) paths = get_arg('paths', False)
deps = get_arg('deps', False)
hashes = get_arg('long', False)
namespace = get_arg('namespace', False)
flags = get_arg('show_flags', False)
full_compiler = get_arg('show_full_compiler', False)
variants = get_arg('variants', False)
sections = get_arg('sections', True)
all_headers = get_arg('all_headers', False)
decorator = get_arg('decorator', None)
if decorator is None:
decorator = lambda s, f: f
indent = get_arg('indent', 0)
hlen = 7
if get_arg('very_long', False):
hashes = True
hlen = None
format_string = get_arg('format', None)
if format_string is None:
nfmt = '{namespace}.{name}' if namespace else '{name}'
ffmt = ''
if full_compiler or flags:
ffmt += '{%compiler.name}'
if full_compiler:
ffmt += '{@compiler.version}'
ffmt += ' {compiler_flags}'
vfmt = '{variants}' if variants else ''
format_string = nfmt + '{@version}' + ffmt + vfmt
transform = {'package': decorator, 'fullpackage': decorator}
def fmt(s, depth=0):
"""Formatter function for all output specs"""
string = ""
if hashes:
string += gray_hash(s, hlen) + ' '
string += depth * " "
string += s.cformat(format_string, transform=transform)
return string
def format_list(specs):
"""Display a single list of specs, with no sections"""
# create the final, formatted versions of all specs
formatted = []
for spec in specs:
formatted.append((fmt(spec), spec))
if deps:
for depth, dep in spec.traverse(root=False, depth=True):
formatted.append((fmt(dep, depth), dep))
formatted.append(('', None)) # mark newlines
# unless any of these are set, we can just colify and be done.
if not any((deps, paths)):
colify((f[0] for f in formatted), indent=indent)
return
# otherwise, we'll print specs one by one
max_width = max(len(f[0]) for f in formatted)
path_fmt = "%%-%ds%%s" % (max_width + 2)
for string, spec in formatted:
if not string:
print() # print newline from above
continue
if paths:
print(path_fmt % (string, spec.prefix))
else:
print(string)
if sections:
for specs in iter_sections(specs, indent, all_headers):
format_list(specs)
else:
format_list(sorted(specs))
def spack_is_git_repo(): def spack_is_git_repo():

View File

@ -9,8 +9,7 @@
from llnl.util.tty.colify import colify from llnl.util.tty.colify import colify
import spack.environment as ev import spack.environment as ev
import spack.cmd import spack.cmd as cmd
import spack.cmd.find
import spack.repo import spack.repo
import spack.store import spack.store
from spack.filesystem_view import YamlFilesystemView from spack.filesystem_view import YamlFilesystemView
@ -21,16 +20,16 @@
def setup_parser(subparser): def setup_parser(subparser):
format_group = subparser.add_mutually_exclusive_group() subparser.add_argument(
format_group.add_argument(
'-l', '--long', action='store_true', dest='long', '-l', '--long', action='store_true', dest='long',
help='show dependency hashes as well as versions') help='show dependency hashes as well as versions')
format_group.add_argument(
'-p', '--paths', action='store_const', dest='mode', const='paths', subparser.add_argument('-d', '--deps', action='store_true',
help='show paths to extension install directories') help='output dependencies along with found specs')
format_group.add_argument(
'-d', '--deps', action='store_const', dest='mode', const='deps', subparser.add_argument('-p', '--paths', action='store_true',
help='show full dependency DAG of extensions') help='show paths to package install directories')
subparser.add_argument( subparser.add_argument(
'-s', '--show', dest='show', metavar='TYPE', type=str, '-s', '--show', dest='show', metavar='TYPE', type=str,
default='all', default='all',
@ -69,7 +68,7 @@ def extensions(parser, args):
# #
# Checks # Checks
# #
spec = spack.cmd.parse_specs(args.spec) spec = cmd.parse_specs(args.spec)
if len(spec) > 1: if len(spec) > 1:
tty.die("Can only list extensions for one package.") tty.die("Can only list extensions for one package.")
@ -77,14 +76,11 @@ def extensions(parser, args):
tty.die("%s is not an extendable package." % spec[0].name) tty.die("%s is not an extendable package." % spec[0].name)
env = ev.get_env(args, 'extensions') env = ev.get_env(args, 'extensions')
spec = spack.cmd.disambiguate_spec(spec[0], env) spec = cmd.disambiguate_spec(spec[0], env)
if not spec.package.extendable: if not spec.package.extendable:
tty.die("%s does not have extensions." % spec.short_spec) tty.die("%s does not have extensions." % spec.short_spec)
if not args.mode:
args.mode = 'short'
if show_packages: if show_packages:
# #
# List package names of extensions # List package names of extensions
@ -116,7 +112,7 @@ def extensions(parser, args):
tty.msg("None installed.") tty.msg("None installed.")
else: else:
tty.msg("%d installed:" % len(installed)) tty.msg("%d installed:" % len(installed))
spack.cmd.find.display_specs(installed, mode=args.mode) cmd.display_specs(installed, args)
if show_activated: if show_activated:
# #
@ -129,5 +125,4 @@ def extensions(parser, args):
tty.msg("None activated.") tty.msg("None activated.")
else: else:
tty.msg("%d currently activated:" % len(activated)) tty.msg("%d currently activated:" % len(activated))
spack.cmd.find.display_specs( cmd.display_specs(activated.values(), args)
activated.values(), mode=args.mode, long=args.long)

View File

@ -22,17 +22,6 @@
def setup_parser(subparser): def setup_parser(subparser):
format_group = subparser.add_mutually_exclusive_group() format_group = subparser.add_mutually_exclusive_group()
format_group.add_argument('-s', '--short',
action='store_const',
dest='mode',
const='short',
default='short',
help='show only specs (default)')
format_group.add_argument('-p', '--paths',
action='store_const',
dest='mode',
const='paths',
help='show paths to package install directories')
format_group.add_argument( format_group.add_argument(
"--format", action="store", default=None, "--format", action="store", default=None,
help="output specs with the specified format string") help="output specs with the specified format string")
@ -40,15 +29,17 @@ def setup_parser(subparser):
"--json", action="store_true", default=False, "--json", action="store_true", default=False,
help="output specs as machine-readable json records") help="output specs as machine-readable json records")
# TODO: separate this entirely from the "mode" option -- it's subparser.add_argument('-d', '--deps', action='store_true',
# TODO: orthogonal, but changing it for all commands that use it with help='output dependencies along with found specs')
# TODO: display_spec is tricky. Also make -pd work together properly.
subparser.add_argument('-p', '--paths', action='store_true',
help='show paths to package install directories')
subparser.add_argument( subparser.add_argument(
'-d', '--deps', '--sections', action='store_true', default=None, dest='sections',
action='store_const', help='group specs in arch/compiler sections (default on)')
dest='mode', subparser.add_argument(
const='deps', '--no-sections', action='store_false', default=None, dest='sections',
help='output dependencies along with found specs') help='do not group specs by arch/compiler')
arguments.add_common_arguments( arguments.add_common_arguments(
subparser, ['long', 'very_long', 'tags']) subparser, ['long', 'very_long', 'tags'])
@ -186,6 +177,10 @@ def find(parser, args):
if env: if env:
decorator, added, roots, removed = setup_env(env) decorator, added, roots, removed = setup_env(env)
# use sections by default except with format.
if args.sections is None:
args.sections = not args.format
# Exit early if no package matches the constraint # Exit early if no package matches the constraint
if not results and args.constraint: if not results and args.constraint:
msg = "No package matches the query: {0}" msg = "No package matches the query: {0}"
@ -199,13 +194,12 @@ def find(parser, args):
results = [x for x in results if x.name in packages_with_tags] results = [x for x in results if x.name in packages_with_tags]
# Display the result # Display the result
if args.format: if args.json:
cmd.display_formatted_specs( cmd.display_specs_as_json(results, deps=args.deps)
results, args.format, deps=(args.mode == "deps"))
elif args.json:
cmd.display_specs_as_json(results, deps=(args.mode == "deps"))
else: else:
if env: if env:
display_env(env, args, decorator) display_env(env, args, decorator)
tty.msg("%s" % plural(len(results), 'installed package')) if args.sections:
cmd.display_specs(results, args, decorator=decorator, all_headers=True) tty.msg("%s" % plural(len(results), 'installed package'))
cmd.display_specs(
results, args, decorator=decorator, all_headers=True)

View File

@ -35,7 +35,7 @@ def mock_display(monkeypatch, specs):
def display(x, *args, **kwargs): def display(x, *args, **kwargs):
specs.extend(x) specs.extend(x)
monkeypatch.setattr(spack.cmd.find, 'display_specs', display) monkeypatch.setattr(spack.cmd, 'display_specs', display)
def test_query_arguments(): def test_query_arguments():