diff --git a/easybuild/framework/easyconfig/easyconfig.py b/easybuild/framework/easyconfig/easyconfig.py index 560efb8a41..4665a1d1e8 100644 --- a/easybuild/framework/easyconfig/easyconfig.py +++ b/easybuild/framework/easyconfig/easyconfig.py @@ -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) @@ -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""" @@ -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 @@ -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): diff --git a/test/framework/easyconfig.py b/test/framework/easyconfig.py index db71c77153..f8e11e30c8 100644 --- a/test/framework/easyconfig.py +++ b/test/framework/easyconfig.py @@ -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') @@ -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') @@ -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 = [ @@ -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 = [ @@ -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' @@ -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'",