info: rework spack info command to display variants better (#40998)
This changes variant display to use a much more legible format, and to use screen space much better (particularly on narrow terminals). It also adds color the variant display to match other parts of `spack info`. Descriptions and variant value lists that were frequently squished into a tiny column before now have closer to the full terminal width. This change also preserves any whitespace formatting present in `package.py`, so package maintainers can make easer-to-read descriptions of variant values if they want. For example, `gasnet` has had a nice description of the `conduits` variant for a while, but it was wrapped and made illegible by `spack info`. That is now fixed and the original newlines are kept. Conditional variants are grouped by their when clauses by default, but if you do not like the grouping, you can display all the variants in order with `--variants-by-name`. I'm not sure when people will prefer this, but it makes it easier to tell that a particular variant is/isn't there. I do think grouping by `when` is the better default.
This commit is contained in:
parent
3c3476a176
commit
fe57ec2ab7
4 changed files with 184 additions and 41 deletions
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
import spack.install_test
|
import spack.install_test
|
||||||
import spack.repo
|
import spack.repo
|
||||||
import spack.spec
|
import spack.spec
|
||||||
|
import spack.version
|
||||||
from spack.package_base import preferred_version
|
from spack.package_base import preferred_version
|
||||||
|
|
||||||
description = "get detailed information on a particular package"
|
description = "get detailed information on a particular package"
|
||||||
|
@ -53,6 +55,7 @@ def setup_parser(subparser):
|
||||||
("--tags", print_tags.__doc__),
|
("--tags", print_tags.__doc__),
|
||||||
("--tests", print_tests.__doc__),
|
("--tests", print_tests.__doc__),
|
||||||
("--virtuals", print_virtuals.__doc__),
|
("--virtuals", print_virtuals.__doc__),
|
||||||
|
("--variants-by-name", "list variants in strict name order; don't group by condition"),
|
||||||
]
|
]
|
||||||
for opt, help_comment in options:
|
for opt, help_comment in options:
|
||||||
subparser.add_argument(opt, action="store_true", help=help_comment)
|
subparser.add_argument(opt, action="store_true", help=help_comment)
|
||||||
|
@ -77,35 +80,10 @@ def license(s):
|
||||||
|
|
||||||
|
|
||||||
class VariantFormatter:
|
class VariantFormatter:
|
||||||
def __init__(self, variants):
|
def __init__(self, pkg):
|
||||||
self.variants = variants
|
self.variants = pkg.variants
|
||||||
self.headers = ("Name [Default]", "When", "Allowed values", "Description")
|
self.headers = ("Name [Default]", "When", "Allowed values", "Description")
|
||||||
|
|
||||||
# Formats
|
|
||||||
fmt_name = "{0} [{1}]"
|
|
||||||
|
|
||||||
# Initialize column widths with the length of the
|
|
||||||
# corresponding headers, as they cannot be shorter
|
|
||||||
# than that
|
|
||||||
self.column_widths = [len(x) for x in self.headers]
|
|
||||||
|
|
||||||
# Expand columns based on max line lengths
|
|
||||||
for k, e in variants.items():
|
|
||||||
v, w = e
|
|
||||||
candidate_max_widths = (
|
|
||||||
len(fmt_name.format(k, self.default(v))), # Name [Default]
|
|
||||||
len(str(w)),
|
|
||||||
len(v.allowed_values), # Allowed values
|
|
||||||
len(v.description), # Description
|
|
||||||
)
|
|
||||||
|
|
||||||
self.column_widths = (
|
|
||||||
max(self.column_widths[0], candidate_max_widths[0]),
|
|
||||||
max(self.column_widths[1], candidate_max_widths[1]),
|
|
||||||
max(self.column_widths[2], candidate_max_widths[2]),
|
|
||||||
max(self.column_widths[3], candidate_max_widths[3]),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Don't let name or possible values be less than max widths
|
# Don't let name or possible values be less than max widths
|
||||||
_, cols = tty.terminal_size()
|
_, cols = tty.terminal_size()
|
||||||
max_name = min(self.column_widths[0], 30)
|
max_name = min(self.column_widths[0], 30)
|
||||||
|
@ -137,6 +115,8 @@ def default(self, v):
|
||||||
def lines(self):
|
def lines(self):
|
||||||
if not self.variants:
|
if not self.variants:
|
||||||
yield " None"
|
yield " None"
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
else:
|
||||||
yield " " + self.fmt % self.headers
|
yield " " + self.fmt % self.headers
|
||||||
underline = tuple([w * "=" for w in self.column_widths])
|
underline = tuple([w * "=" for w in self.column_widths])
|
||||||
|
@ -271,15 +251,165 @@ def print_tests(pkg):
|
||||||
color.cprint(" None")
|
color.cprint(" None")
|
||||||
|
|
||||||
|
|
||||||
def print_variants(pkg):
|
def _fmt_value(v):
|
||||||
|
if v is None or isinstance(v, bool):
|
||||||
|
return str(v).lower()
|
||||||
|
else:
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_name_and_default(variant):
|
||||||
|
"""Print colorized name [default] for a variant."""
|
||||||
|
return color.colorize(f"@c{{{variant.name}}} @C{{[{_fmt_value(variant.default)}]}}")
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_when(when, indent):
|
||||||
|
return color.colorize(f"{indent * ' '}@B{{when}} {color.cescape(when)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_variant_description(variant, width, indent):
|
||||||
|
"""Format a variant's description, preserving explicit line breaks."""
|
||||||
|
return "\n".join(
|
||||||
|
textwrap.fill(
|
||||||
|
line, width=width, initial_indent=indent * " ", subsequent_indent=indent * " "
|
||||||
|
)
|
||||||
|
for line in variant.description.split("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_variant(variant, max_name_default_len, indent, when=None, out=None):
|
||||||
|
out = out or sys.stdout
|
||||||
|
|
||||||
|
_, cols = tty.terminal_size()
|
||||||
|
|
||||||
|
name_and_default = _fmt_name_and_default(variant)
|
||||||
|
name_default_len = color.clen(name_and_default)
|
||||||
|
|
||||||
|
values = variant.values
|
||||||
|
if not isinstance(variant.values, (tuple, list, spack.variant.DisjointSetsOfValues)):
|
||||||
|
values = [variant.values]
|
||||||
|
|
||||||
|
# put 'none' first, sort the rest by value
|
||||||
|
sorted_values = sorted(values, key=lambda v: (v != "none", v))
|
||||||
|
|
||||||
|
pad = 4 # min padding between 'name [default]' and values
|
||||||
|
value_indent = (indent + max_name_default_len + pad) * " " # left edge of values
|
||||||
|
|
||||||
|
# This preserves any formatting (i.e., newlines) from how the description was
|
||||||
|
# written in package.py, but still wraps long lines for small terminals.
|
||||||
|
# This allows some packages to provide detailed help on their variants (see, e.g., gasnet).
|
||||||
|
formatted_values = "\n".join(
|
||||||
|
textwrap.wrap(
|
||||||
|
f"{', '.join(_fmt_value(v) for v in sorted_values)}",
|
||||||
|
width=cols - 2,
|
||||||
|
initial_indent=value_indent,
|
||||||
|
subsequent_indent=value_indent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
formatted_values = formatted_values[indent + name_default_len + pad :]
|
||||||
|
|
||||||
|
# name [default] value1, value2, value3, ...
|
||||||
|
padding = pad * " "
|
||||||
|
color.cprint(f"{indent * ' '}{name_and_default}{padding}@c{{{formatted_values}}}", stream=out)
|
||||||
|
|
||||||
|
# when <spec>
|
||||||
|
description_indent = indent + 4
|
||||||
|
if when is not None and when != spack.spec.Spec():
|
||||||
|
out.write(_fmt_when(when, description_indent - 2))
|
||||||
|
out.write("\n")
|
||||||
|
|
||||||
|
# description, preserving explicit line breaks from the way it's written in the package file
|
||||||
|
out.write(_fmt_variant_description(variant, cols - 2, description_indent))
|
||||||
|
out.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _variants_by_name_when(pkg):
|
||||||
|
"""Adaptor to get variants keyed by { name: { when: { [Variant...] } }."""
|
||||||
|
# TODO: replace with pkg.variants_by_name(when=True) when unified directive dicts are merged.
|
||||||
|
variants = {}
|
||||||
|
for name, (variant, whens) in pkg.variants.items():
|
||||||
|
for when in whens:
|
||||||
|
variants.setdefault(name, {}).setdefault(when, []).append(variant)
|
||||||
|
return variants
|
||||||
|
|
||||||
|
|
||||||
|
def _variants_by_when_name(pkg):
|
||||||
|
"""Adaptor to get variants keyed by { when: { name: Variant } }"""
|
||||||
|
# TODO: replace with pkg.variants when unified directive dicts are merged.
|
||||||
|
variants = {}
|
||||||
|
for name, (variant, whens) in pkg.variants.items():
|
||||||
|
for when in whens:
|
||||||
|
variants.setdefault(when, {})[name] = variant
|
||||||
|
return variants
|
||||||
|
|
||||||
|
|
||||||
|
def _print_variants_header(pkg):
|
||||||
"""output variants"""
|
"""output variants"""
|
||||||
|
|
||||||
|
if not pkg.variants:
|
||||||
|
print(" None")
|
||||||
|
return
|
||||||
|
|
||||||
color.cprint("")
|
color.cprint("")
|
||||||
color.cprint(section_title("Variants:"))
|
color.cprint(section_title("Variants:"))
|
||||||
|
|
||||||
formatter = VariantFormatter(pkg.variants)
|
variants_by_name = _variants_by_name_when(pkg)
|
||||||
for line in formatter.lines:
|
|
||||||
color.cprint(color.cescape(line))
|
# Calculate the max length of the "name [default]" part of the variant display
|
||||||
|
# This lets us know where to print variant values.
|
||||||
|
max_name_default_len = max(
|
||||||
|
color.clen(_fmt_name_and_default(variant))
|
||||||
|
for name, when_variants in variants_by_name.items()
|
||||||
|
for variants in when_variants.values()
|
||||||
|
for variant in variants
|
||||||
|
)
|
||||||
|
|
||||||
|
return max_name_default_len, variants_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def _unconstrained_ver_first(item):
|
||||||
|
"""sort key that puts specs with open version ranges first"""
|
||||||
|
spec, _ = item
|
||||||
|
return (spack.version.any_version not in spec.versions, spec)
|
||||||
|
|
||||||
|
|
||||||
|
def print_variants_grouped_by_when(pkg):
|
||||||
|
max_name_default_len, _ = _print_variants_header(pkg)
|
||||||
|
|
||||||
|
indent = 4
|
||||||
|
variants = _variants_by_when_name(pkg)
|
||||||
|
for when, variants_by_name in sorted(variants.items(), key=_unconstrained_ver_first):
|
||||||
|
padded_values = max_name_default_len + 4
|
||||||
|
start_indent = indent
|
||||||
|
|
||||||
|
if when != spack.spec.Spec():
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.write(_fmt_when(when, indent))
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
# indent names slightly inside 'when', but line up values
|
||||||
|
padded_values -= 2
|
||||||
|
start_indent += 2
|
||||||
|
|
||||||
|
for name, variant in sorted(variants_by_name.items()):
|
||||||
|
_fmt_variant(variant, padded_values, start_indent, None, out=sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def print_variants_by_name(pkg):
|
||||||
|
max_name_default_len, variants_by_name = _print_variants_header(pkg)
|
||||||
|
max_name_default_len += 4
|
||||||
|
|
||||||
|
indent = 4
|
||||||
|
for name, when_variants in variants_by_name.items():
|
||||||
|
for when, variants in sorted(when_variants.items(), key=_unconstrained_ver_first):
|
||||||
|
for variant in variants:
|
||||||
|
_fmt_variant(variant, max_name_default_len, indent, when, out=sys.stdout)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def print_variants(pkg):
|
||||||
|
"""output variants"""
|
||||||
|
print_variants_grouped_by_when(pkg)
|
||||||
|
|
||||||
|
|
||||||
def print_versions(pkg):
|
def print_versions(pkg):
|
||||||
|
@ -300,18 +430,24 @@ def print_versions(pkg):
|
||||||
pad = padder(pkg.versions, 4)
|
pad = padder(pkg.versions, 4)
|
||||||
|
|
||||||
preferred = preferred_version(pkg)
|
preferred = preferred_version(pkg)
|
||||||
url = ""
|
|
||||||
if pkg.has_code:
|
|
||||||
url = fs.for_package_version(pkg, preferred)
|
|
||||||
|
|
||||||
|
def get_url(version):
|
||||||
|
try:
|
||||||
|
return fs.for_package_version(pkg, version)
|
||||||
|
except spack.fetch_strategy.InvalidArgsError:
|
||||||
|
return "No URL"
|
||||||
|
|
||||||
|
url = get_url(preferred) if pkg.has_code else ""
|
||||||
line = version(" {0}".format(pad(preferred))) + color.cescape(url)
|
line = version(" {0}".format(pad(preferred))) + color.cescape(url)
|
||||||
color.cprint(line)
|
color.cwrite(line)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
safe = []
|
safe = []
|
||||||
deprecated = []
|
deprecated = []
|
||||||
for v in reversed(sorted(pkg.versions)):
|
for v in reversed(sorted(pkg.versions)):
|
||||||
if pkg.has_code:
|
if pkg.has_code:
|
||||||
url = fs.for_package_version(pkg, v)
|
url = get_url(v)
|
||||||
if pkg.versions[v].get("deprecated", False):
|
if pkg.versions[v].get("deprecated", False):
|
||||||
deprecated.append((v, url))
|
deprecated.append((v, url))
|
||||||
else:
|
else:
|
||||||
|
@ -384,7 +520,12 @@ def info(parser, args):
|
||||||
else:
|
else:
|
||||||
color.cprint(" None")
|
color.cprint(" None")
|
||||||
|
|
||||||
color.cprint(section_title("Homepage: ") + pkg.homepage)
|
if getattr(pkg, "homepage"):
|
||||||
|
color.cprint(section_title("Homepage: ") + pkg.homepage)
|
||||||
|
|
||||||
|
_print_variants = (
|
||||||
|
print_variants_by_name if args.variants_by_name else print_variants_grouped_by_when
|
||||||
|
)
|
||||||
|
|
||||||
# Now output optional information in expected order
|
# Now output optional information in expected order
|
||||||
sections = [
|
sections = [
|
||||||
|
@ -392,7 +533,7 @@ def info(parser, args):
|
||||||
(args.all or args.detectable, print_detectable),
|
(args.all or args.detectable, print_detectable),
|
||||||
(args.all or args.tags, print_tags),
|
(args.all or args.tags, print_tags),
|
||||||
(args.all or not args.no_versions, print_versions),
|
(args.all or not args.no_versions, print_versions),
|
||||||
(args.all or not args.no_variants, print_variants),
|
(args.all or not args.no_variants, _print_variants),
|
||||||
(args.all or args.phases, print_phases),
|
(args.all or args.phases, print_phases),
|
||||||
(args.all or not args.no_dependencies, print_dependencies),
|
(args.all or not args.no_dependencies, print_dependencies),
|
||||||
(args.all or args.virtuals, print_virtuals),
|
(args.all or args.virtuals, print_virtuals),
|
||||||
|
|
|
@ -25,7 +25,7 @@ def parser():
|
||||||
def print_buffer(monkeypatch):
|
def print_buffer(monkeypatch):
|
||||||
buffer = []
|
buffer = []
|
||||||
|
|
||||||
def _print(*args):
|
def _print(*args, **kwargs):
|
||||||
buffer.extend(args)
|
buffer.extend(args)
|
||||||
|
|
||||||
monkeypatch.setattr(spack.cmd.info.color, "cprint", _print, raising=False)
|
monkeypatch.setattr(spack.cmd.info.color, "cprint", _print, raising=False)
|
||||||
|
|
|
@ -1267,7 +1267,7 @@ _spack_help() {
|
||||||
_spack_info() {
|
_spack_info() {
|
||||||
if $list_options
|
if $list_options
|
||||||
then
|
then
|
||||||
SPACK_COMPREPLY="-h --help -a --all --detectable --maintainers --no-dependencies --no-variants --no-versions --phases --tags --tests --virtuals"
|
SPACK_COMPREPLY="-h --help -a --all --detectable --maintainers --no-dependencies --no-variants --no-versions --phases --tags --tests --virtuals --variants-by-name"
|
||||||
else
|
else
|
||||||
_all_packages
|
_all_packages
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1855,7 +1855,7 @@ complete -c spack -n '__fish_spack_using_command help' -l spec -f -a guide
|
||||||
complete -c spack -n '__fish_spack_using_command help' -l spec -d 'help on the package specification syntax'
|
complete -c spack -n '__fish_spack_using_command help' -l spec -d 'help on the package specification syntax'
|
||||||
|
|
||||||
# spack info
|
# spack info
|
||||||
set -g __fish_spack_optspecs_spack_info h/help a/all detectable maintainers no-dependencies no-variants no-versions phases tags tests virtuals
|
set -g __fish_spack_optspecs_spack_info h/help a/all detectable maintainers no-dependencies no-variants no-versions phases tags tests virtuals variants-by-name
|
||||||
complete -c spack -n '__fish_spack_using_command_pos 0 info' -f -a '(__fish_spack_packages)'
|
complete -c spack -n '__fish_spack_using_command_pos 0 info' -f -a '(__fish_spack_packages)'
|
||||||
complete -c spack -n '__fish_spack_using_command info' -s h -l help -f -a help
|
complete -c spack -n '__fish_spack_using_command info' -s h -l help -f -a help
|
||||||
complete -c spack -n '__fish_spack_using_command info' -s h -l help -d 'show this help message and exit'
|
complete -c spack -n '__fish_spack_using_command info' -s h -l help -d 'show this help message and exit'
|
||||||
|
@ -1879,6 +1879,8 @@ complete -c spack -n '__fish_spack_using_command info' -l tests -f -a tests
|
||||||
complete -c spack -n '__fish_spack_using_command info' -l tests -d 'output relevant build-time and stand-alone tests'
|
complete -c spack -n '__fish_spack_using_command info' -l tests -d 'output relevant build-time and stand-alone tests'
|
||||||
complete -c spack -n '__fish_spack_using_command info' -l virtuals -f -a virtuals
|
complete -c spack -n '__fish_spack_using_command info' -l virtuals -f -a virtuals
|
||||||
complete -c spack -n '__fish_spack_using_command info' -l virtuals -d 'output virtual packages'
|
complete -c spack -n '__fish_spack_using_command info' -l virtuals -d 'output virtual packages'
|
||||||
|
complete -c spack -n '__fish_spack_using_command info' -l variants-by-name -f -a variants_by_name
|
||||||
|
complete -c spack -n '__fish_spack_using_command info' -l variants-by-name -d 'list variants in strict name order; don\'t group by condition'
|
||||||
|
|
||||||
# spack install
|
# spack install
|
||||||
set -g __fish_spack_optspecs_spack_install h/help only= u/until= j/jobs= overwrite fail-fast keep-prefix keep-stage dont-restage use-cache no-cache cache-only use-buildcache= include-build-deps no-check-signature show-log-on-error source n/no-checksum deprecated v/verbose fake only-concrete add no-add f/file= clean dirty test= log-format= log-file= help-cdash cdash-upload-url= cdash-build= cdash-site= cdash-track= cdash-buildstamp= y/yes-to-all U/fresh reuse reuse-deps
|
set -g __fish_spack_optspecs_spack_install h/help only= u/until= j/jobs= overwrite fail-fast keep-prefix keep-stage dont-restage use-cache no-cache cache-only use-buildcache= include-build-deps no-check-signature show-log-on-error source n/no-checksum deprecated v/verbose fake only-concrete add no-add f/file= clean dirty test= log-format= log-file= help-cdash cdash-upload-url= cdash-build= cdash-site= cdash-track= cdash-buildstamp= y/yes-to-all U/fresh reuse reuse-deps
|
||||||
|
|
Loading…
Reference in a new issue