Skip to content

Commit cf9fd79

Browse files
authored
Merge pull request #3287 from Flamefire/less_templating
Introduce contextmanager for disabling templating and reduce resolving errors
2 parents 4b29b70 + 91c7f0d commit cf9fd79

File tree

5 files changed

+125
-129
lines changed

5 files changed

+125
-129
lines changed

easybuild/framework/easyblock.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,7 +1194,8 @@ def make_module_extra_extensions(self):
11941194
lines = [self.module_extra_extensions]
11951195

11961196
# set environment variable that specifies list of extensions
1197-
exts_list = ','.join(['%s-%s' % (ext[0], ext[1]) for ext in self.cfg['exts_list']])
1197+
# We need only name and version, so don't resolve templates
1198+
exts_list = ','.join(['-'.join(ext[:2]) for ext in self.cfg.get_ref('exts_list')])
11981199
env_var_name = convert_name(self.name, upper=True)
11991200
lines.append(self.module_generator.set_environment('EBEXTSLIST%s' % env_var_name, exts_list))
12001201

@@ -1207,7 +1208,7 @@ def make_module_footer(self):
12071208
footer = [self.module_generator.comment("Built with EasyBuild version %s" % VERBOSE_VERSION)]
12081209

12091210
# add extra stuff for extensions (if any)
1210-
if self.cfg['exts_list']:
1211+
if self.cfg.get_ref('exts_list'):
12111212
footer.append(self.make_module_extra_extensions())
12121213

12131214
# include modules footer if one is specified
@@ -1791,7 +1792,7 @@ def fetch_step(self, skip_checksums=False):
17911792
trace_msg(msg)
17921793

17931794
# fetch extensions
1794-
if self.cfg['exts_list']:
1795+
if self.cfg.get_ref('exts_list'):
17951796
self.exts = self.fetch_extension_sources(skip_checksums=skip_checksums)
17961797

17971798
# create parent dirs in install and modules path already
@@ -2065,7 +2066,7 @@ def extensions_step(self, fetch=False):
20652066
- find source for extensions, in 'extensions' (and 'packages' for legacy reasons)
20662067
- run extra_extensions
20672068
"""
2068-
if len(self.cfg['exts_list']) == 0:
2069+
if not self.cfg.get_ref('exts_list'):
20692070
self.log.debug("No extensions in exts_list")
20702071
return
20712072

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 113 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import os
4646
import re
4747
from distutils.version import LooseVersion
48+
from contextlib import contextmanager
4849

4950
import easybuild.tools.filetools as filetools
5051
from easybuild.base import fancylogger
@@ -383,6 +384,23 @@ def get_toolchain_hierarchy(parent_toolchain, incl_capabilities=False):
383384
return toolchain_hierarchy
384385

385386

387+
@contextmanager
388+
def disable_templating(ec):
389+
"""Temporarily disable templating on the given EasyConfig
390+
391+
Usage:
392+
with disable_templating(ec):
393+
# Do what you want without templating
394+
# Templating set to previous value
395+
"""
396+
old_enable_templating = ec.enable_templating
397+
ec.enable_templating = False
398+
try:
399+
yield old_enable_templating
400+
finally:
401+
ec.enable_templating = old_enable_templating
402+
403+
386404
class EasyConfig(object):
387405
"""
388406
Class which handles loading, reading, validation of easyconfigs
@@ -592,18 +610,15 @@ def set_keys(self, params):
592610
"""
593611
# disable templating when setting easyconfig parameters
594612
# required to avoid problems with values that need more parsing to be done (e.g. dependencies)
595-
prev_enable_templating = self.enable_templating
596-
self.enable_templating = False
597-
598-
for key in sorted(params.keys()):
599-
# validations are skipped, just set in the config
600-
if key in self._config.keys():
601-
self[key] = params[key]
602-
self.log.info("setting easyconfig parameter %s: value %s (type: %s)", key, self[key], type(self[key]))
603-
else:
604-
raise EasyBuildError("Unknown easyconfig parameter: %s (value '%s')", key, params[key])
605-
606-
self.enable_templating = prev_enable_templating
613+
with disable_templating(self):
614+
for key in sorted(params.keys()):
615+
# validations are skipped, just set in the config
616+
if key in self._config.keys():
617+
self[key] = params[key]
618+
self.log.info("setting easyconfig parameter %s: value %s (type: %s)",
619+
key, self[key], type(self[key]))
620+
else:
621+
raise EasyBuildError("Unknown easyconfig parameter: %s (value '%s')", key, params[key])
607622

608623
def parse(self):
609624
"""
@@ -647,42 +662,39 @@ def parse(self):
647662

648663
# templating is disabled when parse_hook is called to allow for easy updating of mutable easyconfig parameters
649664
# (see also comment in resolve_template)
650-
prev_enable_templating = self.enable_templating
651-
self.enable_templating = False
652-
653-
# if any lists of dependency versions are specified over which we should iterate,
654-
# deal with them now, before calling parse hook, parsing of dependencies & iterative easyconfig parameters...
655-
self.handle_multi_deps()
656-
657-
parse_hook_msg = None
658-
if self.path:
659-
parse_hook_msg = "Running %s hook for %s..." % (PARSE, os.path.basename(self.path))
660-
661-
# trigger parse hook
662-
hooks = load_hooks(build_option('hooks'))
663-
run_hook(PARSE, hooks, args=[self], msg=parse_hook_msg)
664-
665-
# parse dependency specifications
666-
# it's important that templating is still disabled at this stage!
667-
self.log.info("Parsing dependency specifications...")
668-
self['dependencies'] = [self._parse_dependency(dep) for dep in self['dependencies']]
669-
self['hiddendependencies'] = [self._parse_dependency(dep, hidden=True) for dep in self['hiddendependencies']]
670-
671-
# need to take into account that builddependencies may need to be iterated over,
672-
# i.e. when the value is a list of lists of tuples
673-
builddeps = self['builddependencies']
674-
if builddeps and all(isinstance(x, (list, tuple)) for b in builddeps for x in b):
675-
self.iterate_options.append('builddependencies')
676-
builddeps = [[self._parse_dependency(dep, build_only=True) for dep in x] for x in builddeps]
677-
else:
678-
builddeps = [self._parse_dependency(dep, build_only=True) for dep in builddeps]
679-
self['builddependencies'] = builddeps
665+
with disable_templating(self):
666+
# if any lists of dependency versions are specified over which we should iterate,
667+
# deal with them now, before calling parse hook, parsing of dependencies & iterative easyconfig parameters
668+
self.handle_multi_deps()
680669

681-
# keep track of parsed multi deps, they'll come in handy during sanity check & module steps...
682-
self.multi_deps = self.get_parsed_multi_deps()
670+
parse_hook_msg = None
671+
if self.path:
672+
parse_hook_msg = "Running %s hook for %s..." % (PARSE, os.path.basename(self.path))
673+
674+
# trigger parse hook
675+
hooks = load_hooks(build_option('hooks'))
676+
run_hook(PARSE, hooks, args=[self], msg=parse_hook_msg)
677+
678+
# parse dependency specifications
679+
# it's important that templating is still disabled at this stage!
680+
self.log.info("Parsing dependency specifications...")
681+
self['dependencies'] = [self._parse_dependency(dep) for dep in self['dependencies']]
682+
self['hiddendependencies'] = [
683+
self._parse_dependency(dep, hidden=True) for dep in self['hiddendependencies']
684+
]
685+
686+
# need to take into account that builddependencies may need to be iterated over,
687+
# i.e. when the value is a list of lists of tuples
688+
builddeps = self['builddependencies']
689+
if builddeps and all(isinstance(x, (list, tuple)) for b in builddeps for x in b):
690+
self.iterate_options.append('builddependencies')
691+
builddeps = [[self._parse_dependency(dep, build_only=True) for dep in x] for x in builddeps]
692+
else:
693+
builddeps = [self._parse_dependency(dep, build_only=True) for dep in builddeps]
694+
self['builddependencies'] = builddeps
683695

684-
# restore templating
685-
self.enable_templating = prev_enable_templating
696+
# keep track of parsed multi deps, they'll come in handy during sanity check & module steps...
697+
self.multi_deps = self.get_parsed_multi_deps()
686698

687699
# update templating dictionary
688700
self.generate_template_values()
@@ -1108,63 +1120,57 @@ def dump(self, fp, always_overwrite=True, backup=False, explicit_toolchains=Fals
11081120
:param always_overwrite: overwrite existing file at specified location without use of --force
11091121
:param backup: create backup of existing file before overwriting it
11101122
"""
1111-
orig_enable_templating = self.enable_templating
1112-
11131123
# templated values should be dumped unresolved
1114-
self.enable_templating = False
1115-
1116-
# build dict of default values
1117-
default_values = dict([(key, DEFAULT_CONFIG[key][0]) for key in DEFAULT_CONFIG])
1118-
default_values.update(dict([(key, self.extra_options[key][0]) for key in self.extra_options]))
1124+
with disable_templating(self):
1125+
# build dict of default values
1126+
default_values = dict([(key, DEFAULT_CONFIG[key][0]) for key in DEFAULT_CONFIG])
1127+
default_values.update(dict([(key, self.extra_options[key][0]) for key in self.extra_options]))
1128+
1129+
self.generate_template_values()
1130+
templ_const = dict([(quote_py_str(const[1]), const[0]) for const in TEMPLATE_CONSTANTS])
1131+
1132+
# create reverse map of templates, to inject template values where possible
1133+
# longer template values are considered first, shorter template keys get preference over longer ones
1134+
sorted_keys = sorted(self.template_values, key=lambda k: (len(self.template_values[k]), -len(k)),
1135+
reverse=True)
1136+
templ_val = OrderedDict([])
1137+
for key in sorted_keys:
1138+
# shortest template 'key' is retained in case of duplicates
1139+
# ('namelower' is preferred over 'github_account')
1140+
# only template values longer than 2 characters are retained
1141+
if self.template_values[key] not in templ_val and len(self.template_values[key]) > 2:
1142+
templ_val[self.template_values[key]] = key
1143+
1144+
toolchain_hierarchy = None
1145+
if not explicit_toolchains:
1146+
try:
1147+
toolchain_hierarchy = get_toolchain_hierarchy(self['toolchain'])
1148+
except EasyBuildError as err:
1149+
# don't fail hard just because we can't get the hierarchy
1150+
self.log.warning('Could not generate toolchain hierarchy for %s to use in easyconfig dump method, '
1151+
'error:\n%s', self['toolchain'], str(err))
11191152

1120-
self.generate_template_values()
1121-
templ_const = dict([(quote_py_str(const[1]), const[0]) for const in TEMPLATE_CONSTANTS])
1122-
1123-
# create reverse map of templates, to inject template values where possible
1124-
# longer template values are considered first, shorter template keys get preference over longer ones
1125-
sorted_keys = sorted(self.template_values, key=lambda k: (len(self.template_values[k]), -len(k)), reverse=True)
1126-
templ_val = OrderedDict([])
1127-
for key in sorted_keys:
1128-
# shortest template 'key' is retained in case of duplicates ('namelower' is preferred over 'github_account')
1129-
# only template values longer than 2 characters are retained
1130-
if self.template_values[key] not in templ_val and len(self.template_values[key]) > 2:
1131-
templ_val[self.template_values[key]] = key
1132-
1133-
toolchain_hierarchy = None
1134-
if not explicit_toolchains:
11351153
try:
1136-
toolchain_hierarchy = get_toolchain_hierarchy(self['toolchain'])
1137-
except EasyBuildError as err:
1138-
# don't fail hard just because we can't get the hierarchy
1139-
self.log.warning('Could not generate toolchain hierarchy for %s to use in easyconfig dump method, '
1140-
'error:\n%s', self['toolchain'], str(err))
1154+
ectxt = self.parser.dump(self, default_values, templ_const, templ_val,
1155+
toolchain_hierarchy=toolchain_hierarchy)
1156+
except NotImplementedError as err:
1157+
raise NotImplementedError(err)
11411158

1142-
try:
1143-
ectxt = self.parser.dump(self, default_values, templ_const, templ_val,
1144-
toolchain_hierarchy=toolchain_hierarchy)
1145-
except NotImplementedError as err:
1146-
# need to restore enable_templating value in case this method is caught in a try/except block and ignored
1147-
# (the ability to dump is not a hard requirement for build success)
1148-
self.enable_templating = orig_enable_templating
1149-
raise NotImplementedError(err)
1159+
self.log.debug("Dumped easyconfig: %s", ectxt)
11501160

1151-
self.log.debug("Dumped easyconfig: %s", ectxt)
1161+
if build_option('dump_autopep8'):
1162+
autopep8_opts = {
1163+
'aggressive': 1, # enable non-whitespace changes, but don't be too aggressive
1164+
'max_line_length': 120,
1165+
}
1166+
self.log.info("Reformatting dumped easyconfig using autopep8 (options: %s)", autopep8_opts)
1167+
ectxt = autopep8.fix_code(ectxt, options=autopep8_opts)
1168+
self.log.debug("Dumped easyconfig after autopep8 reformatting: %s", ectxt)
11521169

1153-
if build_option('dump_autopep8'):
1154-
autopep8_opts = {
1155-
'aggressive': 1, # enable non-whitespace changes, but don't be too aggressive
1156-
'max_line_length': 120,
1157-
}
1158-
self.log.info("Reformatting dumped easyconfig using autopep8 (options: %s)", autopep8_opts)
1159-
ectxt = autopep8.fix_code(ectxt, options=autopep8_opts)
1160-
self.log.debug("Dumped easyconfig after autopep8 reformatting: %s", ectxt)
1170+
if not ectxt.endswith('\n'):
1171+
ectxt += '\n'
11611172

1162-
if not ectxt.endswith('\n'):
1163-
ectxt += '\n'
1164-
1165-
write_file(fp, ectxt, always_overwrite=always_overwrite, backup=backup, verbose=backup)
1166-
1167-
self.enable_templating = orig_enable_templating
1173+
write_file(fp, ectxt, always_overwrite=always_overwrite, backup=backup, verbose=backup)
11681174

11691175
def _validate(self, attr, values): # private method
11701176
"""
@@ -1473,7 +1479,7 @@ def _parse_dependency(self, dep, hidden=False, build_only=False):
14731479

14741480
# (true) boolean value simply indicates that a system toolchain is used
14751481
elif isinstance(tc_spec, bool) and tc_spec:
1476-
tc = {'name': SYSTEM_TOOLCHAIN_NAME, 'version': ''}
1482+
tc = {'name': SYSTEM_TOOLCHAIN_NAME, 'version': ''}
14771483

14781484
# two-element list/tuple value indicates custom toolchain specification
14791485
elif isinstance(tc_spec, (list, tuple,)):
@@ -1593,27 +1599,21 @@ def _generate_template_values(self, ignore=None):
15931599

15941600
# step 1-3 work with easyconfig.templates constants
15951601
# disable templating with creating dict with template values to avoid looping back to here via __getitem__
1596-
prev_enable_templating = self.enable_templating
1597-
1598-
self.enable_templating = False
1599-
1600-
if self.template_values is None:
1601-
# if no template values are set yet, initiate with a minimal set of template values;
1602-
# this is important for easyconfig that use %(version_minor)s to define 'toolchain',
1603-
# which is a pretty weird use case, but fine...
1604-
self.template_values = template_constant_dict(self, ignore=ignore)
1605-
1606-
self.enable_templating = prev_enable_templating
1602+
with disable_templating(self):
1603+
if self.template_values is None:
1604+
# if no template values are set yet, initiate with a minimal set of template values;
1605+
# this is important for easyconfig that use %(version_minor)s to define 'toolchain',
1606+
# which is a pretty weird use case, but fine...
1607+
self.template_values = template_constant_dict(self, ignore=ignore)
16071608

16081609
# grab toolchain instance with templating support enabled,
16091610
# which is important in case the Toolchain instance was not created yet
16101611
toolchain = self.toolchain
16111612

16121613
# get updated set of template values, now with toolchain instance
16131614
# (which is used to define the %(mpi_cmd_prefix)s template)
1614-
self.enable_templating = False
1615-
template_values = template_constant_dict(self, ignore=ignore, toolchain=toolchain)
1616-
self.enable_templating = prev_enable_templating
1615+
with disable_templating(self):
1616+
template_values = template_constant_dict(self, ignore=ignore, toolchain=toolchain)
16171617

16181618
# update the template_values dict
16191619
self.template_values.update(template_values)
@@ -1656,13 +1656,8 @@ def get_ref(self, key):
16561656
# see also comments in resolve_template
16571657

16581658
# temporarily disable templating
1659-
prev_enable_templating = self.enable_templating
1660-
self.enable_templating = False
1661-
1662-
ref = self[key]
1663-
1664-
# restore previous value for 'enable_templating'
1665-
self.enable_templating = prev_enable_templating
1659+
with disable_templating(self):
1660+
ref = self[key]
16661661

16671662
return ref
16681663

easybuild/framework/extension.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ def __init__(self, mself, ext, extra_params=None):
127127
# make sure they are merged into self.cfg so they can be queried;
128128
# unknown easyconfig parameters are ignored since self.options may include keys only there for extensions;
129129
# this allows to specify custom easyconfig parameters on a per-extension basis
130-
for key in self.options:
130+
for key, value in self.options.items():
131131
if key in self.cfg:
132-
self.cfg[key] = resolve_template(self.options[key], self.cfg.template_values)
132+
self.cfg[key] = value
133133
self.log.debug("Customising known easyconfig parameter '%s' for extension %s/%s: %s",
134-
key, name, version, self.cfg[key])
134+
key, name, version, value)
135135
else:
136136
self.log.debug("Skipping unknown custom easyconfig parameter '%s' for extension %s/%s: %s",
137-
key, name, version, self.options[key])
137+
key, name, version, value)
138138

139139
self.sanity_check_fail_msgs = []
140140

easybuild/tools/module_generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,8 @@ def _generate_extension_list(self):
524524
"""
525525
Generate a string with a comma-separated list of extensions.
526526
"""
527-
exts_list = self.app.cfg['exts_list']
527+
# We need only name and version, so don't resolve templates
528+
exts_list = self.app.cfg.get_ref('exts_list')
528529
extensions = ', '.join(sorted(['-'.join(ext[:2]) for ext in exts_list], key=str.lower))
529530

530531
return extensions

test/framework/tweak.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,7 @@ def test_map_easyconfig_to_target_tc_hierarchy(self):
471471
update_build_specs={'version': new_version},
472472
update_dep_versions=False)
473473
tweaked_ec = process_easyconfig(tweaked_spec)[0]
474-
tweaked_dict = tweaked_ec['ec'].asdict()
475-
extensions = tweaked_dict['exts_list']
474+
extensions = tweaked_ec['ec']['exts_list']
476475
# check one extension with the same name exists and that the version has been updated
477476
hit_extension = 0
478477
for extension in extensions:

0 commit comments

Comments
 (0)