Repository: spack/spack

Issue:
"spack config add" fails for config:concretizer due to enum requirement
### Steps to reproduce the issue

```console
$ spack config --scope defaults add config:concretizer:clingo
```

This also requires that `concretizer: original` *not* be present in `etc/.../config.yaml` - this may be an unsupported configuration.

### Error Message

<!-- If Spack reported an error, provide the error message. If it did not report an error but the output appears incorrect, provide the incorrect output. If there was no error message and no output but the result is incorrect, describe how it does not match what you expect. -->
```console
$ spack config --scope defaults add config:concretizer:clingo
spack.config.ConfigError: Cannot determine valid type for path 'config:concretizer'.
```

### Information on your system

<!-- Please include the output of `spack debug report` -->
* **Spack:** 0.16.1
* **Python:** 3.7.2
* **Platform:** linux-rhel7-broadwell
* **Concretizer:** original

<!-- If you have any relevant configuration detail (custom `packages.yaml` or `modules.yaml`, etc.) you can add that here as well. -->

### Additional information

<!-- These boxes can be checked by replacing [ ] with [x] or by clicking them after submitting the issue. -->
- [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

<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question about the tool (how to use it, what it can currently do, etc.), try the `#general` channel on our Slack first. We have a welcoming community and chances are you'll get your reply faster and without opening an issue.

Other than that, thanks for taking the time to contribute to Spack! -->

The issue appears to be the logic in [get_valid_type](https://github.com/spack/spack/blob/develop/lib/spack/spack/config.py#L1036) - because it uses the "default-constructed" instance of a type for schema checking, it only works with JSONSchema's `type` attribute - it does not appear to consider the case where the `enum` attribute is also used.

Fixing this may require traversing the schema, and either:
1. Using the enum values to determine the time (when `enum` is used)
2. Grabbing the required type information directly (from the `type` attr)

config add: use the provided value to validate the type
Fixes #17543
Fixes #23259

When adding a config parameter via `spack config add 'foo:bar:baz'` an attempt is made to determine the appropriate type for the config entry by cycling through the default values for various types and attempting to validate against the schema.  This fails for enums since the default value is most likely not one of the valid enum values.  This change uses the user-provided value itself to construct the test value to validate with rather than the default value for a given type.

Before this change the following would fail and now succeeds:

```bash
$ spack config add 'config:concretizer:clingo'
==> Error: Cannot determine valid type for path 'config:concretizer'.
```


Discussion:
Tag: @becker33 
Duplicate of #17543
Making the style check happy for `config.py` seems to put the imports in a weird order:
```diff
diff --git a/lib/spack/spack/config.py b/lib/spack/spack/config.py
index 18d8ff59e7..fb72056e34 100644
--- a/lib/spack/spack/config.py
+++ b/lib/spack/spack/config.py
@@ -36,35 +36,32 @@
 import re
 import sys
 from contextlib import contextmanager
-from six import iteritems
-from ordereddict_backport import OrderedDict
 from typing import List  # novm
 
 import ruamel.yaml as yaml
+from ordereddict_backport import OrderedDict
 from ruamel.yaml.error import MarkedYAMLError
+from six import iteritems
 
 import llnl.util.lang
 import llnl.util.tty as tty
-from llnl.util.filesystem import mkdirp
-
-import spack.paths
 import spack.architecture
 import spack.compilers
+import spack.paths
 import spack.schema
 import spack.schema.compilers
+import spack.schema.config
+import spack.schema.env
 import spack.schema.mirrors
-import spack.schema.repos
-import spack.schema.packages
 import spack.schema.modules
-import spack.schema.config
+import spack.schema.packages
+import spack.schema.repos
 import spack.schema.upstreams
-import spack.schema.env
+import spack.util.spack_yaml as syaml
+from llnl.util.filesystem import mkdirp
 from spack.error import SpackError
 from spack.util.cpus import cpus_available
 
-# Hacked yaml for configuration files preserves line numbers.
-import spack.util.spack_yaml as syaml
-
 #: Dict from section names -> schema for that section
 section_schemas = {
     'compilers': spack.schema.compilers.schema,
```

Is there a better way to make that check happy?
@chuckatkins Can you please rebase this one? All imports in spack have been sorted now.

Resolving patch:
diff --git a/lib/spack/spack/config.py b/lib/spack/spack/config.py
--- a/lib/spack/spack/config.py
+++ b/lib/spack/spack/config.py
@@ -1067,22 +1067,36 @@ def get_valid_type(path):
     path given, the priority order is ``list``, ``dict``, ``str``, ``bool``,
     ``int``, ``float``.
     """
+    types = {
+        'array': list,
+        'object': syaml.syaml_dict,
+        'string': str,
+        'boolean': bool,
+        'integer': int,
+        'number': float
+    }
+
     components = process_config_path(path)
     section = components[0]
-    for type in (list, syaml.syaml_dict, str, bool, int, float):
-        try:
-            ret = type()
-            test_data = ret
-            for component in reversed(components):
-                test_data = {component: test_data}
-            validate(test_data, section_schemas[section])
-            return ret
-        except (ConfigFormatError, AttributeError):
-            # This type won't validate, try the next one
-            # Except AttributeError because undefined behavior of dict ordering
-            # in python 3.5 can cause the validator to raise an AttributeError
-            # instead of a ConfigFormatError.
-            pass
+
+    # Use None to construct the test data
+    test_data = None
+    for component in reversed(components):
+        test_data = {component: test_data}
+
+    try:
+        validate(test_data, section_schemas[section])
+    except (ConfigFormatError, AttributeError) as e:
+        jsonschema_error = e.validation_error
+        if jsonschema_error.validator == 'type':
+            return types[jsonschema_error.validator_value]()
+        elif jsonschema_error.validator == 'anyOf':
+            for subschema in jsonschema_error.validator_value:
+                anyof_type = subschema.get('type')
+                if anyof_type is not None:
+                    return types[anyof_type]()
+    else:
+        return type(None)
     raise ConfigError("Cannot determine valid type for path '%s'." % path)
 
 
@@ -1301,6 +1315,7 @@ class ConfigFormatError(ConfigError):
     def __init__(self, validation_error, data, filename=None, line=None):
         # spack yaml has its own file/line marks -- try to find them
         # we prioritize these over the inputs
+        self.validation_error = validation_error
         mark = self._get_mark(validation_error, data)
         if mark:
             filename = mark.name


Test patch:
diff --git a/lib/spack/spack/test/cmd/config.py b/lib/spack/spack/test/cmd/config.py
--- a/lib/spack/spack/test/cmd/config.py
+++ b/lib/spack/spack/test/cmd/config.py
@@ -225,7 +225,7 @@ def test_config_with_c_argument(mutable_empty_config):
     # Add the path to the config
     config("add", args.config_vars[0], scope='command_line')
     output = config("get", 'config')
-    assert "config:\n  install_root:\n  - root: /path/to/config.yaml" in output
+    assert "config:\n  install_root:\n    root: /path/to/config.yaml" in output
 
 
 def test_config_add_ordered_dict(mutable_empty_config):
diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py
--- a/lib/spack/spack/test/config.py
+++ b/lib/spack/spack/test/config.py
@@ -279,21 +279,32 @@ def test_write_to_same_priority_file(mock_low_high_config, compiler_specs):
 # Test setting config values via path in filename
 
 
-def test_add_config_path():
-
+def test_add_config_path(mutable_config):
     # Try setting a new install tree root
     path = "config:install_tree:root:/path/to/config.yaml"
-    spack.config.add(path, scope="command_line")
+    spack.config.add(path)
     set_value = spack.config.get('config')['install_tree']['root']
     assert set_value == '/path/to/config.yaml'
 
     # Now a package:all setting
     path = "packages:all:compiler:[gcc]"
-    spack.config.add(path, scope="command_line")
+    spack.config.add(path)
     compilers = spack.config.get('packages')['all']['compiler']
     assert "gcc" in compilers
 
 
+@pytest.mark.regression('17543,23259')
+def test_add_config_path_with_enumerated_type(mutable_config):
+    spack.config.add("config:concretizer:clingo")
+    assert spack.config.get('config')['concretizer'] == "clingo"
+
+    spack.config.add("config:concretizer:original")
+    assert spack.config.get('config')['concretizer'] == "original"
+
+    with pytest.raises(spack.config.ConfigError):
+        spack.config.add("config:concretizer:foo")
+
+
 def test_add_config_filename(mock_low_high_config, tmpdir):
 
     config_yaml = tmpdir.join('config-filename.yaml')