From 2536029ea9e09707bb2a4c4a2b60a6a726ac8b9e Mon Sep 17 00:00:00 2001
From: Todd Gamblin <tgamblin@llnl.gov>
Date: Fri, 4 Nov 2016 11:47:57 -0700
Subject: [PATCH] Better spack spec (#2238)

* Add options for hashes, tree depth, and YAML to `spack spec`.

- Can now display hashes with `spack spec`, like `spack find`.
  - Removed the old "ids" argument to `spack spec` (which
    printed numerical values)b

- Can output YAML spec from `spack spec` with `-y`

- Can control depth of DAG traversal with --cover=[nodes|edges|paths]

- Can print install status (installed, missing, not installed) with -I

* Don't use YAML aliases in specs.

- Force Spack's YAML dumper to ignore aliases.
- aliases cause non-canonical YAML to be used in DAG hash, and result in
  redundant hashes.
- add a test to ensure this behavior stays
---
 lib/spack/spack/cmd/common/arguments.py |  8 +++++
 lib/spack/spack/cmd/find.py             | 10 ++-----
 lib/spack/spack/cmd/spec.py             | 39 +++++++++++++++++++------
 lib/spack/spack/spec.py                 | 32 +++++++++++++++-----
 lib/spack/spack/test/spack_yaml.py      | 16 ++++++++++
 lib/spack/spack/util/spack_yaml.py      |  5 ++++
 6 files changed, 85 insertions(+), 25 deletions(-)

diff --git a/lib/spack/spack/cmd/common/arguments.py b/lib/spack/spack/cmd/common/arguments.py
index d5bd4bb711..1470ea035d 100644
--- a/lib/spack/spack/cmd/common/arguments.py
+++ b/lib/spack/spack/cmd/common/arguments.py
@@ -89,3 +89,11 @@ def _specs(self, **kwargs):
 _arguments['dirty'] = Args(
     '--dirty', action='store_true', dest='dirty',
     help='Do NOT clean environment before installing.')
+
+_arguments['long'] = Args(
+    '-l', '--long', action='store_true',
+    help='Show dependency hashes as well as versions.')
+
+_arguments['very_long'] = Args(
+    '-L', '--very-long', action='store_true',
+    help='Show full dependency hashes as well as versions.')
diff --git a/lib/spack/spack/cmd/find.py b/lib/spack/spack/cmd/find.py
index 29bf263f51..27949ef5db 100644
--- a/lib/spack/spack/cmd/find.py
+++ b/lib/spack/spack/cmd/find.py
@@ -52,14 +52,8 @@ def setup_parser(subparser):
         const='deps',
         help='Show full dependency DAG of installed packages')
 
-    subparser.add_argument('-l', '--long',
-                           action='store_true',
-                           dest='long',
-                           help='Show dependency hashes as well as versions.')
-    subparser.add_argument('-L', '--very-long',
-                           action='store_true',
-                           dest='very_long',
-                           help='Show dependency hashes as well as versions.')
+    arguments.add_common_arguments(subparser, ['long', 'very_long'])
+
     subparser.add_argument('-f', '--show-flags',
                            action='store_true',
                            dest='show_flags',
diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py
index 6e6d1c1277..0a6fb330ac 100644
--- a/lib/spack/spack/cmd/spec.py
+++ b/lib/spack/spack/cmd/spec.py
@@ -23,36 +23,57 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
 import argparse
-import spack.cmd
 
 import spack
+import spack.cmd
+import spack.cmd.common.arguments as arguments
 
 description = "print out abstract and concrete versions of a spec."
 
 
 def setup_parser(subparser):
-    subparser.add_argument('-i', '--ids', action='store_true',
-                           help="show numerical ids for dependencies.")
+    arguments.add_common_arguments(subparser, ['long', 'very_long'])
+    subparser.add_argument(
+        '-y', '--yaml', action='store_true', default=False,
+        help='Print concrete spec as YAML.')
+    subparser.add_argument(
+        '-c', '--cover', action='store',
+        default='nodes', choices=['nodes', 'edges', 'paths'],
+        help='How extensively to traverse the DAG. (default: nodes).')
+    subparser.add_argument(
+        '-I', '--install-status', action='store_true', default=False,
+        help='Show install status of packages.  Packages can be: '
+             'installed [+], missing and needed by an installed package [-], '
+             'or not installed (no annotation).')
     subparser.add_argument(
         'specs', nargs=argparse.REMAINDER, help="specs of packages")
 
 
 def spec(parser, args):
-    kwargs = {'ids': args.ids,
-              'indent': 2,
-              'color': True}
+    kwargs = {'color': True,
+              'cover': args.cover,
+              'install_status': args.install_status,
+              'hashes': args.long or args.very_long,
+              'hashlen': None if args.very_long else 7}
 
     for spec in spack.cmd.parse_specs(args.specs):
+        # With -y, just print YAML to output.
+        if args.yaml:
+            spec.concretize()
+            print spec.to_yaml()
+            continue
+
+        # Print some diagnostic info by default.
         print "Input spec"
-        print "------------------------------"
+        print "--------------------------------"
         print spec.tree(**kwargs)
 
         print "Normalized"
-        print "------------------------------"
+        print "--------------------------------"
         spec.normalize()
         print spec.tree(**kwargs)
 
         print "Concretized"
-        print "------------------------------"
+        print "--------------------------------"
         spec.concretize()
         print spec.tree(**kwargs)
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index cbcb2199f6..f830b73fa0 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -2396,12 +2396,24 @@ def __cmp__(self, other):
     def __str__(self):
         return self.format() + self.dep_string()
 
+    def _install_status(self):
+        """Helper for tree to print DB install status."""
+        if not self.concrete:
+            return None
+        try:
+            record = spack.store.db.get_record(self)
+            return record.installed
+        except KeyError:
+            return None
+
     def tree(self, **kwargs):
         """Prints out this spec and its dependencies, tree-formatted
            with indentation."""
         color = kwargs.pop('color', False)
         depth = kwargs.pop('depth', False)
-        showid = kwargs.pop('ids',   False)
+        hashes = kwargs.pop('hashes', True)
+        hlen = kwargs.pop('hashlen', None)
+        install_status = kwargs.pop('install_status', True)
         cover = kwargs.pop('cover', 'nodes')
         indent = kwargs.pop('indent', 0)
         fmt = kwargs.pop('format', '$_$@$%@+$+$=')
@@ -2410,8 +2422,6 @@ def tree(self, **kwargs):
         check_kwargs(kwargs, self.tree)
 
         out = ""
-        cur_id = 0
-        ids = {}
         for d, node in self.traverse(
                 order='pre', cover=cover, depth=True, deptypes=deptypes):
             if prefix is not None:
@@ -2419,11 +2429,17 @@ def tree(self, **kwargs):
             out += " " * indent
             if depth:
                 out += "%-4d" % d
-            if not id(node) in ids:
-                cur_id += 1
-                ids[id(node)] = cur_id
-            if showid:
-                out += "%-4d" % ids[id(node)]
+            if install_status:
+                status = node._install_status()
+                if status is None:
+                    out += "     "  # Package isn't installed
+                elif status:
+                    out += colorize("@g{[+]}  ", color=color)  # installed
+                else:
+                    out += colorize("@r{[-]}  ", color=color)  # missing
+
+            if hashes:
+                out += colorize('@K{%s}  ', color=color) % node.dag_hash(hlen)
             out += ("    " * d)
             if d > 0:
                 out += "^"
diff --git a/lib/spack/spack/test/spack_yaml.py b/lib/spack/spack/test/spack_yaml.py
index 30ed1672e2..fbbb7b8e60 100644
--- a/lib/spack/spack/test/spack_yaml.py
+++ b/lib/spack/spack/test/spack_yaml.py
@@ -90,3 +90,19 @@ def check(obj, start_line, end_line):
         check(self.data['config_file']['some_list'][2],   8,  8)
         check(self.data['config_file']['another_list'],  10, 10)
         check(self.data['config_file']['some_key'],      11, 11)
+
+    def test_yaml_aliases(self):
+        aliased_list_1 = ['foo']
+        aliased_list_2 = []
+        dict_with_aliases = {
+            'a': aliased_list_1,
+            'b': aliased_list_1,
+            'c': aliased_list_1,
+            'd': aliased_list_2,
+            'e': aliased_list_2,
+            'f': aliased_list_2,
+        }
+        string = syaml.dump(dict_with_aliases)
+
+        # ensure no YAML aliases appear in syaml dumps.
+        self.assertFalse('*id' in string)
diff --git a/lib/spack/spack/util/spack_yaml.py b/lib/spack/spack/util/spack_yaml.py
index 674c79bca1..c27db52066 100644
--- a/lib/spack/spack/util/spack_yaml.py
+++ b/lib/spack/spack/util/spack_yaml.py
@@ -202,6 +202,11 @@ def represent_mapping(self, tag, mapping, flow_style=None):
                 node.flow_style = best_style
         return node
 
+    def ignore_aliases(self, _data):
+        """Make the dumper NEVER print YAML aliases."""
+        return True
+
+
 # Make our special objects look like normal YAML ones.
 OrderedLineDumper.add_representer(syaml_dict, OrderedLineDumper.represent_dict)
 OrderedLineDumper.add_representer(syaml_list, OrderedLineDumper.represent_list)
-- 
GitLab