adding spack -c to set one off config arguments (#22251)
This pull request will add the ability for a user to add a configuration argument on the fly, on the command line, e.g.,: ```bash $ spack -c config:install_tree:root:/path/to/config.yaml -c packages:all:compiler:[gcc] list --help ``` The above command doesn't do anything (I'm just getting help for list) but you can imagine having another root of packages, and updating it on the fly for a command (something I'd like to do in the near future!) I've moved the logic for config_add that used to be in spack/cmd/config.py into spack/config.py proper, and now both the main.py (where spack commands live) and spack/cmd/config.py use these functions. I only needed spack config add, so I didn't move the others. We can move the others if there are also needed in multiple places.
This commit is contained in:
		@@ -200,71 +200,11 @@ def config_add(args):
 | 
			
		||||
 | 
			
		||||
    scope, section = _get_scope_and_section(args)
 | 
			
		||||
 | 
			
		||||
    # Updates from file
 | 
			
		||||
    if args.file:
 | 
			
		||||
        # Get file as config dict
 | 
			
		||||
        data = spack.config.read_config_file(args.file)
 | 
			
		||||
        if any(k in data for k in spack.schema.env.keys):
 | 
			
		||||
            data = ev.config_dict(data)
 | 
			
		||||
 | 
			
		||||
        # update all sections from config dict
 | 
			
		||||
        # We have to iterate on keys to keep overrides from the file
 | 
			
		||||
        for section in data.keys():
 | 
			
		||||
            if section in spack.config.section_schemas.keys():
 | 
			
		||||
                # Special handling for compiler scope difference
 | 
			
		||||
                # Has to be handled after we choose a section
 | 
			
		||||
                if scope is None:
 | 
			
		||||
                    scope = spack.config.default_modify_scope(section)
 | 
			
		||||
 | 
			
		||||
                value = data[section]
 | 
			
		||||
                existing = spack.config.get(section, scope=scope)
 | 
			
		||||
                new = spack.config.merge_yaml(existing, value)
 | 
			
		||||
 | 
			
		||||
                spack.config.set(section, new, scope)
 | 
			
		||||
        spack.config.add_from_file(args.file, scope=scope)
 | 
			
		||||
 | 
			
		||||
    if args.path:
 | 
			
		||||
        components = spack.config.process_config_path(args.path)
 | 
			
		||||
 | 
			
		||||
        has_existing_value = True
 | 
			
		||||
        path = ''
 | 
			
		||||
        override = False
 | 
			
		||||
        for idx, name in enumerate(components[:-1]):
 | 
			
		||||
            # First handle double colons in constructing path
 | 
			
		||||
            colon = '::' if override else ':' if path else ''
 | 
			
		||||
            path += colon + name
 | 
			
		||||
            if getattr(name, 'override', False):
 | 
			
		||||
                override = True
 | 
			
		||||
            else:
 | 
			
		||||
                override = False
 | 
			
		||||
 | 
			
		||||
            # Test whether there is an existing value at this level
 | 
			
		||||
            existing = spack.config.get(path, scope=scope)
 | 
			
		||||
 | 
			
		||||
            if existing is None:
 | 
			
		||||
                has_existing_value = False
 | 
			
		||||
                # We've nested further than existing config, so we need the
 | 
			
		||||
                # type information for validation to know how to handle bare
 | 
			
		||||
                # values appended to lists.
 | 
			
		||||
                existing = spack.config.get_valid_type(path)
 | 
			
		||||
 | 
			
		||||
                # construct value from this point down
 | 
			
		||||
                value = syaml.load_config(components[-1])
 | 
			
		||||
                for component in reversed(components[idx + 1:-1]):
 | 
			
		||||
                    value = {component: value}
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if has_existing_value:
 | 
			
		||||
            path, _, value = args.path.rpartition(':')
 | 
			
		||||
            value = syaml.load_config(value)
 | 
			
		||||
            existing = spack.config.get(path, scope=scope)
 | 
			
		||||
 | 
			
		||||
        # append values to lists
 | 
			
		||||
        if isinstance(existing, list) and not isinstance(value, list):
 | 
			
		||||
            value = [value]
 | 
			
		||||
 | 
			
		||||
        # merge value into existing
 | 
			
		||||
        new = spack.config.merge_yaml(existing, value)
 | 
			
		||||
        spack.config.set(path, new, scope)
 | 
			
		||||
        spack.config.add(args.path, scope=scope)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def config_remove(args):
 | 
			
		||||
 
 | 
			
		||||
@@ -806,6 +806,81 @@ def _config():
 | 
			
		||||
config = llnl.util.lang.Singleton(_config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_from_file(filename, scope=None):
 | 
			
		||||
    """Add updates to a config from a filename
 | 
			
		||||
    """
 | 
			
		||||
    import spack.environment as ev
 | 
			
		||||
 | 
			
		||||
    # Get file as config dict
 | 
			
		||||
    data = read_config_file(filename)
 | 
			
		||||
    if any(k in data for k in spack.schema.env.keys):
 | 
			
		||||
        data = ev.config_dict(data)
 | 
			
		||||
 | 
			
		||||
    # update all sections from config dict
 | 
			
		||||
    # We have to iterate on keys to keep overrides from the file
 | 
			
		||||
    for section in data.keys():
 | 
			
		||||
        if section in section_schemas.keys():
 | 
			
		||||
            # Special handling for compiler scope difference
 | 
			
		||||
            # Has to be handled after we choose a section
 | 
			
		||||
            if scope is None:
 | 
			
		||||
                scope = default_modify_scope(section)
 | 
			
		||||
 | 
			
		||||
            value = data[section]
 | 
			
		||||
            existing = get(section, scope=scope)
 | 
			
		||||
            new = merge_yaml(existing, value)
 | 
			
		||||
 | 
			
		||||
            # We cannot call config.set directly (set is a type)
 | 
			
		||||
            config.set(section, new, scope)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add(fullpath, scope=None):
 | 
			
		||||
    """Add the given configuration to the specified config scope.
 | 
			
		||||
    Add accepts a path. If you want to add from a filename, use add_from_file"""
 | 
			
		||||
 | 
			
		||||
    components = process_config_path(fullpath)
 | 
			
		||||
 | 
			
		||||
    has_existing_value = True
 | 
			
		||||
    path = ''
 | 
			
		||||
    override = False
 | 
			
		||||
    for idx, name in enumerate(components[:-1]):
 | 
			
		||||
        # First handle double colons in constructing path
 | 
			
		||||
        colon = '::' if override else ':' if path else ''
 | 
			
		||||
        path += colon + name
 | 
			
		||||
        if getattr(name, 'override', False):
 | 
			
		||||
            override = True
 | 
			
		||||
        else:
 | 
			
		||||
            override = False
 | 
			
		||||
 | 
			
		||||
        # Test whether there is an existing value at this level
 | 
			
		||||
        existing = get(path, scope=scope)
 | 
			
		||||
 | 
			
		||||
        if existing is None:
 | 
			
		||||
            has_existing_value = False
 | 
			
		||||
            # We've nested further than existing config, so we need the
 | 
			
		||||
            # type information for validation to know how to handle bare
 | 
			
		||||
            # values appended to lists.
 | 
			
		||||
            existing = get_valid_type(path)
 | 
			
		||||
 | 
			
		||||
            # construct value from this point down
 | 
			
		||||
            value = syaml.load_config(components[-1])
 | 
			
		||||
            for component in reversed(components[idx + 1:-1]):
 | 
			
		||||
                value = {component: value}
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    if has_existing_value:
 | 
			
		||||
        path, _, value = fullpath.rpartition(':')
 | 
			
		||||
        value = syaml.load_config(value)
 | 
			
		||||
        existing = get(path, scope=scope)
 | 
			
		||||
 | 
			
		||||
    # append values to lists
 | 
			
		||||
    if isinstance(existing, list) and not isinstance(value, list):
 | 
			
		||||
        value = [value]
 | 
			
		||||
 | 
			
		||||
    # merge value into existing
 | 
			
		||||
    new = merge_yaml(existing, value)
 | 
			
		||||
    config.set(path, new, scope)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get(path, default=None, scope=None):
 | 
			
		||||
    """Module-level wrapper for ``Configuration.get()``."""
 | 
			
		||||
    return config.get(path, default, scope)
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,6 @@
 | 
			
		||||
import spack.util.executable as exe
 | 
			
		||||
from spack.error import SpackError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#: names of profile statistics
 | 
			
		||||
stat_names = pstats.Stats.sort_arg_dict_default
 | 
			
		||||
 | 
			
		||||
@@ -358,6 +357,9 @@ def make_argument_parser(**kwargs):
 | 
			
		||||
        '--color', action='store', default='auto',
 | 
			
		||||
        choices=('always', 'never', 'auto'),
 | 
			
		||||
        help="when to colorize output (default: auto)")
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '-c', '--config', default=None, action="append", dest="config_vars",
 | 
			
		||||
        help="add one or more custom, one off config settings.")
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '-C', '--config-scope', dest='config_scopes', action='append',
 | 
			
		||||
        metavar='DIR', help="add a custom configuration scope")
 | 
			
		||||
@@ -463,6 +465,10 @@ def setup_main_options(args):
 | 
			
		||||
        tty.warn("You asked for --insecure. Will NOT check SSL certificates.")
 | 
			
		||||
        spack.config.set('config:verify_ssl', False, scope='command_line')
 | 
			
		||||
 | 
			
		||||
    # Use the spack config command to handle parsing the config strings
 | 
			
		||||
    for config_var in (args.config_vars or []):
 | 
			
		||||
        spack.config.add(path=config_var, scope="command_line")
 | 
			
		||||
 | 
			
		||||
    # when to use color (takes always, auto, or never)
 | 
			
		||||
    color.set_color_when(args.color)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,7 @@ def test_get_config_scope_merged(mock_low_high_config):
 | 
			
		||||
 | 
			
		||||
def test_config_edit():
 | 
			
		||||
    """Ensure `spack config edit` edits the right paths."""
 | 
			
		||||
 | 
			
		||||
    dms = spack.config.default_modify_scope('compilers')
 | 
			
		||||
    dms_path = spack.config.config.scopes[dms].path
 | 
			
		||||
    user_path = spack.config.config.scopes['user'].path
 | 
			
		||||
@@ -204,20 +205,27 @@ def test_config_add_override_leaf(mutable_empty_config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_add_update_dict(mutable_empty_config):
 | 
			
		||||
    config('add', 'packages:all:compiler:[gcc]')
 | 
			
		||||
    config('add', 'packages:all:version:1.0.0')
 | 
			
		||||
    config('add', 'packages:all:version:[1.0.0]')
 | 
			
		||||
    output = config('get', 'packages')
 | 
			
		||||
 | 
			
		||||
    expected = """packages:
 | 
			
		||||
  all:
 | 
			
		||||
    compiler: [gcc]
 | 
			
		||||
    version:
 | 
			
		||||
    - 1.0.0
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
    expected = 'packages:\n  all:\n    version: [1.0.0]\n'
 | 
			
		||||
    assert output == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_with_c_argument(mutable_empty_config):
 | 
			
		||||
 | 
			
		||||
    # I don't know how to add a spack argument to a Spack Command, so we test this way
 | 
			
		||||
    config_file = 'config:install_root:root:/path/to/config.yaml'
 | 
			
		||||
    parser = spack.main.make_argument_parser()
 | 
			
		||||
    args = parser.parse_args(['-c', config_file])
 | 
			
		||||
    assert config_file in args.config_vars
 | 
			
		||||
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_config_add_ordered_dict(mutable_empty_config):
 | 
			
		||||
    config('add', 'mirrors:first:/path/to/first')
 | 
			
		||||
    config('add', 'mirrors:second:/path/to/second')
 | 
			
		||||
 
 | 
			
		||||
@@ -258,6 +258,37 @@ def test_write_to_same_priority_file(mock_low_high_config, compiler_specs):
 | 
			
		||||
repos_low = {'repos': ["/some/path"]}
 | 
			
		||||
repos_high = {'repos': ["/some/other/path"]}
 | 
			
		||||
 | 
			
		||||
# Test setting config values via path in filename
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_add_config_path():
 | 
			
		||||
 | 
			
		||||
    # Try setting a new install tree root
 | 
			
		||||
    path = "config:install_tree:root:/path/to/config.yaml"
 | 
			
		||||
    spack.config.add(path, scope="command_line")
 | 
			
		||||
    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")
 | 
			
		||||
    compilers = spack.config.get('packages')['all']['compiler']
 | 
			
		||||
    assert "gcc" in compilers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_add_config_filename(mock_low_high_config, tmpdir):
 | 
			
		||||
 | 
			
		||||
    config_yaml = tmpdir.join('config-filename.yaml')
 | 
			
		||||
    config_yaml.ensure()
 | 
			
		||||
    with config_yaml.open('w') as f:
 | 
			
		||||
        syaml.dump_config(config_low, f)
 | 
			
		||||
 | 
			
		||||
    spack.config.add_from_file(str(config_yaml), scope="low")
 | 
			
		||||
    assert "build_stage" in spack.config.get('config')
 | 
			
		||||
    build_stages = spack.config.get('config')['build_stage']
 | 
			
		||||
    for stage in config_low['config']['build_stage']:
 | 
			
		||||
        assert stage in build_stages
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# repos
 | 
			
		||||
def test_write_list_in_memory(mock_low_high_config):
 | 
			
		||||
 
 | 
			
		||||
@@ -331,7 +331,7 @@ _spacktivate() {
 | 
			
		||||
_spack() {
 | 
			
		||||
    if $list_options
 | 
			
		||||
    then
 | 
			
		||||
        SPACK_COMPREPLY="-h --help -H --all-help --color -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
 | 
			
		||||
        SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
 | 
			
		||||
    else
 | 
			
		||||
        SPACK_COMPREPLY="activate add arch blame build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop docs edit env extensions external fetch find flake8 gc gpg graph help info install license list load location log-parse maintainers mark mirror module patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
 | 
			
		||||
    fi
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user