Skip to content
Merged
75 changes: 55 additions & 20 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,10 +442,11 @@ def __init__(self, path, extra_options=None, build_specs=None, validate=True, hi
:param local_var_naming_check: mode to use when checking if local variables use the recommended naming scheme
"""
self.template_values = None
# a boolean to control templating, can be (temporarily) disabled in easyblocks
self.enable_templating = True
# boolean to control whether all template values must be resolvable, can be (temporarily) disabled in easyblocks
self.expect_resolved_template_values = True
# a boolean to control templating, can be (temporarily) disabled via disable_templating context manager
self._templating_enabled = True
# boolean to control whether all template values must be resolvable on access,
# can be (temporarily) disabled via allow_unresolved_templates context manager
self._expect_resolved_template_values = True

self.log = fancylogger.getLogger(self.__class__.__name__, fname=False)

Expand Down Expand Up @@ -565,12 +566,48 @@ def disable_templating(self):
# Do what you want without templating
# Templating set to previous value
"""
old_enable_templating = self.enable_templating
self.enable_templating = False
old_templating_enabled = self._templating_enabled
self._templating_enabled = False
try:
yield old_enable_templating
yield old_templating_enabled
finally:
self.enable_templating = old_enable_templating
self._templating_enabled = old_templating_enabled

@property
def templating_enabled(self):
"""Check whether templating is enabled on this EasyConfig"""
return self._templating_enabled

def _enable_templating(self, *_):
self.log.nosupport("self.enable_templating is replaced by self.templating_enabled. "
"To disable it use the self.disable_templating context manager", '5.0')
enable_templating = property(_enable_templating, _enable_templating)

@contextmanager
def allow_unresolved_templates(self):
"""Temporarily allow templates to be not (fully) resolved.

This should only be used when it is intended to use partially resolved templates.
Otherwise `ec.get(key, resolve=False)` should be used.
See also @ref disable_templating.

Usage:
with ec.allow_unresolved_templates():
value = ec.get('key') # This will not raise an error
print(value % {'extra_key': exta_value})
# Resolving is enforced again if it was before
"""
old_expect_resolved_template_values = self._expect_resolved_template_values
self._expect_resolved_template_values = False
try:
yield old_expect_resolved_template_values
finally:
self._expect_resolved_template_values = old_expect_resolved_template_values

@property
def expect_resolved_template_values(self):
"""Check whether resolving all template values on access is enforced."""
return self._expect_resolved_template_values

def __str__(self):
"""Return a string representation of this EasyConfig instance"""
Expand Down Expand Up @@ -1839,13 +1876,12 @@ def __contains__(self, key):
@handle_deprecated_or_replaced_easyconfig_parameters
def __getitem__(self, key):
"""Return value of specified easyconfig parameter (without help text, etc.)"""
value = None
if key in self._config:
try:
value = self._config[key][0]
else:
except KeyError:
raise EasyBuildError("Use of unknown easyconfig parameter '%s' when getting parameter value", key)

if self.enable_templating:
if self.templating_enabled:
value = self.resolve_template(value)

return value
Expand Down Expand Up @@ -1923,14 +1959,13 @@ def asdict(self):
Return dict representation of this EasyConfig instance.
"""
res = {}
for key, tup in self._config.items():
value = tup[0]
if self.enable_templating:
if not self.template_values:
self.generate_template_values()
# Not all values can be resolved, e.g. %(installdir)s
value = resolve_template(value, self.template_values, expect_resolved=False)
res[key] = value
# Not all values can be resolved, e.g. %(installdir)s
with self.allow_unresolved_templates():
for key, tup in self._config.items():
value = tup[0]
if self.templating_enabled:
value = self.resolve_template(value)
res[key] = value
return res

def get_cuda_cc_template_value(self, key):
Expand Down
36 changes: 20 additions & 16 deletions test/framework/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -1215,9 +1215,11 @@ def test_templating_constants(self):
ec.validate()

# temporarily disable templating, just so we can check later whether it's *still* disabled
self.assertTrue(ec.templating_enabled)
with ec.disable_templating():
ec.generate_template_values()
self.assertFalse(ec.enable_templating)
self.assertFalse(ec.templating_enabled)
self.assertTrue(ec.templating_enabled)

self.assertEqual(ec['description'], "test easyconfig PI")
self.assertEqual(ec['sources'][0], 'PI-3.04.tar.gz')
Expand Down Expand Up @@ -1316,12 +1318,14 @@ def test_ec_method_resolve_template(self):
self.assertErrorRegex(EasyBuildError, error_pattern, ec.resolve_template, val)
self.assertErrorRegex(EasyBuildError, error_pattern, ec.get, 'installopts')

# this can be (temporarily) disabled via expect_resolved_template_values in EasyConfig instance
ec.expect_resolved_template_values = False
self.assertEqual(ec.resolve_template(val), val)
self.assertEqual(ec['installopts'], val)
# this can be (temporarily) disabled
with ec.allow_unresolved_templates():
self.assertFalse(ec.expect_resolved_template_values)
self.assertEqual(ec.resolve_template(val), val)
self.assertEqual(ec['installopts'], val)

ec.expect_resolved_template_values = True
# Enforced again
self.assertTrue(ec.expect_resolved_template_values)
self.assertErrorRegex(EasyBuildError, error_pattern, ec.resolve_template, val)
self.assertErrorRegex(EasyBuildError, error_pattern, ec.get, 'installopts')

Expand Down Expand Up @@ -2542,11 +2546,11 @@ def test_dump(self):
test_ec = os.path.join(self.test_prefix, 'test.eb')

ec = EasyConfig(os.path.join(test_ecs_dir, ecfile))
ec.enable_templating = False
ecdict = ec.asdict()
ec.dump(test_ec)
# dict representation of EasyConfig instance should not change after dump
self.assertEqual(ecdict, ec.asdict())
with ec.disable_templating():
ecdict = ec.asdict()
ec.dump(test_ec)
# dict representation of EasyConfig instance should not change after dump
self.assertEqual(ecdict, ec.asdict())
ectxt = read_file(test_ec)

patterns = [
Expand All @@ -2561,7 +2565,6 @@ def test_dump(self):

# parse result again
dumped_ec = EasyConfig(test_ec)
dumped_ec.enable_templating = False

# check that selected parameters still have the same value
params = [
Expand All @@ -2570,9 +2573,10 @@ def test_dump(self):
'dependencies', # checking this is important w.r.t. filtered hidden dependencies being restored in dump
'exts_list', # exts_lists (in Python easyconfig) use another layer of templating so shouldn't change
]
for param in params:
if param in ec:
self.assertEqual(ec[param], dumped_ec[param])
with ec.disable_templating(), dumped_ec.disable_templating():
for param in params:
if param in ec:
self.assertEqual(ec[param], dumped_ec[param])

ec_txt = textwrap.dedent("""
easyblock = 'EB_toy'
Expand Down Expand Up @@ -2823,7 +2827,7 @@ def test_dump_template(self):
ec.dump(testec)
ectxt = read_file(testec)

self.assertTrue(ec.enable_templating) # templating should still be enabled after calling dump()
self.assertTrue(ec.templating_enabled) # templating should still be enabled after calling dump()

patterns = [
r"easyblock = 'EB_foo'",
Expand Down
Loading