diff --git a/lib/spack/spack/cmd/common/env_utility.py b/lib/spack/spack/cmd/common/env_utility.py index 1d04e199d9c..9a658b39fe6 100644 --- a/lib/spack/spack/cmd/common/env_utility.py +++ b/lib/spack/spack/cmd/common/env_utility.py @@ -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 diff --git a/lib/spack/spack/cmd/solve.py b/lib/spack/spack/cmd/solve.py index 9b03e596ed1..6e6dc3f3ae2 100644 --- a/lib/spack/spack/cmd/solve.py +++ b/lib/spack/spack/cmd/solve.py @@ -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, } diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py index e2d5cb10557..9c85fb412b1 100644 --- a/lib/spack/spack/cmd/spec.py +++ b/lib/spack/spack/cmd/spec.py @@ -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 diff --git a/lib/spack/spack/environment/environment.py b/lib/spack/spack/environment/environment.py index 2433e59b217..0233a2ce323 100644 --- a/lib/spack/spack/environment/environment.py +++ b/lib/spack/spack/environment/environment.py @@ -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, ) diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index eb6c81a9ae3..80cc1e748b2 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -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) diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py index d159bf744ae..30d3fa421be 100644 --- a/lib/spack/spack/test/spec_semantics.py +++ b/lib/spack/spack/test/spec_semantics.py @@ -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")