Compare commits

...

3 Commits

Author SHA1 Message Date
Todd Gamblin
690fad1182
WIP 2025-03-25 22:34:39 -07:00
Todd Gamblin
13446994ab
WIP 2025-03-25 22:34:39 -07:00
Todd Gamblin
327462e8e2
info: generify when-grouping code
We want to show dependencies grouped by conditions, as we already do
with variants. This takes the first step and generifies the variant
display code.
2025-03-25 22:34:39 -07:00
2 changed files with 180 additions and 88 deletions

View File

@ -5,6 +5,7 @@
import sys
import textwrap
from itertools import zip_longest
from typing import Callable, Dict, TypeVar
import llnl.util.tty as tty
import llnl.util.tty.color as color
@ -14,11 +15,12 @@
import spack.deptypes as dt
import spack.fetch_strategy as fs
import spack.install_test
import spack.package_base
import spack.repo
import spack.spec
import spack.variant
from spack.cmd.common import arguments
from spack.package_base import preferred_version
from spack.util.typing import SupportsRichComparison
description = "get detailed information on a particular package"
section = "basic"
@ -28,6 +30,44 @@
plain_format = "@."
class Formatter:
"""Generic formatter for elements displayed by `spack info`.
Elements have four parts: name, values, when condition, and description. They can
be formatted two ways (shown here for variants)::
Grouped by when (default)::
when +cuda
cuda_arch [none] none, 10, 100, 100a, 101,
101a, 11, 12, 120, 120a, 13
CUDA architecture
Or, by name (each name has a when nested under it)::
cuda_arch [none] none, 10, 100, 100a, 101,
101a, 11, 12, 120, 120a, 13
when +cuda
CUDA architecture
The values and description will be wrapped if needed. the name (and any additional info)
will not (so they should be kept short).
Subclasses are responsible for generating colorized text, but not wrapping,
indentation, or other formatting, for the name, values, and description.
"""
def format_name(self, element) -> str:
return ""
def format_values(self, element) -> str:
return ""
def format_description(self, element) -> str:
return ""
def padder(str_list, extra=0):
"""Return a function to pad elements of a list."""
length = max(len(str(s)) for s in str_list) + extra
@ -140,17 +180,19 @@ def lines(self):
yield " " + self.fmt % t
class DependencyFormatter(Formatter):
def format_name(self, dep) -> str:
return str(dep.spec)
def format_values(self, dep) -> str:
return str(dt.flag_to_tuple(dep.depflag))
def print_dependencies(pkg, args):
"""output build, link, and run package dependencies"""
for deptype in ("build", "link", "run"):
color.cprint("")
color.cprint(section_title("%s Dependencies:" % deptype.capitalize()))
deps = sorted(pkg.dependencies_of_type(dt.flag_from_string(deptype)))
if deps:
colify(deps, indent=4)
else:
color.cprint(" None")
print_fn = print_by_name if args.variants_by_name else print_grouped_by_when
print_fn("Dependencies", pkg.dependencies, DependencyFormatter())
def print_detectable(pkg, args):
@ -263,40 +305,21 @@ def print_tests(pkg, args):
color.cprint(" None")
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: "spack.spec.Spec", indent: int):
return color.colorize(f"{indent * ' '}@B{{when}} {color.cescape(str(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_value(v):
return str(v).lower() if v is None or isinstance(v, bool) else str(v)
class VariantFormatter(Formatter):
def format_name(self, variant) -> str:
return color.colorize(
f"@c{{{variant.name}}} @C{{[{_fmt_variant_value(variant.default)}]}}"
)
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)
def format_values(self, variant) -> str:
values = variant.values
if not isinstance(variant.values, (tuple, list, spack.variant.DisjointSetsOfValues)):
values = [variant.values]
@ -304,25 +327,48 @@ def _fmt_variant(variant, max_name_default_len, indent, when=None, out=None):
# 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
return color.colorize(f"@c{{{', '.join(_fmt_variant_value(v) for v in sorted_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).
def format_description(self, variant) -> str:
return variant.description
def _fmt_definition(
name_field, values_field, description, max_name_len, indent, when=None, out=None
):
"""Format a definition entry in `spack info` output.
Arguments:
name_field: name and optional info, e.g. a default; should be short.
values_field: possible values for the entry; Wrapped if long.
description: description of the field (wrapped if overly long)
indent: size of leading indent for entry
when: optional when condition
out: stream to print to
"""
out = out or sys.stdout
_, cols = tty.terminal_size()
name_len = color.clen(name_field)
pad = 4 # min padding between name and values
value_indent = (indent + max_name_len + pad) * " " # left edge of values
if values_field:
formatted_values = "\n".join(
textwrap.wrap(
f"{', '.join(_fmt_value(v) for v in sorted_values)}",
values_field,
width=cols - 2,
initial_indent=value_indent,
subsequent_indent=value_indent,
)
)
formatted_values = formatted_values[indent + name_default_len + pad :]
# trim initial indentation
formatted_values = formatted_values[indent + name_len + pad :]
# name [default] value1, value2, value3, ...
padding = pad * " "
color.cprint(f"{indent * ' '}{name_and_default}{padding}@c{{{formatted_values}}}", stream=out)
out.write(f"{indent * ' '}{name_field}{pad * ' '}{formatted_values}\n")
# when <spec>
description_indent = indent + 4
@ -330,38 +376,65 @@ def _fmt_variant(variant, max_name_default_len, indent, when=None, out=None):
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))
# description, preserving explicit line breaks from the way it's written in the
# package file, but still wrapoing long lines for small terminals. This allows
# descriptions to provide detailed help in descriptions (see, e.g., gasnet's variants).
if description:
formatted_description = "\n".join(
textwrap.fill(
line,
width=cols - 2,
initial_indent=description_indent * " ",
subsequent_indent=description_indent * " ",
)
for line in description.split("\n")
)
out.write(formatted_description)
out.write("\n")
def _print_variants_header(pkg):
"""output variants"""
K = TypeVar("K", bound=SupportsRichComparison)
V = TypeVar("V")
if not pkg.variants:
print(" None")
return
def print_header(header: str, when_indexed_dictionary: Dict, formatter: Formatter):
color.cprint("")
color.cprint(section_title("Variants:"))
color.cprint(section_title(f"{header}:"))
# 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 in pkg.variant_names()
for _, variant in pkg.variant_definitions(name)
if not when_indexed_dictionary:
print(" None")
def max_name_length(when_indexed_dictionary: Dict, formatter: Formatter) -> int:
# Calculate the max length of the first field of the definition. Lets us know how
# much to pad other fields on the first line.
return max(
color.clen(formatter.format_name(definition))
for subkey in spack.package_base._subkeys(when_indexed_dictionary)
for _, definition in spack.package_base._definitions(when_indexed_dictionary, subkey)
)
return max_name_default_len
def print_grouped_by_when(header: str, when_indexed_dictionary: Dict, formatter: Formatter):
"""Generic method to print metadata grouped by when conditions."""
def print_variants_grouped_by_when(pkg):
max_name_default_len = _print_variants_header(pkg)
print_header(header, when_indexed_dictionary, formatter)
if not when_indexed_dictionary:
return
max_name_len = max_name_length(when_indexed_dictionary, formatter)
# Calculate the max length of the first field of the definition. Lets us know how
# much to pad other fields on the first line.
max_name_len = max(
color.clen(formatter.format_name(definition))
for subkey in spack.package_base._subkeys(when_indexed_dictionary)
for _, definition in spack.package_base._definitions(when_indexed_dictionary, subkey)
)
indent = 4
for when, variants_by_name in pkg.variant_items():
padded_values = max_name_default_len + 4
for when, by_name in when_indexed_dictionary.items():
padded_values = max_name_len + 4
start_indent = indent
if when != spack.spec.Spec():
@ -373,27 +446,46 @@ def print_variants_grouped_by_when(pkg):
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)
for subkey, definition in sorted(by_name.items()):
_fmt_definition(
formatter.format_name(definition),
formatter.format_values(definition),
formatter.format_description(definition),
max_name_len,
start_indent,
when=None,
out=sys.stdout,
)
def print_variants_by_name(pkg):
max_name_default_len = _print_variants_header(pkg)
max_name_default_len += 4
def print_by_name(header: str, when_indexed_dictionary: Dict, formatter: Formatter):
print_header(header, when_indexed_dictionary, formatter)
if not when_indexed_dictionary:
return
max_name_len = max_name_length(when_indexed_dictionary, formatter)
max_name_len += 4
indent = 4
for name in pkg.variant_names():
for when, variant in pkg.variant_definitions(name):
_fmt_variant(variant, max_name_default_len, indent, when, out=sys.stdout)
for subkey in spack.package_base._subkeys(when_indexed_dictionary):
for when, definition in spack.package_base._definitions(when_indexed_dictionary, subkey):
_fmt_definition(
formatter.format_name(definition),
formatter.format_values(definition),
formatter.format_description(definition),
max_name_len,
indent,
when=when,
out=sys.stdout,
)
sys.stdout.write("\n")
def print_variants(pkg, args):
"""output variants"""
if args.variants_by_name:
print_variants_by_name(pkg)
else:
print_variants_grouped_by_when(pkg)
print_fn = print_by_name if args.variants_by_name else print_grouped_by_when
print_fn("Variants", pkg.variants, VariantFormatter())
def print_versions(pkg, args):
@ -413,7 +505,7 @@ def print_versions(pkg, args):
else:
pad = padder(pkg.versions, 4)
preferred = preferred_version(pkg)
preferred = spack.package_base.preferred_version(pkg)
def get_url(version):
try:

View File

@ -444,7 +444,7 @@ def _precedence(obj) -> int:
"""Get either a 'precedence' attribute or item from an object."""
precedence = getattr(obj, "precedence", None)
if precedence is None:
raise KeyError(f"Couldn't get precedence from {type(obj)}")
return 0 # raise KeyError(f"Couldn't get precedence from {type(obj)}")
return precedence