summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/cmd/info.py217
-rw-r--r--lib/spack/spack/test/cmd/info.py2
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)