Compare commits

...

1 Commits

Author SHA1 Message Date
Todd Gamblin
6cbe4e1311
spec: add {install_status} format attribute
`{install_status}` is handled in a funny way in `Spec.tree()`, and it can't be used in
other useful places like `Spec.format()`.

- [x] Make `{install_status}` a format attribute like most other things we want to print
      about specs.

- [x] Refactor whitespace handling in `Spec.format()` to only strip whitespace that wasn't
      in the original format string (i.e. that was added by our own attributes)
2024-02-16 22:46:58 -08:00
6 changed files with 49 additions and 36 deletions

View File

@ -115,7 +115,7 @@ def emulate_env_utility(cmd_name, context: Context, args):
f"Not all dependencies of {spec.name} are installed. "
f"Cannot setup {context} environment:",
spec.tree(
status_fn=spack.spec.Spec.install_status,
install_status=True,
hashlen=7,
hashes=True,
# This shows more than necessary, but we cannot dynamically change deptypes

View File

@ -135,8 +135,6 @@ def _process_result(result, show, required_format, kwargs):
def solve(parser, args):
# these are the same options as `spack spec`
install_status_fn = spack.spec.Spec.install_status
fmt = spack.spec.DISPLAY_FORMAT
if args.namespaces:
fmt = "{namespace}." + fmt
@ -146,7 +144,7 @@ def solve(parser, args):
"format": fmt,
"hashlen": None if args.very_long else 7,
"show_types": args.types,
"status_fn": install_status_fn if args.install_status else None,
"install_status": args.install_status,
"hashes": args.long or args.very_long,
}

View File

@ -75,8 +75,6 @@ def setup_parser(subparser):
def spec(parser, args):
install_status_fn = spack.spec.Spec.install_status
fmt = spack.spec.DISPLAY_FORMAT
if args.namespaces:
fmt = "{namespace}." + fmt
@ -86,7 +84,7 @@ def spec(parser, args):
"format": fmt,
"hashlen": None if args.very_long else 7,
"show_types": args.types,
"status_fn": install_status_fn if args.install_status else None,
"install_status": args.install_status,
}
# use a read transaction if we are getting install status for every

View File

@ -2212,7 +2212,7 @@ def _tree_to_display(spec):
return spec.tree(
recurse_dependencies=True,
format=spack.spec.DISPLAY_FORMAT,
status_fn=spack.spec.Spec.install_status,
install_status=True,
hashlen=7,
hashes=True,
)

View File

@ -186,11 +186,11 @@ class InstallStatus(enum.Enum):
Options are artificially disjoint for display purposes
"""
installed = "@g{[+]} "
upstream = "@g{[^]} "
external = "@g{[e]} "
absent = "@K{ - } "
missing = "@r{[-]} "
INSTALLED = "@g{[+]}"
UPSTREAM = "@g{[^]}"
EXTERNAL = "@g{[e]}"
ABSENT = "@K{ - }"
MISSING = "@r{[-]}"
def colorize_spec(spec):
@ -1499,7 +1499,7 @@ def edge_attributes(self) -> str:
if not deptypes_str and not virtuals_str:
return ""
result = f"{deptypes_str} {virtuals_str}".strip()
return f"[{result}]"
return f"[{result}] "
def dependencies(
self, name=None, deptype: Union[dt.DepTypes, dt.DepFlag] = dt.ALL
@ -4319,8 +4319,7 @@ def colorized(self):
return colorize_spec(self)
def format(self, format_string=DEFAULT_FORMAT, **kwargs):
r"""Prints out particular pieces of a spec, depending on what is
in the format string.
r"""Prints out particular pieces of a spec, depending on what is in the format string.
Using the ``{attribute}`` syntax, any field of the spec can be
selected. Those attributes can be recursive. For example,
@ -4446,6 +4445,9 @@ def write_attribute(spec, attribute, color):
elif attribute == "spack_install":
write(morph(spec, spack.store.STORE.layout.root))
return
elif re.match(r"install_status", attribute):
write(self.install_status_symbol())
return
elif re.match(r"hash(:\d)?", attribute):
col = "#"
if ":" in attribute:
@ -4540,8 +4542,18 @@ def write_attribute(spec, attribute, color):
"Format string terminated while reading attribute." "Missing terminating }."
)
# remove leading whitespace from directives that add it for internal formatting.
# Arch, compiler flags, and variants add spaces for spec format correctness, but
# we don't really want them in formatted string output. We do want to preserve
# whitespace from the format string.
formatted_spec = out.getvalue()
return formatted_spec.strip()
whitespace_attrs = [r"{arch=[^}]*}", r"{architecture}", r"{compiler_flags}", r"{variants}"]
if any(re.match(rx, format_string) for rx in whitespace_attrs):
formatted_spec = formatted_spec.lstrip()
if any(re.search(f"{rx}$", format_string) for rx in whitespace_attrs):
formatted_spec = formatted_spec.rstrip()
return formatted_spec
def cformat(self, *args, **kwargs):
"""Same as format, but color defaults to auto instead of False."""
@ -4591,7 +4603,7 @@ def __str__(self):
self.traverse(root=False), key=lambda x: (x.name, x.abstract_hash)
)
sorted_dependencies = [
d.format("{edge_attributes} " + DEFAULT_FORMAT) for d in sorted_dependencies
d.format("{edge_attributes}" + DEFAULT_FORMAT) for d in sorted_dependencies
]
spec_str = " ^".join(root_str + sorted_dependencies)
return spec_str.strip()
@ -4611,20 +4623,25 @@ def colored_str(self):
def install_status(self):
"""Helper for tree to print DB install status."""
if not self.concrete:
return InstallStatus.absent
return InstallStatus.ABSENT
if self.external:
return InstallStatus.external
return InstallStatus.EXTERNAL
upstream, record = spack.store.STORE.db.query_by_spec_hash(self.dag_hash())
if not record:
return InstallStatus.absent
return InstallStatus.ABSENT
elif upstream and record.installed:
return InstallStatus.upstream
return InstallStatus.UPSTREAM
elif record.installed:
return InstallStatus.installed
return InstallStatus.INSTALLED
else:
return InstallStatus.missing
return InstallStatus.MISSING
def install_status_symbol(self):
"""Get an install status symbol."""
status = self.install_status()
return clr.colorize(status.value)
def _installed_explicitly(self):
"""Helper for tree to print DB install status."""
@ -4650,7 +4667,7 @@ def tree(
show_types: bool = False,
depth_first: bool = False,
recurse_dependencies: bool = True,
status_fn: Optional[Callable[["Spec"], InstallStatus]] = None,
install_status: bool = False,
prefix: Optional[Callable[["Spec"], str]] = None,
) -> str:
"""Prints out this spec and its dependencies, tree-formatted
@ -4671,8 +4688,7 @@ def tree(
show_types: if True, show the (merged) dependency type of a node
depth_first: if True, traverse the DAG depth first when representing it as a tree
recurse_dependencies: if True, recurse on dependencies
status_fn: optional callable that takes a node as an argument and return its
installation status
install_status: if True, show installation status next to each spec
prefix: optional callable that takes a node as an argument and return its
installation prefix
"""
@ -4686,6 +4702,9 @@ def tree(
):
node = dep_spec.spec
if install_status:
out += node.format("{install_status} ")
if prefix is not None:
out += prefix(node)
out += " " * indent
@ -4693,15 +4712,6 @@ def tree(
if depth:
out += "%-4d" % d
if status_fn:
status = status_fn(node)
if status in list(InstallStatus):
out += clr.colorize(status.value, color=color)
elif status:
out += clr.colorize("@g{[+]} ", color=color)
else:
out += clr.colorize("@r{[-]} ", color=color)
if hashes:
out += clr.colorize("@K{%s} ", color=color) % node.dag_hash(hashlen)

View File

@ -703,6 +703,13 @@ def check_prop(check_spec, fmt_str, prop, getter):
actual = spec.format(named_str)
assert expected == actual
def test_spec_format_instalL_status(self, database):
installed = database.query_one("mpileaks^zmpi")
assert installed.format("{install_status}") == "[+]"
not_installed = Spec("foo")
assert not_installed.format("{install_status}") == " - "
def test_spec_formatting_escapes(self, default_mock_concretization):
spec = default_mock_concretization("multivalue-variant cflags=-O2")