diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/spack/cmd/info.py | 217 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/info.py | 2 |
2 files changed, 180 insertions, 39 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) |