summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2023-11-10 03:31:28 -0800
committerGitHub <noreply@github.com>2023-11-10 12:31:28 +0100
commitf0ced1af42c521ffac780a117f64e01d40c82d27 (patch)
treecd37b2b2251d1e6ea62e2f2332c7f0cfdd661b0d
parent2e45edf4e371966e5f0f0f03183bb150ecbd23f1 (diff)
downloadspack-f0ced1af42c521ffac780a117f64e01d40c82d27.tar.gz
spack-f0ced1af42c521ffac780a117f64e01d40c82d27.tar.bz2
spack-f0ced1af42c521ffac780a117f64e01d40c82d27.tar.xz
spack-f0ced1af42c521ffac780a117f64e01d40c82d27.zip
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.
-rw-r--r--lib/spack/spack/cmd/info.py217
-rw-r--r--lib/spack/spack/test/cmd/info.py2
-rwxr-xr-xshare/spack/spack-completion.bash2
-rwxr-xr-xshare/spack/spack-completion.fish4
4 files changed, 184 insertions, 41 deletions
diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py
index 5e667f4876..dd56c25451 100644
--- a/lib/spack/spack/cmd/info.py
+++ b/lib/spack/spack/cmd/info.py
@@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import sys
import textwrap
from itertools import zip_longest
@@ -16,6 +17,7 @@ import spack.fetch_strategy as fs
import spack.install_test
import spack.repo
import spack.spec
+import spack.version
from spack.package_base import preferred_version
description = "get detailed information on a particular package"
@@ -53,6 +55,7 @@ def setup_parser(subparser):
("--tags", print_tags.__doc__),
("--tests", print_tests.__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:
subparser.add_argument(opt, action="store_true", help=help_comment)
@@ -77,35 +80,10 @@ def license(s):
class VariantFormatter:
- def __init__(self, variants):
- self.variants = variants
+ def __init__(self, pkg):
+ self.variants = pkg.variants
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
_, cols = tty.terminal_size()
max_name = min(self.column_widths[0], 30)
@@ -137,6 +115,8 @@ class VariantFormatter:
def lines(self):
if not self.variants:
yield " None"
+ return
+
else:
yield " " + self.fmt % self.headers
underline = tuple([w * "=" for w in self.column_widths])
@@ -271,15 +251,165 @@ def print_tests(pkg):
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"""
+ if not pkg.variants:
+ print(" None")
+ return
+
color.cprint("")
color.cprint(section_title("Variants:"))
- formatter = VariantFormatter(pkg.variants)
- for line in formatter.lines:
- color.cprint(color.cescape(line))
+ variants_by_name = _variants_by_name_when(pkg)
+
+ # 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):
@@ -300,18 +430,24 @@ def print_versions(pkg):
pad = padder(pkg.versions, 4)
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)
- color.cprint(line)
+ color.cwrite(line)
+
+ print()
safe = []
deprecated = []
for v in reversed(sorted(pkg.versions)):
if pkg.has_code:
- url = fs.for_package_version(pkg, v)
+ url = get_url(v)
if pkg.versions[v].get("deprecated", False):
deprecated.append((v, url))
else:
@@ -384,7 +520,12 @@ def info(parser, args):
else:
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
sections = [
@@ -392,7 +533,7 @@ def info(parser, args):
(args.all or args.detectable, print_detectable),
(args.all or args.tags, print_tags),
(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 not args.no_dependencies, print_dependencies),
(args.all or args.virtuals, print_virtuals),
diff --git a/lib/spack/spack/test/cmd/info.py b/lib/spack/spack/test/cmd/info.py
index c4528f9852..5748323d8c 100644
--- a/lib/spack/spack/test/cmd/info.py
+++ b/lib/spack/spack/test/cmd/info.py
@@ -25,7 +25,7 @@ def parser():
def print_buffer(monkeypatch):
buffer = []
- def _print(*args):
+ def _print(*args, **kwargs):
buffer.extend(args)
monkeypatch.setattr(spack.cmd.info.color, "cprint", _print, raising=False)
diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash
index 20bb886b10..e84fe10134 100755
--- a/share/spack/spack-completion.bash
+++ b/share/spack/spack-completion.bash
@@ -1267,7 +1267,7 @@ _spack_help() {
_spack_info() {
if $list_options
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
_all_packages
fi
diff --git a/share/spack/spack-completion.fish b/share/spack/spack-completion.fish
index 769768c04c..d660c251af 100755
--- a/share/spack/spack-completion.fish
+++ b/share/spack/spack-completion.fish
@@ -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'
# 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 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'
@@ -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 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 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
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