Repository: spack/spack

Issue:
--format of dependency's hash is wrong
### Steps to reproduce

I wanted to get the hash from a dependency package and there seems to be a bug. The problem should arise with any spec/dependency combination. Here my example:

```console
$ spack find -ld perl
-- linux-rocky8-zen / gcc@8.5.0 ---------------------------------
mwalrc6 perl@5.36.0
lzfh6ek     berkeley-db@18.1.40
t64oo25     bzip2@1.0.8
lfzvmkn         diffutils@3.9
cv75ifv             libiconv@1.17
u6bdgsp     gdbm@1.23
jrjkg7s         readline@8.2
idtlmtd             ncurses@6.4
m7qknuv                 pkgconf@1.8.0
rw3jnfu     zlib@1.2.13

==> 1 installed package
$ spack find --format="{^gdbm.name}" perl
gdbm
$ spack find --format="{^gdbm.hash}" perl
mwalrc6y3ii2ogb37vk664pqyzrajndq
```

Here in debug mode:
```console
$ spack --debug find --format="{^gdbm.hash}" perl
==> [2023-04-19-08:29:35.866029] Imported find from built-in commands
==> [2023-04-19-08:29:35.866721] Imported find from built-in commands
==> [2023-04-19-08:29:35.867928] Reading config from file /albedo/home/shinck/tmp/spack/etc/spack/defaults/config.yaml
==> [2023-04-19-08:29:35.888015] DATABASE LOCK TIMEOUT: 60s
==> [2023-04-19-08:29:35.888054] PACKAGE LOCK TIMEOUT: No timeout
==> [2023-04-19-08:29:35.899897] Reading config from file /albedo/home/shinck/tmp/spack/etc/spack/defaults/repos.yaml
mwalrc6y3ii2ogb37vk664pqyzrajndq
```


### Error message

There is no error message.

However, if you look at the above output you see that the hash of the dependency package is wrong. It shows the hash of the "original" package instead. Whereas the name of the dependency package is shown correctly!

### Information on your system

```console
$ spack debug report 
* **Spack:** 0.20.0.dev0 (11e5b0cd917cca4665f9e19e078fdd94df418485)
* **Python:** 3.6.8
* **Platform:** linux-rocky8-zen2
* **Concretizer:** clingo
```

For this test I did not change spack's default configuration.

But it also shows the same problem with Spack 0.18.1

### General information

- [X] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [X] I have searched the issues of this repo and believe this is not a duplicate
- [X] I have run the failing commands in debug mode and reported the output


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -4115,18 +4115,10 @@ def write(s, c=None):
             clr.cwrite(f, stream=out, color=color)
 
         def write_attribute(spec, attribute, color):
-            current = spec
-            if attribute.startswith("^"):
-                attribute = attribute[1:]
-                dep, attribute = attribute.split(".", 1)
-                current = self[dep]
-
-            if attribute == "":
-                raise SpecFormatStringError("Format string attributes must be non-empty")
             attribute = attribute.lower()
 
             sig = ""
-            if attribute[0] in "@%/":
+            if attribute.startswith(("@", "%", "/")):
                 # color sigils that are inside braces
                 sig = attribute[0]
                 attribute = attribute[1:]
@@ -4134,6 +4126,15 @@ def write_attribute(spec, attribute, color):
                 sig = " arch="  # include space as separator
                 attribute = attribute[5:]
 
+            current = spec
+            if attribute.startswith("^"):
+                attribute = attribute[1:]
+                dep, attribute = attribute.split(".", 1)
+                current = self[dep]
+
+            if attribute == "":
+                raise SpecFormatStringError("Format string attributes must be non-empty")
+
             parts = attribute.split(".")
             assert parts
 
@@ -4162,9 +4163,9 @@ def write_attribute(spec, attribute, color):
                 col = "#"
                 if ":" in attribute:
                     _, length = attribute.split(":")
-                    write(sig + morph(spec, spec.dag_hash(int(length))), col)
+                    write(sig + morph(spec, current.dag_hash(int(length))), col)
                 else:
-                    write(sig + morph(spec, spec.dag_hash()), col)
+                    write(sig + morph(spec, current.dag_hash()), col)
                 return
 
             # Iterate over components using getattr to get next element


Test patch:
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -621,65 +621,62 @@ def test_spec_formatting(self, default_mock_concretization):
         # Testing named strings ie {string} and whether we get
         # the correct component
         # Mixed case intentional to test both
+        # Fields are as follow
+        # fmt_str: the format string to test
+        # sigil: the portion that is a sigil (may be empty string)
+        # prop: the property to get
+        # component: subcomponent of spec from which to get property
         package_segments = [
-            ("{NAME}", "name"),
-            ("{VERSION}", "versions"),
-            ("{compiler}", "compiler"),
-            ("{compiler_flags}", "compiler_flags"),
-            ("{variants}", "variants"),
-            ("{architecture}", "architecture"),
+            ("{NAME}", "", "name", lambda spec: spec),
+            ("{VERSION}", "", "versions", lambda spec: spec),
+            ("{compiler}", "", "compiler", lambda spec: spec),
+            ("{compiler_flags}", "", "compiler_flags", lambda spec: spec),
+            ("{variants}", "", "variants", lambda spec: spec),
+            ("{architecture}", "", "architecture", lambda spec: spec),
+            ("{@VERSIONS}", "@", "version", lambda spec: spec),
+            ("{%compiler}", "%", "compiler", lambda spec: spec),
+            ("{arch=architecture}", "arch=", "architecture", lambda spec: spec),
+            ("{compiler.name}", "", "name", lambda spec: spec.compiler),
+            ("{compiler.version}", "", "versions", lambda spec: spec.compiler),
+            ("{%compiler.name}", "%", "name", lambda spec: spec.compiler),
+            ("{@compiler.version}", "@", "version", lambda spec: spec.compiler),
+            ("{architecture.platform}", "", "platform", lambda spec: spec.architecture),
+            ("{architecture.os}", "", "os", lambda spec: spec.architecture),
+            ("{architecture.target}", "", "target", lambda spec: spec.architecture),
+            ("{prefix}", "", "prefix", lambda spec: spec),
         ]
 
-        sigil_package_segments = [
-            ("{@VERSIONS}", "@" + str(spec.version)),
-            ("{%compiler}", "%" + str(spec.compiler)),
-            ("{arch=architecture}", "arch=" + str(spec.architecture)),
-        ]
-
-        compiler_segments = [("{compiler.name}", "name"), ("{compiler.version}", "versions")]
-
-        sigil_compiler_segments = [
-            ("{%compiler.name}", "%" + spec.compiler.name),
-            ("{@compiler.version}", "@" + str(spec.compiler.version)),
-        ]
-
-        architecture_segments = [
-            ("{architecture.platform}", "platform"),
-            ("{architecture.os}", "os"),
-            ("{architecture.target}", "target"),
+        hash_segments = [
+            ("{hash:7}", "", lambda s: s.dag_hash(7)),
+            ("{/hash}", "/", lambda s: "/" + s.dag_hash()),
         ]
 
         other_segments = [
             ("{spack_root}", spack.paths.spack_root),
             ("{spack_install}", spack.store.layout.root),
-            ("{hash:7}", spec.dag_hash(7)),
-            ("{/hash}", "/" + spec.dag_hash()),
         ]
 
-        for named_str, prop in package_segments:
-            expected = getattr(spec, prop, "")
-            actual = spec.format(named_str)
-            assert str(expected).strip() == actual
-
-        for named_str, expected in sigil_package_segments:
-            actual = spec.format(named_str)
-            assert expected == actual
-
-        compiler = spec.compiler
-        for named_str, prop in compiler_segments:
-            expected = getattr(compiler, prop, "")
-            actual = spec.format(named_str)
-            assert str(expected) == actual
-
-        for named_str, expected in sigil_compiler_segments:
-            actual = spec.format(named_str)
-            assert expected == actual
-
-        arch = spec.architecture
-        for named_str, prop in architecture_segments:
-            expected = getattr(arch, prop, "")
-            actual = spec.format(named_str)
-            assert str(expected) == actual
+        def depify(depname, fmt_str, sigil):
+            sig = len(sigil)
+            opening = fmt_str[: 1 + sig]
+            closing = fmt_str[1 + sig :]
+            return spec[depname], opening + f"^{depname}." + closing
+
+        def check_prop(check_spec, fmt_str, prop, getter):
+            actual = spec.format(fmt_str)
+            expected = getter(check_spec)
+            assert actual == str(expected).strip()
+
+        for named_str, sigil, prop, get_component in package_segments:
+            getter = lambda s: sigil + str(getattr(get_component(s), prop, ""))
+            check_prop(spec, named_str, prop, getter)
+            mpi, fmt_str = depify("mpi", named_str, sigil)
+            check_prop(mpi, fmt_str, prop, getter)
+
+        for named_str, sigil, getter in hash_segments:
+            assert spec.format(named_str) == getter(spec)
+            callpath, fmt_str = depify("callpath", named_str, sigil)
+            assert spec.format(fmt_str) == getter(callpath)
 
         for named_str, expected in other_segments:
             actual = spec.format(named_str)