Deprecate blacklist/whitelist in favor of include/exclude (#31569)

For a long time the module configuration has had a few settings that use
`blacklist`/`whitelist` terminology. We've been asked by some of our users to replace
this with more inclusive language. In addition to being non-inclusive, `blacklist` and
`whitelist` are inconsistent with the rest of Spack, which uses `include` and `exclude`
for the same concepts.

- [x] Deprecate `blacklist`, `whitelist`, `blacklist_implicits` and `environment_blacklist`
      in favor of `exclude`, `include`, `exclude_implicits` and `exclude_env_vars` in module
      configuration, to be removed in Spack v0.20.
- [x] Print deprecation warnings if any of the deprecated names are in module config.
- [x] Update tests to test old and new names.
- [x] Update docs.
- [x] Update `spack config update` to fix this automatically, and include a note in the error
      that you can use this command.
This commit is contained in:
Todd Gamblin 2022-07-14 13:42:33 -07:00 committed by GitHub
parent 875b032151
commit 3d0347ddd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 375 additions and 135 deletions

View File

@ -308,7 +308,7 @@ the variable ``FOOBAR`` will be unset.
spec constraints are instead evaluated top to bottom. spec constraints are instead evaluated top to bottom.
"""""""""""""""""""""""""""""""""""""""""""" """"""""""""""""""""""""""""""""""""""""""""
Blacklist or whitelist specific module files Exclude or include specific module files
"""""""""""""""""""""""""""""""""""""""""""" """"""""""""""""""""""""""""""""""""""""""""
You can use anonymous specs also to prevent module files from being written or You can use anonymous specs also to prevent module files from being written or
@ -322,8 +322,8 @@ your system. If you write a configuration file like:
modules: modules:
default: default:
tcl: tcl:
whitelist: ['gcc', 'llvm'] # Whitelist will have precedence over blacklist include: ['gcc', 'llvm'] # include will have precedence over exclude
blacklist: ['%gcc@4.4.7'] # Assuming gcc@4.4.7 is the system compiler exclude: ['%gcc@4.4.7'] # Assuming gcc@4.4.7 is the system compiler
you will prevent the generation of module files for any package that you will prevent the generation of module files for any package that
is compiled with ``gcc@4.4.7``, with the only exception of any ``gcc`` is compiled with ``gcc@4.4.7``, with the only exception of any ``gcc``
@ -490,7 +490,7 @@ satisfies a default, Spack will generate the module file in the
appropriate path, and will generate a default symlink to the module appropriate path, and will generate a default symlink to the module
file as well. file as well.
.. warning:: .. warning::
If Spack is configured to generate multiple default packages in the If Spack is configured to generate multiple default packages in the
same directory, the last modulefile to be generated will be the same directory, the last modulefile to be generated will be the
default module. default module.
@ -589,7 +589,7 @@ Filter out environment modifications
Modifications to certain environment variables in module files are there by Modifications to certain environment variables in module files are there by
default, for instance because they are generated by prefix inspections. default, for instance because they are generated by prefix inspections.
If you want to prevent modifications to some environment variables, you can If you want to prevent modifications to some environment variables, you can
do so by using the environment blacklist: do so by using the ``exclude_env_vars``:
.. code-block:: yaml .. code-block:: yaml
@ -599,7 +599,7 @@ do so by using the environment blacklist:
all: all:
filter: filter:
# Exclude changes to any of these variables # Exclude changes to any of these variables
environment_blacklist: ['CPATH', 'LIBRARY_PATH'] exclude_env_vars: ['CPATH', 'LIBRARY_PATH']
The configuration above will generate module files that will not contain The configuration above will generate module files that will not contain
modifications to either ``CPATH`` or ``LIBRARY_PATH``. modifications to either ``CPATH`` or ``LIBRARY_PATH``.

View File

@ -618,7 +618,7 @@ def get_buildfile_manifest(spec):
Return a data structure with information about a build, including Return a data structure with information about a build, including
text_to_relocate, binary_to_relocate, binary_to_relocate_fullpath text_to_relocate, binary_to_relocate, binary_to_relocate_fullpath
link_to_relocate, and other, which means it doesn't fit any of previous link_to_relocate, and other, which means it doesn't fit any of previous
checks (and should not be relocated). We blacklist docs (man) and checks (and should not be relocated). We exclude docs (man) and
metadata (.spack). This can be used to find a particular kind of file metadata (.spack). This can be used to find a particular kind of file
in spack, or to generate the build metadata. in spack, or to generate the build metadata.
""" """
@ -626,12 +626,12 @@ def get_buildfile_manifest(spec):
"link_to_relocate": [], "other": [], "link_to_relocate": [], "other": [],
"binary_to_relocate_fullpath": []} "binary_to_relocate_fullpath": []}
blacklist = (".spack", "man") exclude_list = (".spack", "man")
# Do this at during tarball creation to save time when tarball unpacked. # Do this at during tarball creation to save time when tarball unpacked.
# Used by make_package_relative to determine binaries to change. # Used by make_package_relative to determine binaries to change.
for root, dirs, files in os.walk(spec.prefix, topdown=True): for root, dirs, files in os.walk(spec.prefix, topdown=True):
dirs[:] = [d for d in dirs if d not in blacklist] dirs[:] = [d for d in dirs if d not in exclude_list]
# Directories may need to be relocated too. # Directories may need to be relocated too.
for directory in dirs: for directory in dirs:

View File

@ -104,9 +104,9 @@ def edit(parser, args):
path = os.path.join(path, name) path = os.path.join(path, name)
if not os.path.exists(path): if not os.path.exists(path):
files = glob.glob(path + '*') files = glob.glob(path + '*')
blacklist = ['.pyc', '~'] # blacklist binaries and backups exclude_list = ['.pyc', '~'] # exclude binaries and backups
files = list(filter( files = list(filter(
lambda x: all(s not in x for s in blacklist), files)) lambda x: all(s not in x for s in exclude_list), files))
if len(files) > 1: if len(files) > 1:
m = 'Multiple files exist with the name {0}.'.format(name) m = 'Multiple files exist with the name {0}.'.format(name)
m += ' Please specify a suffix. Files are:\n\n' m += ' Please specify a suffix. Files are:\n\n'

View File

@ -131,7 +131,7 @@ def check_module_set_name(name):
_missing_modules_warning = ( _missing_modules_warning = (
"Modules have been omitted for one or more specs, either" "Modules have been omitted for one or more specs, either"
" because they were blacklisted or because the spec is" " because they were excluded or because the spec is"
" associated with a package that is installed upstream and" " associated with a package that is installed upstream and"
" that installation has not generated a module file. Rerun" " that installation has not generated a module file. Rerun"
" this command with debug output enabled for more details.") " this command with debug output enabled for more details.")
@ -180,7 +180,7 @@ def loads(module_type, specs, args, out=None):
for spec, mod in modules: for spec, mod in modules:
if not mod: if not mod:
module_output_for_spec = ( module_output_for_spec = (
'## blacklisted or missing from upstream: {0}'.format( '## excluded or missing from upstream: {0}'.format(
spec.format())) spec.format()))
else: else:
d['exclude'] = '## ' if spec.name in exclude_set else '' d['exclude'] = '## ' if spec.name in exclude_set else ''
@ -293,8 +293,8 @@ def refresh(module_type, specs, args):
cls(spec, args.module_set_name) for spec in specs cls(spec, args.module_set_name) for spec in specs
if spack.repo.path.exists(spec.name)] if spack.repo.path.exists(spec.name)]
# Filter blacklisted packages early # Filter excluded packages early
writers = [x for x in writers if not x.conf.blacklisted] writers = [x for x in writers if not x.conf.excluded]
# Detect name clashes in module files # Detect name clashes in module files
file2writer = collections.defaultdict(list) file2writer = collections.defaultdict(list)

View File

@ -54,6 +54,34 @@
import spack.util.spack_yaml as syaml import spack.util.spack_yaml as syaml
def get_deprecated(dictionary, name, old_name, default):
"""Get a deprecated property from a ``dict``.
Arguments:
dictionary (dict): dictionary to get a value from.
name (str): New name for the property. If present, supersedes ``old_name``.
old_name (str): Deprecated name for the property. If present, a warning
is printed.
default (object): value to return if neither name is found.
"""
value = default
# always warn if old name is present
if old_name in dictionary:
value = dictionary.get(old_name, value)
main_msg = "`{}:` is deprecated in module config and will be removed in v0.20."
details = (
"Use `{}:` instead. You can run `spack config update` to translate your "
"configuration files automatically."
)
tty.warn(main_msg.format(old_name), details.format(name))
# name overrides old name if present
value = dictionary.get(name, value)
return value
#: config section for this file #: config section for this file
def configuration(module_set_name): def configuration(module_set_name):
config_path = 'modules:%s' % module_set_name config_path = 'modules:%s' % module_set_name
@ -351,14 +379,14 @@ def get_module(
Retrieve the module file for the given spec if it is available. If the Retrieve the module file for the given spec if it is available. If the
module is not available, this will raise an exception unless the module module is not available, this will raise an exception unless the module
is blacklisted or if the spec is installed upstream. is excluded or if the spec is installed upstream.
Args: Args:
module_type: the type of module we want to retrieve (e.g. lmod) module_type: the type of module we want to retrieve (e.g. lmod)
spec: refers to the installed package that we want to retrieve a module spec: refers to the installed package that we want to retrieve a module
for for
required: if the module is required but blacklisted, this function will required: if the module is required but excluded, this function will
print a debug message. If a module is missing but not blacklisted, print a debug message. If a module is missing but not excluded,
then an exception is raised (regardless of whether it is required) then an exception is raised (regardless of whether it is required)
get_full_path: if ``True``, this returns the full path to the module. get_full_path: if ``True``, this returns the full path to the module.
Otherwise, this returns the module name. Otherwise, this returns the module name.
@ -386,13 +414,13 @@ def get_module(
else: else:
writer = spack.modules.module_types[module_type](spec, module_set_name) writer = spack.modules.module_types[module_type](spec, module_set_name)
if not os.path.isfile(writer.layout.filename): if not os.path.isfile(writer.layout.filename):
if not writer.conf.blacklisted: if not writer.conf.excluded:
err_msg = "No module available for package {0} at {1}".format( err_msg = "No module available for package {0} at {1}".format(
spec, writer.layout.filename spec, writer.layout.filename
) )
raise ModuleNotFoundError(err_msg) raise ModuleNotFoundError(err_msg)
elif required: elif required:
tty.debug("The module configuration has blacklisted {0}: " tty.debug("The module configuration has excluded {0}: "
"omitting it".format(spec)) "omitting it".format(spec))
else: else:
return None return None
@ -483,26 +511,30 @@ def hash(self):
return None return None
@property @property
def blacklisted(self): def excluded(self):
"""Returns True if the module has been blacklisted, """Returns True if the module has been excluded, False otherwise."""
False otherwise.
"""
# A few variables for convenience of writing the method # A few variables for convenience of writing the method
spec = self.spec spec = self.spec
conf = self.module.configuration(self.name) conf = self.module.configuration(self.name)
# Compute the list of whitelist rules that match # Compute the list of include rules that match
wlrules = conf.get('whitelist', []) # DEPRECATED: remove 'whitelist' in v0.20
whitelist_matches = [x for x in wlrules if spec.satisfies(x)] include_rules = get_deprecated(conf, "include", "whitelist", [])
include_matches = [x for x in include_rules if spec.satisfies(x)]
# Compute the list of blacklist rules that match # Compute the list of exclude rules that match
blrules = conf.get('blacklist', []) # DEPRECATED: remove 'blacklist' in v0.20
blacklist_matches = [x for x in blrules if spec.satisfies(x)] exclude_rules = get_deprecated(conf, "exclude", "blacklist", [])
exclude_matches = [x for x in exclude_rules if spec.satisfies(x)]
# Should I blacklist the module because it's implicit? # Should I exclude the module because it's implicit?
blacklist_implicits = conf.get('blacklist_implicits') # DEPRECATED: remove 'blacklist_implicits' in v0.20
exclude_implicits = get_deprecated(
conf, "exclude_implicits", "blacklist_implicits", None
)
installed_implicitly = not spec._installed_explicitly() installed_implicitly = not spec._installed_explicitly()
blacklisted_as_implicit = blacklist_implicits and installed_implicitly excluded_as_implicit = exclude_implicits and installed_implicitly
def debug_info(line_header, match_list): def debug_info(line_header, match_list):
if match_list: if match_list:
@ -511,15 +543,15 @@ def debug_info(line_header, match_list):
for rule in match_list: for rule in match_list:
tty.debug('\t\tmatches rule: {0}'.format(rule)) tty.debug('\t\tmatches rule: {0}'.format(rule))
debug_info('WHITELIST', whitelist_matches) debug_info('INCLUDE', include_matches)
debug_info('BLACKLIST', blacklist_matches) debug_info('EXCLUDE', exclude_matches)
if blacklisted_as_implicit: if excluded_as_implicit:
msg = '\tBLACKLISTED_AS_IMPLICIT : {0}'.format(spec.cshort_spec) msg = '\tEXCLUDED_AS_IMPLICIT : {0}'.format(spec.cshort_spec)
tty.debug(msg) tty.debug(msg)
is_blacklisted = blacklist_matches or blacklisted_as_implicit is_excluded = exclude_matches or excluded_as_implicit
if not whitelist_matches and is_blacklisted: if not include_matches and is_excluded:
return True return True
return False return False
@ -544,17 +576,22 @@ def specs_to_prereq(self):
return self._create_list_for('prerequisites') return self._create_list_for('prerequisites')
@property @property
def environment_blacklist(self): def exclude_env_vars(self):
"""List of variables that should be left unmodified.""" """List of variables that should be left unmodified."""
return self.conf.get('filter', {}).get('environment_blacklist', {}) filter = self.conf.get('filter', {})
# DEPRECATED: remove in v0.20
return get_deprecated(
filter, "exclude_env_vars", "environment_blacklist", {}
)
def _create_list_for(self, what): def _create_list_for(self, what):
whitelist = [] include = []
for item in self.conf[what]: for item in self.conf[what]:
conf = type(self)(item, self.name) conf = type(self)(item, self.name)
if not conf.blacklisted: if not conf.excluded:
whitelist.append(item) include.append(item)
return whitelist return include
@property @property
def verbose(self): def verbose(self):
@ -733,8 +770,8 @@ def environment_modifications(self):
# Modifications required from modules.yaml # Modifications required from modules.yaml
env.extend(self.conf.env) env.extend(self.conf.env)
# List of variables that are blacklisted in modules.yaml # List of variables that are excluded in modules.yaml
blacklist = self.conf.environment_blacklist exclude = self.conf.exclude_env_vars
# We may have tokens to substitute in environment commands # We may have tokens to substitute in environment commands
@ -758,7 +795,7 @@ def environment_modifications(self):
pass pass
x.name = str(x.name).replace('-', '_') x.name = str(x.name).replace('-', '_')
return [(type(x).__name__, x) for x in env if x.name not in blacklist] return [(type(x).__name__, x) for x in env if x.name not in exclude]
@tengine.context_property @tengine.context_property
def autoload(self): def autoload(self):
@ -831,9 +868,9 @@ def write(self, overwrite=False):
existing file. If False the operation is skipped an we print existing file. If False the operation is skipped an we print
a warning to the user. a warning to the user.
""" """
# Return immediately if the module is blacklisted # Return immediately if the module is excluded
if self.conf.blacklisted: if self.conf.excluded:
msg = '\tNOT WRITING: {0} [BLACKLISTED]' msg = '\tNOT WRITING: {0} [EXCLUDED]'
tty.debug(msg.format(self.spec.cshort_spec)) tty.debug(msg.format(self.spec.cshort_spec))
return return

View File

@ -18,9 +18,13 @@
#: #:
#: THIS NEEDS TO BE UPDATED FOR EVERY NEW KEYWORD THAT #: THIS NEEDS TO BE UPDATED FOR EVERY NEW KEYWORD THAT
#: IS ADDED IMMEDIATELY BELOW THE MODULE TYPE ATTRIBUTE #: IS ADDED IMMEDIATELY BELOW THE MODULE TYPE ATTRIBUTE
spec_regex = r'(?!hierarchy|core_specs|verbose|hash_length|whitelist|' \ spec_regex = (
r'blacklist|projections|naming_scheme|core_compilers|all|' \ r'(?!hierarchy|core_specs|verbose|hash_length|defaults|'
r'defaults)(^\w[\w-]*)' r'whitelist|blacklist|' # DEPRECATED: remove in 0.20.
r'include|exclude|' # use these more inclusive/consistent options
r'projections|naming_scheme|core_compilers|all)(^\w[\w-]*)'
)
#: Matches a valid name for a module set #: Matches a valid name for a module set
valid_module_set_name = r'^(?!arch_folder$|lmod$|roots$|enable$|prefix_inspections$|'\ valid_module_set_name = r'^(?!arch_folder$|lmod$|roots$|enable$|prefix_inspections$|'\
@ -50,12 +54,21 @@
'default': {}, 'default': {},
'additionalProperties': False, 'additionalProperties': False,
'properties': { 'properties': {
# DEPRECATED: remove in 0.20.
'environment_blacklist': { 'environment_blacklist': {
'type': 'array', 'type': 'array',
'default': [], 'default': [],
'items': { 'items': {
'type': 'string' 'type': 'string'
} }
},
# use exclude_env_vars instead
'exclude_env_vars': {
'type': 'array',
'default': [],
'items': {
'type': 'string'
}
} }
} }
}, },
@ -95,12 +108,20 @@
'minimum': 0, 'minimum': 0,
'default': 7 'default': 7
}, },
# DEPRECATED: remove in 0.20.
'whitelist': array_of_strings, 'whitelist': array_of_strings,
'blacklist': array_of_strings, 'blacklist': array_of_strings,
'blacklist_implicits': { 'blacklist_implicits': {
'type': 'boolean', 'type': 'boolean',
'default': False 'default': False
}, },
# whitelist/blacklist have been replaced with include/exclude
'include': array_of_strings,
'exclude': array_of_strings,
'exclude_implicits': {
'type': 'boolean',
'default': False
},
'defaults': array_of_strings, 'defaults': array_of_strings,
'naming_scheme': { 'naming_scheme': {
'type': 'string' # Can we be more specific here? 'type': 'string' # Can we be more specific here?
@ -224,14 +245,51 @@ def deprecation_msg_default_module_set(instance, props):
} }
def update(data): # deprecated keys and their replacements
"""Update the data in place to remove deprecated properties. exclude_include_translations = {
"whitelist": "include",
"blacklist": "exclude",
"blacklist_implicits": "exclude_implicits",
"environment_blacklist": "exclude_env_vars",
}
Args:
data (dict): dictionary to be updated
Returns: def update_keys(data, key_translations):
True if data was changed, False otherwise """Change blacklist/whitelist to exclude/include.
Arguments:
data (dict): data from a valid modules configuration.
key_translations (dict): A dictionary of keys to translate to
their respective values.
Return:
(bool) whether anything was changed in data
"""
changed = False
if isinstance(data, dict):
keys = list(data.keys())
for key in keys:
value = data[key]
translation = key_translations.get(key)
if translation:
data[translation] = data.pop(key)
changed = True
changed |= update_keys(value, key_translations)
elif isinstance(data, list):
for elt in data:
changed |= update_keys(elt, key_translations)
return changed
def update_default_module_set(data):
"""Update module configuration to move top-level keys inside default module set.
This change was introduced in v0.18 (see 99083f1706 or #28659).
""" """
changed = False changed = False
@ -258,3 +316,21 @@ def update(data):
data['default'] = default data['default'] = default
return changed return changed
def update(data):
"""Update the data in place to remove deprecated properties.
Args:
data (dict): dictionary to be updated
Returns:
True if data was changed, False otherwise
"""
# deprecated top-level module config (everything in default module set)
changed = update_default_module_set(data)
# translate blacklist/whitelist to exclude/include
changed |= update_keys(data, exclude_include_translations)
return changed

View File

@ -149,16 +149,20 @@ def test_find_recursive():
@pytest.mark.db @pytest.mark.db
def test_find_recursive_blacklisted(database, module_configuration): # DEPRECATED: remove blacklist in v0.20
module_configuration('blacklist') @pytest.mark.parametrize("config_name", ["exclude", "blacklist"])
def test_find_recursive_excluded(database, module_configuration, config_name):
module_configuration(config_name)
module('lmod', 'refresh', '-y', '--delete-tree') module('lmod', 'refresh', '-y', '--delete-tree')
module('lmod', 'find', '-r', 'mpileaks ^mpich') module('lmod', 'find', '-r', 'mpileaks ^mpich')
@pytest.mark.db @pytest.mark.db
def test_loads_recursive_blacklisted(database, module_configuration): # DEPRECATED: remove blacklist in v0.20
module_configuration('blacklist') @pytest.mark.parametrize("config_name", ["exclude", "blacklist"])
def test_loads_recursive_excluded(database, module_configuration, config_name):
module_configuration(config_name)
module('lmod', 'refresh', '-y', '--delete-tree') module('lmod', 'refresh', '-y', '--delete-tree')
output = module('lmod', 'loads', '-r', 'mpileaks ^mpich') output = module('lmod', 'loads', '-r', 'mpileaks ^mpich')
@ -166,7 +170,7 @@ def test_loads_recursive_blacklisted(database, module_configuration):
assert any(re.match(r'[^#]*module load.*mpileaks', ln) for ln in lines) assert any(re.match(r'[^#]*module load.*mpileaks', ln) for ln in lines)
assert not any(re.match(r'[^#]module load.*callpath', ln) for ln in lines) assert not any(re.match(r'[^#]module load.*callpath', ln) for ln in lines)
assert any(re.match(r'## blacklisted or missing.*callpath', ln) assert any(re.match(r'## excluded or missing.*callpath', ln)
for ln in lines) for ln in lines)
# TODO: currently there is no way to separate stdout and stderr when # TODO: currently there is no way to separate stdout and stderr when

View File

@ -1003,7 +1003,7 @@ def __call__(self, filename):
@pytest.fixture() @pytest.fixture()
def module_configuration(monkeypatch, request): def module_configuration(monkeypatch, request, mutable_config):
"""Reads the module configuration file from the mock ones prepared """Reads the module configuration file from the mock ones prepared
for tests and monkeypatches the right classes to hook it in. for tests and monkeypatches the right classes to hook it in.
""" """
@ -1018,6 +1018,8 @@ def module_configuration(monkeypatch, request):
spack.paths.test_path, 'data', 'modules', writer_key spack.paths.test_path, 'data', 'modules', writer_key
) )
# ConfigUpdate, when called, will modify configuration, so we need to use
# the mutable_config fixture
return ConfigUpdate(root_for_conf, writer_mod, writer_key, monkeypatch) return ConfigUpdate(root_for_conf, writer_mod, writer_key, monkeypatch)

View File

@ -10,7 +10,7 @@ lmod:
all: all:
autoload: none autoload: none
filter: filter:
environment_blacklist: exclude_env_vars:
- CMAKE_PREFIX_PATH - CMAKE_PREFIX_PATH
environment: environment:
set: set:

View File

@ -1,3 +1,5 @@
# DEPRECATED: remove this in v0.20
# See `exclude.yaml` for the new syntax
enable: enable:
- lmod - lmod
lmod: lmod:

View File

@ -0,0 +1,30 @@
# DEPRECATED: remove this in v0.20
# See `alter_environment.yaml` for the new syntax
enable:
- lmod
lmod:
core_compilers:
- 'clang@3.3'
hierarchy:
- mpi
all:
autoload: none
filter:
environment_blacklist:
- CMAKE_PREFIX_PATH
environment:
set:
'{name}_ROOT': '{prefix}'
'platform=test target=x86_64':
environment:
set:
FOO: 'foo'
unset:
- BAR
'platform=test target=core2':
load:
- 'foo/bar'

View File

@ -0,0 +1,12 @@
enable:
- lmod
lmod:
core_compilers:
- 'clang@3.3'
hierarchy:
- mpi
exclude:
- callpath
all:
autoload: direct

View File

@ -4,7 +4,7 @@ tcl:
all: all:
autoload: none autoload: none
filter: filter:
environment_blacklist: exclude_env_vars:
- CMAKE_PREFIX_PATH - CMAKE_PREFIX_PATH
environment: environment:
set: set:

View File

@ -1,3 +1,5 @@
# DEPRECATED: remove this in v0.20
# See `exclude.yaml` for the new syntax
enable: enable:
- tcl - tcl
tcl: tcl:

View File

@ -0,0 +1,25 @@
# DEPRECATED: remove this in v0.20
# See `alter_environment.yaml` for the new syntax
enable:
- tcl
tcl:
all:
autoload: none
filter:
environment_blacklist:
- CMAKE_PREFIX_PATH
environment:
set:
'{name}_ROOT': '{prefix}'
'platform=test target=x86_64':
environment:
set:
FOO: 'foo'
OMPI_MCA_mpi_leave_pinned: '1'
unset:
- BAR
'platform=test target=core2':
load:
- 'foo/bar'

View File

@ -1,3 +1,5 @@
# DEPRECATED: remove this in v0.20
# See `exclude_implicits.yaml` for the new syntax
enable: enable:
- tcl - tcl
tcl: tcl:

View File

@ -0,0 +1,10 @@
enable:
- tcl
tcl:
include:
- zmpi
exclude:
- callpath
- mpi
all:
autoload: direct

View File

@ -0,0 +1,6 @@
enable:
- tcl
tcl:
exclude_implicits: true
all:
autoload: direct

View File

@ -379,40 +379,40 @@ def test_clear(env):
assert len(env) == 0 assert len(env) == 0
@pytest.mark.parametrize('env,blacklist,whitelist', [ @pytest.mark.parametrize('env,exclude,include', [
# Check we can blacklist a literal # Check we can exclude a literal
({'SHLVL': '1'}, ['SHLVL'], []), ({'SHLVL': '1'}, ['SHLVL'], []),
# Check whitelist takes precedence # Check include takes precedence
({'SHLVL': '1'}, ['SHLVL'], ['SHLVL']), ({'SHLVL': '1'}, ['SHLVL'], ['SHLVL']),
]) ])
def test_sanitize_literals(env, blacklist, whitelist): def test_sanitize_literals(env, exclude, include):
after = environment.sanitize(env, blacklist, whitelist) after = environment.sanitize(env, exclude, include)
# Check that all the whitelisted variables are there # Check that all the included variables are there
assert all(x in after for x in whitelist) assert all(x in after for x in include)
# Check that the blacklisted variables that are not # Check that the excluded variables that are not
# whitelisted are there # included are there
blacklist = list(set(blacklist) - set(whitelist)) exclude = list(set(exclude) - set(include))
assert all(x not in after for x in blacklist) assert all(x not in after for x in exclude)
@pytest.mark.parametrize('env,blacklist,whitelist,expected,deleted', [ @pytest.mark.parametrize('env,exclude,include,expected,deleted', [
# Check we can blacklist using a regex # Check we can exclude using a regex
({'SHLVL': '1'}, ['SH.*'], [], [], ['SHLVL']), ({'SHLVL': '1'}, ['SH.*'], [], [], ['SHLVL']),
# Check we can whitelist using a regex # Check we can include using a regex
({'SHLVL': '1'}, ['SH.*'], ['SH.*'], ['SHLVL'], []), ({'SHLVL': '1'}, ['SH.*'], ['SH.*'], ['SHLVL'], []),
# Check regex to blacklist Modules v4 related vars # Check regex to exclude Modules v4 related vars
({'MODULES_LMALTNAME': '1', 'MODULES_LMCONFLICT': '2'}, ({'MODULES_LMALTNAME': '1', 'MODULES_LMCONFLICT': '2'},
['MODULES_(.*)'], [], [], ['MODULES_LMALTNAME', 'MODULES_LMCONFLICT']), ['MODULES_(.*)'], [], [], ['MODULES_LMALTNAME', 'MODULES_LMCONFLICT']),
({'A_modquar': '1', 'b_modquar': '2', 'C_modshare': '3'}, ({'A_modquar': '1', 'b_modquar': '2', 'C_modshare': '3'},
[r'(\w*)_mod(quar|share)'], [], [], [r'(\w*)_mod(quar|share)'], [], [],
['A_modquar', 'b_modquar', 'C_modshare']), ['A_modquar', 'b_modquar', 'C_modshare']),
]) ])
def test_sanitize_regex(env, blacklist, whitelist, expected, deleted): def test_sanitize_regex(env, exclude, include, expected, deleted):
after = environment.sanitize(env, blacklist, whitelist) after = environment.sanitize(env, exclude, include)
assert all(x in after for x in expected) assert all(x in after for x in expected)
assert all(x not in after for x in deleted) assert all(x not in after for x in deleted)
@ -460,7 +460,7 @@ def test_from_environment_diff(before, after, search_list):
@pytest.mark.skipif(sys.platform == 'win32', @pytest.mark.skipif(sys.platform == 'win32',
reason="LMod not supported on Windows") reason="LMod not supported on Windows")
@pytest.mark.regression('15775') @pytest.mark.regression('15775')
def test_blacklist_lmod_variables(): def test_exclude_lmod_variables():
# Construct the list of environment modifications # Construct the list of environment modifications
file = os.path.join(datadir, 'sourceme_lmod.sh') file = os.path.join(datadir, 'sourceme_lmod.sh')
env = EnvironmentModifications.from_sourcing_file(file) env = EnvironmentModifications.from_sourcing_file(file)

View File

@ -11,7 +11,9 @@
import spack.error import spack.error
import spack.modules.tcl import spack.modules.tcl
import spack.package_base import spack.package_base
import spack.schema.modules
import spack.spec import spack.spec
import spack.util.spack_yaml as syaml
from spack.modules.common import UpstreamModuleIndex from spack.modules.common import UpstreamModuleIndex
from spack.spec import Spec from spack.spec import Spec
@ -226,3 +228,34 @@ def find_nothing(*args):
assert module_path assert module_path
spack.package_base.PackageBase.uninstall_by_spec(spec) spack.package_base.PackageBase.uninstall_by_spec(spec)
# DEPRECATED: remove blacklist in v0.20
@pytest.mark.parametrize("module_type, old_config,new_config", [
("tcl", "blacklist.yaml", "exclude.yaml"),
("tcl", "blacklist_implicits.yaml", "exclude_implicits.yaml"),
("tcl", "blacklist_environment.yaml", "alter_environment.yaml"),
("lmod", "blacklist.yaml", "exclude.yaml"),
("lmod", "blacklist_environment.yaml", "alter_environment.yaml"),
])
def test_exclude_include_update(module_type, old_config, new_config):
module_test_data_root = os.path.join(
spack.paths.test_path, 'data', 'modules', module_type
)
with open(os.path.join(module_test_data_root, old_config)) as f:
old_yaml = syaml.load(f)
with open(os.path.join(module_test_data_root, new_config)) as f:
new_yaml = syaml.load(f)
# ensure file that needs updating is translated to the right thing.
assert spack.schema.modules.update_keys(
old_yaml, spack.schema.modules.exclude_include_translations
)
assert new_yaml == old_yaml
# ensure a file that doesn't need updates doesn't get updated
original_new_yaml = new_yaml.copy()
assert not spack.schema.modules.update_keys(
new_yaml, spack.schema.modules.exclude_include_translations
)
original_new_yaml == new_yaml

View File

@ -110,10 +110,16 @@ def test_autoload_all(self, modulefile_content, module_configuration):
assert len([x for x in content if 'depends_on(' in x]) == 5 assert len([x for x in content if 'depends_on(' in x]) == 5
def test_alter_environment(self, modulefile_content, module_configuration): # DEPRECATED: remove blacklist in v0.20
@pytest.mark.parametrize(
"config_name", ["alter_environment", "blacklist_environment"]
)
def test_alter_environment(
self, modulefile_content, module_configuration, config_name
):
"""Tests modifications to run-time environment.""" """Tests modifications to run-time environment."""
module_configuration('alter_environment') module_configuration(config_name)
content = modulefile_content('mpileaks platform=test target=x86_64') content = modulefile_content('mpileaks platform=test target=x86_64')
assert len( assert len(
@ -145,10 +151,11 @@ def test_prepend_path_separator(self, modulefile_content,
elif re.match(r'[a-z]+_path\("SEMICOLON"', line): elif re.match(r'[a-z]+_path\("SEMICOLON"', line):
assert line.endswith('"bar", ";")') assert line.endswith('"bar", ";")')
def test_blacklist(self, modulefile_content, module_configuration): @pytest.mark.parametrize("config_name", ["exclude", "blacklist"])
"""Tests blacklisting the generation of selected modules.""" def test_exclude(self, modulefile_content, module_configuration, config_name):
"""Tests excluding the generation of selected modules."""
module_configuration('blacklist') module_configuration(config_name)
content = modulefile_content(mpileaks_spec_string) content = modulefile_content(mpileaks_spec_string)
assert len([x for x in content if 'depends_on(' in x]) == 1 assert len([x for x in content if 'depends_on(' in x]) == 1

View File

@ -97,10 +97,16 @@ def test_prerequisites_all(self, modulefile_content, module_configuration):
assert len([x for x in content if 'prereq' in x]) == 5 assert len([x for x in content if 'prereq' in x]) == 5
def test_alter_environment(self, modulefile_content, module_configuration): # DEPRECATED: remove blacklist in v0.20
@pytest.mark.parametrize(
"config_name", ["alter_environment", "blacklist_environment"]
)
def test_alter_environment(
self, modulefile_content, module_configuration, config_name
):
"""Tests modifications to run-time environment.""" """Tests modifications to run-time environment."""
module_configuration('alter_environment') module_configuration(config_name)
content = modulefile_content('mpileaks platform=test target=x86_64') content = modulefile_content('mpileaks platform=test target=x86_64')
assert len([x for x in content assert len([x for x in content
@ -129,10 +135,11 @@ def test_alter_environment(self, modulefile_content, module_configuration):
assert len([x for x in content if 'module load foo/bar' in x]) == 1 assert len([x for x in content if 'module load foo/bar' in x]) == 1
assert len([x for x in content if 'setenv LIBDWARF_ROOT' in x]) == 1 assert len([x for x in content if 'setenv LIBDWARF_ROOT' in x]) == 1
def test_blacklist(self, modulefile_content, module_configuration): @pytest.mark.parametrize("config_name", ["exclude", "blacklist"])
"""Tests blacklisting the generation of selected modules.""" def test_exclude(self, modulefile_content, module_configuration, config_name):
"""Tests excluding the generation of selected modules."""
module_configuration('blacklist') module_configuration(config_name)
content = modulefile_content('mpileaks ^zmpi') content = modulefile_content('mpileaks ^zmpi')
assert len([x for x in content if 'is-loaded' in x]) == 1 assert len([x for x in content if 'is-loaded' in x]) == 1
@ -359,24 +366,27 @@ def test_extend_context(
@pytest.mark.regression('4400') @pytest.mark.regression('4400')
@pytest.mark.db @pytest.mark.db
def test_blacklist_implicits( @pytest.mark.parametrize(
self, modulefile_content, module_configuration, database "config_name", ["exclude_implicits", "blacklist_implicits"]
)
def test_exclude_implicits(
self, modulefile_content, module_configuration, database, config_name
): ):
module_configuration('blacklist_implicits') module_configuration(config_name)
# mpileaks has been installed explicitly when setting up # mpileaks has been installed explicitly when setting up
# the tests database # the tests database
mpileaks_specs = database.query('mpileaks') mpileaks_specs = database.query('mpileaks')
for item in mpileaks_specs: for item in mpileaks_specs:
writer = writer_cls(item, 'default') writer = writer_cls(item, 'default')
assert not writer.conf.blacklisted assert not writer.conf.excluded
# callpath is a dependency of mpileaks, and has been pulled # callpath is a dependency of mpileaks, and has been pulled
# in implicitly # in implicitly
callpath_specs = database.query('callpath') callpath_specs = database.query('callpath')
for item in callpath_specs: for item in callpath_specs:
writer = writer_cls(item, 'default') writer = writer_cls(item, 'default')
assert writer.conf.blacklisted assert writer.conf.excluded
@pytest.mark.regression('9624') @pytest.mark.regression('9624')
@pytest.mark.db @pytest.mark.db

View File

@ -673,10 +673,10 @@ def from_sourcing_file(filename, *arguments, **kwargs):
(default: ``&> /dev/null``) (default: ``&> /dev/null``)
concatenate_on_success (str): operator used to execute a command concatenate_on_success (str): operator used to execute a command
only when the previous command succeeds (default: ``&&``) only when the previous command succeeds (default: ``&&``)
blacklist ([str or re]): ignore any modifications of these exclude ([str or re]): ignore any modifications of these
variables (default: []) variables (default: [])
whitelist ([str or re]): always respect modifications of these include ([str or re]): always respect modifications of these
variables (default: []). has precedence over blacklist. variables (default: []). Supersedes any excluded variables.
clean (bool): in addition to removing empty entries, clean (bool): in addition to removing empty entries,
also remove duplicate entries (default: False). also remove duplicate entries (default: False).
""" """
@ -687,13 +687,13 @@ def from_sourcing_file(filename, *arguments, **kwargs):
msg = 'Trying to source non-existing file: {0}'.format(filename) msg = 'Trying to source non-existing file: {0}'.format(filename)
raise RuntimeError(msg) raise RuntimeError(msg)
# Prepare a whitelist and a blacklist of environment variable names # Prepare include and exclude lists of environment variable names
blacklist = kwargs.get('blacklist', []) exclude = kwargs.get('exclude', [])
whitelist = kwargs.get('whitelist', []) include = kwargs.get('include', [])
clean = kwargs.get('clean', False) clean = kwargs.get('clean', False)
# Other variables unrelated to sourcing a file # Other variables unrelated to sourcing a file
blacklist.extend([ exclude.extend([
# Bash internals # Bash internals
'SHLVL', '_', 'PWD', 'OLDPWD', 'PS1', 'PS2', 'ENV', 'SHLVL', '_', 'PWD', 'OLDPWD', 'PS1', 'PS2', 'ENV',
# Environment modules v4 # Environment modules v4
@ -706,12 +706,12 @@ def from_sourcing_file(filename, *arguments, **kwargs):
# Compute the environments before and after sourcing # Compute the environments before and after sourcing
before = sanitize( before = sanitize(
environment_after_sourcing_files(os.devnull, **kwargs), environment_after_sourcing_files(os.devnull, **kwargs),
blacklist=blacklist, whitelist=whitelist exclude=exclude, include=include
) )
file_and_args = (filename,) + arguments file_and_args = (filename,) + arguments
after = sanitize( after = sanitize(
environment_after_sourcing_files(file_and_args, **kwargs), environment_after_sourcing_files(file_and_args, **kwargs),
blacklist=blacklist, whitelist=whitelist exclude=exclude, include=include
) )
# Delegate to the other factory # Delegate to the other factory
@ -881,22 +881,6 @@ def validate(env, errstream):
set_or_unset_not_first(variable, list_of_changes, errstream) set_or_unset_not_first(variable, list_of_changes, errstream)
def filter_environment_blacklist(env, variables):
"""Generator that filters out any change to environment variables present in
the input list.
Args:
env: list of environment modifications
variables: list of variable names to be filtered
Returns:
items in env if they are not in variables
"""
for item in env:
if item.name not in variables:
yield item
def inspect_path(root, inspections, exclude=None): def inspect_path(root, inspections, exclude=None):
"""Inspects ``root`` to search for the subdirectories in ``inspections``. """Inspects ``root`` to search for the subdirectories in ``inspections``.
Adds every path found to a list of prepend-path commands and returns it. Adds every path found to a list of prepend-path commands and returns it.
@ -1060,17 +1044,15 @@ def _source_single_file(file_and_args, environment):
return current_environment return current_environment
def sanitize(environment, blacklist, whitelist): def sanitize(environment, exclude, include):
"""Returns a copy of the input dictionary where all the keys that """Returns a copy of the input dictionary where all the keys that
match a blacklist pattern and don't match a whitelist pattern are match an excluded pattern and don't match an included pattern are
removed. removed.
Args: Args:
environment (dict): input dictionary environment (dict): input dictionary
blacklist (list): literals or regex patterns to be exclude (list): literals or regex patterns to be excluded
blacklisted include (list): literals or regex patterns to be included
whitelist (list): literals or regex patterns to be
whitelisted
""" """
def set_intersection(fullset, *args): def set_intersection(fullset, *args):
@ -1088,9 +1070,9 @@ def set_intersection(fullset, *args):
# Don't modify input, make a copy instead # Don't modify input, make a copy instead
environment = sjson.decode_json_dict(dict(environment)) environment = sjson.decode_json_dict(dict(environment))
# Retain (whitelist) has priority over prune (blacklist) # include supersedes any excluded items
prune = set_intersection(set(environment), *blacklist) prune = set_intersection(set(environment), *exclude)
prune -= set_intersection(prune, *whitelist) prune -= set_intersection(prune, *include)
for k in prune: for k in prune:
environment.pop(k, None) environment.pop(k, None)