Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add context manager for allowing unresolved templates and make the state members private #4735

Open
wants to merge 7 commits into
base: 5.0.x
Choose a base branch
from
76 changes: 55 additions & 21 deletions easybuild/framework/easyconfig/easyconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,10 @@ 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
self._templating_enabled = True
# boolean to control whether all template values must be resolvable on access, can be (temporarily) disabled
self._expect_resolved_template_values = True

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

Expand Down Expand Up @@ -545,12 +545,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 @@ -1776,7 +1812,7 @@ def resolve_template(self, value):
"""Resolve all templates in the given value using this easyconfig"""
if not self.template_values:
self.generate_template_values()
return resolve_template(value, self.template_values, expect_resolved=self.expect_resolved_template_values)
return resolve_template(value, self.template_values, expect_resolved=self._expect_resolved_template_values)

@handle_deprecated_or_replaced_easyconfig_parameters
def __contains__(self, key):
Expand All @@ -1786,13 +1822,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 @@ -1870,14 +1905,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 @@ -1213,9 +1213,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 @@ -1314,12 +1316,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 @@ -2541,11 +2545,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 @@ -2560,7 +2564,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 @@ -2569,9 +2572,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 @@ -2822,7 +2826,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