Allow overrides for spack.config set() and override(). (#14432)
Allows spack.config InternalConfigScope and Configuration.set() to handle keys with trailing ':' to indicate replacement vs merge behavior with respect to lower priority scopes. Lists may now be replaced rather than merged (this behavior was previously only available for dictionaries). This commit adds tests for the new behavior.
This commit is contained in:
parent
b2e7e7edaa
commit
da5144793e
@ -280,6 +280,7 @@ def __init__(self, name, data=None):
|
|||||||
self.sections = syaml.syaml_dict()
|
self.sections = syaml.syaml_dict()
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
|
data = InternalConfigScope._process_dict_keyname_overrides(data)
|
||||||
for section in data:
|
for section in data:
|
||||||
dsec = data[section]
|
dsec = data[section]
|
||||||
validate({section: dsec}, section_schemas[section])
|
validate({section: dsec}, section_schemas[section])
|
||||||
@ -306,6 +307,25 @@ def write_section(self, section):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<InternalConfigScope: %s>' % self.name
|
return '<InternalConfigScope: %s>' % self.name
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _process_dict_keyname_overrides(data):
|
||||||
|
"""Turn a trailing `:' in a key name into an override attribute."""
|
||||||
|
result = {}
|
||||||
|
for sk, sv in iteritems(data):
|
||||||
|
if sk.endswith(':'):
|
||||||
|
key = syaml.syaml_str(sk[:-1])
|
||||||
|
key.override = True
|
||||||
|
else:
|
||||||
|
key = sk
|
||||||
|
|
||||||
|
if isinstance(sv, dict):
|
||||||
|
result[key]\
|
||||||
|
= InternalConfigScope._process_dict_keyname_overrides(sv)
|
||||||
|
else:
|
||||||
|
result[key] = copy.copy(sv)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Configuration(object):
|
class Configuration(object):
|
||||||
"""A full Spack configuration, from a hierarchy of config files.
|
"""A full Spack configuration, from a hierarchy of config files.
|
||||||
@ -505,14 +525,14 @@ def set(self, path, value, scope=None):
|
|||||||
|
|
||||||
Accepts the path syntax described in ``get()``.
|
Accepts the path syntax described in ``get()``.
|
||||||
"""
|
"""
|
||||||
section, _, rest = path.partition(':')
|
parts = _process_config_path(path)
|
||||||
|
section = parts.pop(0)
|
||||||
|
|
||||||
if not rest:
|
if not parts:
|
||||||
self.update_config(section, value, scope=scope)
|
self.update_config(section, value, scope=scope)
|
||||||
else:
|
else:
|
||||||
section_data = self.get_config(section, scope=scope)
|
section_data = self.get_config(section, scope=scope)
|
||||||
|
|
||||||
parts = rest.split(':')
|
|
||||||
data = section_data
|
data = section_data
|
||||||
while len(parts) > 1:
|
while len(parts) > 1:
|
||||||
key = parts.pop(0)
|
key = parts.pop(0)
|
||||||
@ -612,7 +632,7 @@ def _config():
|
|||||||
"""Singleton Configuration instance.
|
"""Singleton Configuration instance.
|
||||||
|
|
||||||
This constructs one instance associated with this module and returns
|
This constructs one instance associated with this module and returns
|
||||||
it. It is bundled inside a function so that configuratoin can be
|
it. It is bundled inside a function so that configuration can be
|
||||||
initialized lazily.
|
initialized lazily.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
@ -763,17 +783,12 @@ def _merge_yaml(dest, source):
|
|||||||
Config file authors can optionally end any attribute in a dict
|
Config file authors can optionally end any attribute in a dict
|
||||||
with `::` instead of `:`, and the key will override that of the
|
with `::` instead of `:`, and the key will override that of the
|
||||||
parent instead of merging.
|
parent instead of merging.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def they_are(t):
|
def they_are(t):
|
||||||
return isinstance(dest, t) and isinstance(source, t)
|
return isinstance(dest, t) and isinstance(source, t)
|
||||||
|
|
||||||
# If both are None, handle specially and return None.
|
|
||||||
if source is None and dest is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# If source is None, overwrite with source.
|
# If source is None, overwrite with source.
|
||||||
elif source is None:
|
if source is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Source list is prepended (for precedence)
|
# Source list is prepended (for precedence)
|
||||||
@ -799,8 +814,9 @@ def they_are(t):
|
|||||||
# to copy mark information on source keys to dest.
|
# to copy mark information on source keys to dest.
|
||||||
key_marks[sk] = sk
|
key_marks[sk] = sk
|
||||||
|
|
||||||
# ensure that keys are marked in the destination. the key_marks dict
|
# ensure that keys are marked in the destination. The
|
||||||
# ensures we can get the actual source key objects from dest keys
|
# key_marks dict ensures we can get the actual source key
|
||||||
|
# objects from dest keys
|
||||||
for dk in list(dest.keys()):
|
for dk in list(dest.keys()):
|
||||||
if dk in key_marks and syaml.markable(dk):
|
if dk in key_marks and syaml.markable(dk):
|
||||||
syaml.mark(dk, key_marks[dk])
|
syaml.mark(dk, key_marks[dk])
|
||||||
@ -812,9 +828,34 @@ def they_are(t):
|
|||||||
|
|
||||||
return dest
|
return dest
|
||||||
|
|
||||||
# In any other case, overwrite with a copy of the source value.
|
# If we reach here source and dest are either different types or are
|
||||||
else:
|
# not both lists or dicts: replace with source.
|
||||||
return copy.copy(source)
|
return copy.copy(source)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Process a path argument to config.set() that may contain overrides ('::' or
|
||||||
|
# trailing ':')
|
||||||
|
#
|
||||||
|
def _process_config_path(path):
|
||||||
|
result = []
|
||||||
|
if path.startswith(':'):
|
||||||
|
raise syaml.SpackYAMLError("Illegal leading `:' in path `{0}'".
|
||||||
|
format(path), '')
|
||||||
|
seen_override_in_path = False
|
||||||
|
while path:
|
||||||
|
front, sep, path = path.partition(':')
|
||||||
|
if (sep and not path) or path.startswith(':'):
|
||||||
|
if seen_override_in_path:
|
||||||
|
raise syaml.SpackYAMLError("Meaningless second override"
|
||||||
|
" indicator `::' in path `{0}'".
|
||||||
|
format(path), '')
|
||||||
|
path = path.lstrip(':')
|
||||||
|
front = syaml.syaml_str(front)
|
||||||
|
front.override = True
|
||||||
|
seen_override_in_path = True
|
||||||
|
result.append(front)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -46,7 +46,19 @@
|
|||||||
|
|
||||||
config_override_list = {
|
config_override_list = {
|
||||||
'config': {
|
'config': {
|
||||||
'build_stage:': ['patha', 'pathb']}}
|
'build_stage:': ['pathd', 'pathe']}}
|
||||||
|
|
||||||
|
config_merge_dict = {
|
||||||
|
'config': {
|
||||||
|
'info': {
|
||||||
|
'a': 3,
|
||||||
|
'b': 4}}}
|
||||||
|
|
||||||
|
config_override_dict = {
|
||||||
|
'config': {
|
||||||
|
'info:': {
|
||||||
|
'a': 7,
|
||||||
|
'c': 9}}}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@ -382,7 +394,7 @@ def test_read_config_override_list(mock_low_high_config, write_config_file):
|
|||||||
write_config_file('config', config_override_list, 'high')
|
write_config_file('config', config_override_list, 'high')
|
||||||
assert spack.config.get('config') == {
|
assert spack.config.get('config') == {
|
||||||
'install_tree': 'install_tree_path',
|
'install_tree': 'install_tree_path',
|
||||||
'build_stage': ['patha', 'pathb']
|
'build_stage': config_override_list['config']['build_stage:']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -857,3 +869,74 @@ def test_dotkit_in_config_does_not_raise(
|
|||||||
# we throw a a deprecation warning without raising
|
# we throw a a deprecation warning without raising
|
||||||
assert '_sp_sys_type' in captured[0] # stdout
|
assert '_sp_sys_type' in captured[0] # stdout
|
||||||
assert 'Warning' in captured[1] # stderr
|
assert 'Warning' in captured[1] # stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_config_section_override(mock_low_high_config,
|
||||||
|
write_config_file):
|
||||||
|
write_config_file('config', config_merge_list, 'low')
|
||||||
|
wanted_list = config_override_list['config']['build_stage:']
|
||||||
|
mock_low_high_config.push_scope(spack.config.InternalConfigScope
|
||||||
|
('high', {
|
||||||
|
'config:': {
|
||||||
|
'build_stage': wanted_list
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
assert mock_low_high_config.get('config:build_stage') == wanted_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_config_dict_override(mock_low_high_config,
|
||||||
|
write_config_file):
|
||||||
|
write_config_file('config', config_merge_dict, 'low')
|
||||||
|
wanted_dict = config_override_dict['config']['info:']
|
||||||
|
mock_low_high_config.push_scope(spack.config.InternalConfigScope
|
||||||
|
('high', config_override_dict))
|
||||||
|
assert mock_low_high_config.get('config:info') == wanted_dict
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_config_list_override(mock_low_high_config,
|
||||||
|
write_config_file):
|
||||||
|
write_config_file('config', config_merge_list, 'low')
|
||||||
|
wanted_list = config_override_list['config']['build_stage:']
|
||||||
|
mock_low_high_config.push_scope(spack.config.InternalConfigScope
|
||||||
|
('high', config_override_list))
|
||||||
|
assert mock_low_high_config.get('config:build_stage') == wanted_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_section_override(mock_low_high_config, write_config_file):
|
||||||
|
write_config_file('config', config_merge_list, 'low')
|
||||||
|
wanted_list = config_override_list['config']['build_stage:']
|
||||||
|
with spack.config.override('config::build_stage', wanted_list):
|
||||||
|
assert mock_low_high_config.get('config:build_stage') == wanted_list
|
||||||
|
assert config_merge_list['config']['build_stage'] == \
|
||||||
|
mock_low_high_config.get('config:build_stage')
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_list_override(mock_low_high_config, write_config_file):
|
||||||
|
write_config_file('config', config_merge_list, 'low')
|
||||||
|
wanted_list = config_override_list['config']['build_stage:']
|
||||||
|
with spack.config.override('config:build_stage:', wanted_list):
|
||||||
|
assert wanted_list == mock_low_high_config.get('config:build_stage')
|
||||||
|
assert config_merge_list['config']['build_stage'] == \
|
||||||
|
mock_low_high_config.get('config:build_stage')
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_dict_override(mock_low_high_config, write_config_file):
|
||||||
|
write_config_file('config', config_merge_dict, 'low')
|
||||||
|
wanted_dict = config_override_dict['config']['info:']
|
||||||
|
with spack.config.override('config:info:', wanted_dict):
|
||||||
|
assert wanted_dict == mock_low_high_config.get('config:info')
|
||||||
|
assert config_merge_dict['config']['info'] == \
|
||||||
|
mock_low_high_config.get('config:info')
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_bad_path(config):
|
||||||
|
with pytest.raises(syaml.SpackYAMLError, match='Illegal leading'):
|
||||||
|
with spack.config.override(':bad:path', ''):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_path_double_override(config):
|
||||||
|
with pytest.raises(syaml.SpackYAMLError,
|
||||||
|
match='Meaningless second override'):
|
||||||
|
with spack.config.override('bad::double:override::directive', ''):
|
||||||
|
pass
|
||||||
|
Loading…
Reference in New Issue
Block a user