Repository: spack/spack

Issue:
A corrupted packages.yaml causes 'spack config edit' to error out
Adding an invalid key to one of the yaml files causes it to be un-editable by spack.  For example:
```
>> spack config --scope defaults edit packages

packages:
  all:
    compiler: [gcc, intel, pgi, clang, xl, nag]
    providers:
      mpi: [openmpi, mpich]
      blas: [openblas]
      lapack: [openblas]
      pil: [py-pillow]

  openssl:
      paths:
          openssl@system: /usr
      version: [system]
      buildable: False
      **checksum: False**
```
Add the last, invalid key, save an exit.

Now try again
```
>> spack config --scope defaults edit packages
```
You will receive an error:
```
 Traceback (most recent call last):
  File "/home/tom/dev/spack/bin/spack", line 99, in <module>
    import spack
  File "/home/tom/dev/spack/lib/spack/spack/__init__.py", line 103, in <module>
    pkgsort = PreferredPackages()
  File "/home/tom/dev/spack/lib/spack/spack/preferred_packages.py", line 32, in __init__
    self.preferred = spack.config.get_config('packages')
  File "/home/tom/dev/spack/lib/spack/spack/config.py", line 398, in get_config
    data = scope.get_section(section)
  File "/home/tom/dev/spack/lib/spack/spack/config.py", line 177, in get_section
    data   = _read_config_file(path, schema)
  File "/home/tom/dev/spack/lib/spack/spack/config.py", line 272, in _read_config_file
    validate_section(data, schema)
  File "/home/tom/dev/spack/lib/spack/spack/config.py", line 149, in validate_section
    raise ConfigFormatError(e, data)
spack.config.ConfigFormatError: /home/tom/dev/spack/etc/spack/defaults/packages.yaml:25: Additional properties are not allowed ('checksum' was unexpected)
```
I think editing should be successful even if the config is invalid.


Discussion:
Just use your editor directly...
Yes, I understand.  But I think the interface is a little broken.
Confirmed. This happened to me recently.

Resolving patch:
diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py
--- a/lib/spack/spack/cmd/config.py
+++ b/lib/spack/spack/cmd/config.py
@@ -143,7 +143,10 @@ def config_get(args):
     """
     scope, section = _get_scope_and_section(args)
 
-    if scope and scope.startswith('env:'):
+    if section is not None:
+        spack.config.config.print_section(section)
+
+    elif scope and scope.startswith('env:'):
         config_file = spack.config.config.get_config_filename(scope, section)
         if os.path.exists(config_file):
             with open(config_file) as f:
@@ -151,9 +154,6 @@ def config_get(args):
         else:
             tty.die('environment has no %s file' % ev.manifest_name)
 
-    elif section is not None:
-        spack.config.config.print_section(section)
-
     else:
         tty.die('`spack config get` requires a section argument '
                 'or an active environment.')
@@ -170,12 +170,19 @@ def config_edit(args):
     With no arguments and an active environment, edit the spack.yaml for
     the active environment.
     """
-    scope, section = _get_scope_and_section(args)
-    if not scope and not section:
-        tty.die('`spack config edit` requires a section argument '
-                'or an active environment.')
+    spack_env = os.environ.get(ev.spack_env_var)
+    if spack_env and not args.scope:
+        # Don't use the scope object for envs, as `config edit` can be called
+        # for a malformed environment. Use SPACK_ENV to find spack.yaml.
+        config_file = ev.manifest_file(spack_env)
+    else:
+        # If we aren't editing a spack.yaml file, get config path from scope.
+        scope, section = _get_scope_and_section(args)
+        if not scope and not section:
+            tty.die('`spack config edit` requires a section argument '
+                    'or an active environment.')
+        config_file = spack.config.config.get_config_filename(scope, section)
 
-    config_file = spack.config.config.get_config_filename(scope, section)
     if args.print_file:
         print(config_file)
     else:
diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py
--- a/lib/spack/spack/main.py
+++ b/lib/spack/spack/main.py
@@ -725,13 +725,30 @@ def shell_set(var, value):
             shell_set('_sp_module_prefix', 'not_installed')
 
 
-def main(argv=None):
-    """This is the entry point for the Spack command.
+def _main(argv=None):
+    """Logic for the main entry point for the Spack command.
+
+    ``main()`` calls ``_main()`` and catches any errors that emerge.
+
+    ``_main()`` handles:
+
+    1. Parsing arguments;
+    2. Setting up configuration; and
+    3. Finding and executing a Spack command.
 
     Args:
         argv (list or None): command line arguments, NOT including
-            the executable name. If None, parses from sys.argv.
+            the executable name. If None, parses from ``sys.argv``.
+
     """
+    # ------------------------------------------------------------------------
+    # main() is tricky to get right, so be careful where you put things.
+    #
+    # Things in this first part of `main()` should *not* require any
+    # configuration. This doesn't include much -- setting up th parser,
+    # restoring some key environment variables, very simple CLI options, etc.
+    # ------------------------------------------------------------------------
+
     # Create a parser with a simple positional argument first.  We'll
     # lazily load the subcommand(s) we need later. This allows us to
     # avoid loading all the modules from spack.cmd when we don't need
@@ -754,20 +771,6 @@ def main(argv=None):
         if stored_var_name in os.environ:
             os.environ[var] = os.environ[stored_var_name]
 
-    # make spack.config aware of any command line configuration scopes
-    if args.config_scopes:
-        spack.config.command_line_scopes = args.config_scopes
-
-    # activate an environment if one was specified on the command line
-    if not args.no_env:
-        env = spack.cmd.find_environment(args)
-        if env:
-            ev.activate(env, args.use_env_repo)
-
-    if args.print_shell_vars:
-        print_setup_info(*args.print_shell_vars.split(','))
-        return 0
-
     # Just print help and exit if run with no arguments at all
     no_args = (len(sys.argv) == 1) if argv is None else (len(argv) == 0)
     if no_args:
@@ -782,36 +785,93 @@ def main(argv=None):
     elif args.help:
         sys.stdout.write(parser.format_help(level=args.help))
         return 0
-    elif not args.command:
+
+    # ------------------------------------------------------------------------
+    # This part of the `main()` sets up Spack's configuration.
+    #
+    # We set command line options (like --debug), then command line config
+    # scopes, then environment configuration here.
+    # ------------------------------------------------------------------------
+
+    # ensure options on spack command come before everything
+    setup_main_options(args)
+
+    # make spack.config aware of any command line configuration scopes
+    if args.config_scopes:
+        spack.config.command_line_scopes = args.config_scopes
+
+    # activate an environment if one was specified on the command line
+    env_format_error = None
+    if not args.no_env:
+        try:
+            env = spack.cmd.find_environment(args)
+            if env:
+                ev.activate(env, args.use_env_repo)
+        except spack.config.ConfigFormatError as e:
+            # print the context but delay this exception so that commands like
+            # `spack config edit` can still work with a bad environment.
+            e.print_context()
+            env_format_error = e
+
+    # ------------------------------------------------------------------------
+    # Things that require configuration should go below here
+    # ------------------------------------------------------------------------
+    if args.print_shell_vars:
+        print_setup_info(*args.print_shell_vars.split(','))
+        return 0
+
+    # At this point we've considered all the options to spack itself, so we
+    # need a command or we're done.
+    if not args.command:
         parser.print_help()
         return 1
 
-    try:
-        # ensure options on spack command come before everything
-        setup_main_options(args)
+    # Try to load the particular command the caller asked for.
+    cmd_name = args.command[0]
+    cmd_name = aliases.get(cmd_name, cmd_name)
 
-        # Try to load the particular command the caller asked for.
-        cmd_name = args.command[0]
-        cmd_name = aliases.get(cmd_name, cmd_name)
+    command = parser.add_command(cmd_name)
 
-        command = parser.add_command(cmd_name)
+    # Re-parse with the proper sub-parser added.
+    args, unknown = parser.parse_known_args()
 
-        # Re-parse with the proper sub-parser added.
-        args, unknown = parser.parse_known_args()
+    # Now that we know what command this is and what its args are, determine
+    # whether we can continue with a bad environment and raise if not.
+    if env_format_error:
+        subcommand = getattr(args, "config_command", None)
+        if (cmd_name, subcommand) != ("config", "edit"):
+            raise env_format_error
 
-        # many operations will fail without a working directory.
-        set_working_dir()
+    # many operations will fail without a working directory.
+    set_working_dir()
 
-        # now we can actually execute the command.
-        if args.spack_profile or args.sorted_profile:
-            _profile_wrapper(command, parser, args, unknown)
-        elif args.pdb:
-            import pdb
-            pdb.runctx('_invoke_command(command, parser, args, unknown)',
-                       globals(), locals())
-            return 0
-        else:
-            return _invoke_command(command, parser, args, unknown)
+    # now we can actually execute the command.
+    if args.spack_profile or args.sorted_profile:
+        _profile_wrapper(command, parser, args, unknown)
+    elif args.pdb:
+        import pdb
+        pdb.runctx('_invoke_command(command, parser, args, unknown)',
+                   globals(), locals())
+        return 0
+    else:
+        return _invoke_command(command, parser, args, unknown)
+
+
+def main(argv=None):
+    """This is the entry point for the Spack command.
+
+    ``main()`` itself is just an error handler -- it handles errors for
+    everything in Spack that makes it to the top level.
+
+    The logic is all in ``_main()``.
+
+    Args:
+        argv (list or None): command line arguments, NOT including
+            the executable name. If None, parses from sys.argv.
+
+    """
+    try:
+        _main(argv)
 
     except SpackError as e:
         tty.debug(e)


Test patch: