Skip to content

Commit c81572c

Browse files
authored
Merge pull request #1 from lexming/granular_exit_code
Add lexming changes about how to manage exit codes
2 parents e9f201a + 0d31d34 commit c81572c

36 files changed

+1263
-413
lines changed

.github/workflows/linting.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ jobs:
1414
strategy:
1515
matrix:
1616
python-version: [3.6, 3.7, 3.8, 3.9, '3.10', '3.11', '3.12']
17-
1817
steps:
1918
- uses: actions/checkout@v3
2019

easybuild/base/optcomplete.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
from optparse import OptionParser, Option
108108
from pprint import pformat
109109

110+
from easybuild.tools.filetools import get_cwd
110111
from easybuild.tools.utilities import shell_quote
111112

112113
debugfn = None # for debugging only
@@ -537,7 +538,7 @@ def autocomplete(parser, arg_completer=None, opt_completer=None, subcmd_complete
537538
# Note: this will get filtered properly below.
538539

539540
completer_kwargs = {
540-
'pwd': os.getcwd(),
541+
'pwd': get_cwd(),
541542
'cline': cline,
542543
'cpoint': cpoint,
543544
'prefix': prefix,

easybuild/framework/easyblock.py

Lines changed: 67 additions & 64 deletions
Large diffs are not rendered by default.

easybuild/framework/easyconfig/easyconfig.py

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@
5959
from easybuild.framework.easyconfig.format.format import DEPENDENCY_PARAMETERS
6060
from easybuild.framework.easyconfig.format.one import EB_FORMAT_EXTENSION, retrieve_blocks_in_spec
6161
from easybuild.framework.easyconfig.licenses import EASYCONFIG_LICENSES_DICT
62-
from easybuild.framework.easyconfig.parser import DEPRECATED_PARAMETERS, REPLACED_PARAMETERS
62+
from easybuild.framework.easyconfig.parser import ALTERNATE_PARAMETERS, DEPRECATED_PARAMETERS, REPLACED_PARAMETERS
6363
from easybuild.framework.easyconfig.parser import EasyConfigParser, fetch_parameters_from_easyconfig
64-
from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS, TEMPLATE_NAMES_DYNAMIC, template_constant_dict
64+
from easybuild.framework.easyconfig.templates import ALTERNATE_TEMPLATES, DEPRECATED_TEMPLATES, TEMPLATE_CONSTANTS
65+
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_DYNAMIC, template_constant_dict
6566
from easybuild.tools import LooseVersion
66-
from easybuild.tools.build_log import EasyBuildError, print_warning, print_msg
67+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_warning, print_msg
6768
from easybuild.tools.config import GENERIC_EASYBLOCK_PKG, LOCAL_VAR_NAMING_CHECK_ERROR, LOCAL_VAR_NAMING_CHECK_LOG
6869
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN
6970
from easybuild.tools.config import Singleton, build_option, get_module_naming_scheme
@@ -118,11 +119,13 @@ def handle_deprecated_or_replaced_easyconfig_parameters(ec_method):
118119
def new_ec_method(self, key, *args, **kwargs):
119120
"""Check whether any replace easyconfig parameters are still used"""
120121
# map deprecated parameters to their replacements, issue deprecation warning(/error)
121-
if key in DEPRECATED_PARAMETERS:
122+
if key in ALTERNATE_PARAMETERS:
123+
key = ALTERNATE_PARAMETERS[key]
124+
elif key in DEPRECATED_PARAMETERS:
122125
depr_key = key
123126
key, ver = DEPRECATED_PARAMETERS[depr_key]
124127
_log.deprecated("Easyconfig parameter '%s' is deprecated, use '%s' instead" % (depr_key, key), ver)
125-
if key in REPLACED_PARAMETERS:
128+
elif key in REPLACED_PARAMETERS:
126129
_log.nosupport("Easyconfig parameter '%s' is replaced by '%s'" % (key, REPLACED_PARAMETERS[key]), '2.0')
127130
return ec_method(self, key, *args, **kwargs)
128131

@@ -179,7 +182,7 @@ def triage_easyconfig_params(variables, ec):
179182

180183
for key in variables:
181184
# validations are skipped, just set in the config
182-
if key in ec or key in DEPRECATED_PARAMETERS.keys():
185+
if any(key in d for d in (ec, DEPRECATED_PARAMETERS.keys(), ALTERNATE_PARAMETERS.keys())):
183186
ec_params[key] = variables[key]
184187
_log.debug("setting config option %s: value %s (type: %s)", key, ec_params[key], type(ec_params[key]))
185188
elif key in REPLACED_PARAMETERS:
@@ -658,7 +661,7 @@ def set_keys(self, params):
658661
with self.disable_templating():
659662
for key in sorted(params.keys()):
660663
# validations are skipped, just set in the config
661-
if key in self._config.keys() or key in DEPRECATED_PARAMETERS.keys():
664+
if any(key in x.keys() for x in (self._config, ALTERNATE_PARAMETERS, DEPRECATED_PARAMETERS)):
662665
self[key] = params[key]
663666
self.log.info("setting easyconfig parameter %s: value %s (type: %s)",
664667
key, self[key], type(self[key]))
@@ -827,7 +830,7 @@ def check_deprecated(self, path):
827830
if depr_msgs:
828831
depr_msg = ', '.join(depr_msgs)
829832

830-
depr_maj_ver = int(str(VERSION).split('.')[0]) + 1
833+
depr_maj_ver = int(str(VERSION).split('.', maxsplit=1)[0]) + 1
831834
depr_ver = '%s.0' % depr_maj_ver
832835

833836
more_info_depr_ec = " (see also https://docs.easybuild.io/deprecated-easyconfigs)"
@@ -842,8 +845,8 @@ def validate(self, check_osdeps=True):
842845
- check license
843846
"""
844847
self.log.info("Validating easyconfig")
845-
for attr in self.validations:
846-
self._validate(attr, self.validations[attr])
848+
for attr, valid_values in self.validations.items():
849+
self._validate(attr, valid_values)
847850

848851
if check_osdeps:
849852
self.log.info("Checking OS dependencies")
@@ -899,9 +902,12 @@ def validate_os_deps(self):
899902
not_found.append(dep)
900903

901904
if not_found:
902-
raise EasyBuildError("One or more OS dependencies were not found: %s", not_found, exit_code=8)
903-
else:
904-
self.log.info("OS dependencies ok: %s" % self['osdependencies'])
905+
raise EasyBuildError(
906+
"One or more OS dependencies were not found: %s", not_found,
907+
exit_code=EasyBuildExit.MISS_SYSTEM_DEPENDENCY
908+
)
909+
910+
self.log.info("OS dependencies ok: %s" % self['osdependencies'])
905911

906912
return True
907913

@@ -1207,8 +1213,8 @@ def dump(self, fp, always_overwrite=True, backup=False, explicit_toolchains=Fals
12071213
# templated values should be dumped unresolved
12081214
with self.disable_templating():
12091215
# build dict of default values
1210-
default_values = {key: DEFAULT_CONFIG[key][0] for key in DEFAULT_CONFIG}
1211-
default_values.update({key: self.extra_options[key][0] for key in self.extra_options})
1216+
default_values = {key: value[0] for key, value in DEFAULT_CONFIG.items()}
1217+
default_values.update({key: value[0] for key, value in self.extra_options.items()})
12121218

12131219
self.generate_template_values()
12141220
templ_const = {quote_py_str(const[1]): const[0] for const in TEMPLATE_CONSTANTS}
@@ -1264,7 +1270,10 @@ def _validate(self, attr, values): # private method
12641270
if values is None:
12651271
values = []
12661272
if self[attr] and self[attr] not in values:
1267-
raise EasyBuildError("%s provided '%s' is not valid: %s", attr, self[attr], values, exit_code=12)
1273+
raise EasyBuildError(
1274+
"%s provided '%s' is not valid: %s", attr, self[attr], values,
1275+
exit_code=EasyBuildExit.SYNTAX_ERROR
1276+
)
12681277

12691278
def probe_external_module_metadata(self, mod_name, existing_metadata=None):
12701279
"""
@@ -1911,14 +1920,17 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
19111920
modname = modulepath.replace('easybuild.easyblocks.', '')
19121921
error_re = re.compile(r"No module named '?.*/?%s'?" % modname)
19131922
_log.debug("error regexp for ImportError on '%s' easyblock: %s", modname, error_re.pattern)
1914-
if error_re.match(str(err)):
1915-
if error_on_missing_easyblock:
1916-
raise EasyBuildError(
1917-
"No software-specific easyblock '%s' found for %s", class_name, name, exit_code=4)
1918-
elif error_on_failed_import:
1919-
raise EasyBuildError("Failed to import %s easyblock: %s", class_name, err, exit_code=5)
1920-
else:
1921-
_log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
1923+
if error_re.match(str(err)) and error_on_missing_easyblock:
1924+
raise EasyBuildError(
1925+
"No software-specific easyblock '%s' found for %s", class_name, name,
1926+
exit_code=EasyBuildExit.MISS_EASYBLOCK
1927+
)
1928+
if error_on_failed_import:
1929+
raise EasyBuildError(
1930+
"Failed to import %s easyblock: %s", class_name, err,
1931+
exit_code=EasyBuildExit.EASYBLOCK_ERROR
1932+
)
1933+
_log.debug("Failed to import easyblock for %s, but ignoring it: %s" % (class_name, err))
19221934

19231935
if cls is not None:
19241936
_log.info("Successfully obtained class '%s' for easyblock '%s' (software name '%s')",
@@ -1933,7 +1945,9 @@ def get_easyblock_class(easyblock, name=None, error_on_failed_import=True, error
19331945
raise err
19341946
except Exception as err:
19351947
raise EasyBuildError(
1936-
"Failed to obtain class for %s easyblock (not available?): %s", easyblock, err, exit_code=6)
1948+
"Failed to obtain class for %s easyblock (not available?): %s", easyblock, err,
1949+
exit_code=EasyBuildExit.EASYBLOCK_ERROR
1950+
)
19371951

19381952

19391953
def get_module_path(name, generic=None, decode=True):
@@ -1991,12 +2005,41 @@ def resolve_template(value, tmpl_dict):
19912005
# '%(name)s' -> '%(name)s'
19922006
# '%%(name)s' -> '%%(name)s'
19932007
if '%' in value:
2008+
raw_value = value
19942009
value = re.sub(re.compile(r'(%)(?!%*\(\w+\)s)'), r'\1\1', value)
19952010

19962011
try:
19972012
value = value % tmpl_dict
19982013
except KeyError:
1999-
_log.warning("Unable to resolve template value %s with dict %s", value, tmpl_dict)
2014+
# check if any alternate and/or deprecated templates resolve
2015+
try:
2016+
orig_value = value
2017+
# map old templates to new values for alternate and deprecated templates
2018+
alt_map = {old_tmpl: tmpl_dict[new_tmpl] for (old_tmpl, new_tmpl) in
2019+
ALTERNATE_TEMPLATES.items() if new_tmpl in tmpl_dict.keys()}
2020+
alt_map2 = {new_tmpl: tmpl_dict[old_tmpl] for (old_tmpl, new_tmpl) in
2021+
ALTERNATE_TEMPLATES.items() if old_tmpl in tmpl_dict.keys()}
2022+
depr_map = {old_tmpl: tmpl_dict[new_tmpl] for (old_tmpl, (new_tmpl, ver)) in
2023+
DEPRECATED_TEMPLATES.items() if new_tmpl in tmpl_dict.keys()}
2024+
2025+
# try templating with alternate and deprecated templates included
2026+
value = value % {**tmpl_dict, **alt_map, **alt_map2, **depr_map}
2027+
2028+
for old_tmpl, val in depr_map.items():
2029+
# check which deprecated templates were replaced, and issue deprecation warnings
2030+
if old_tmpl in orig_value and val in value:
2031+
new_tmpl, ver = DEPRECATED_TEMPLATES[old_tmpl]
2032+
_log.deprecated(f"Easyconfig template '{old_tmpl}' is deprecated, use '{new_tmpl}' instead",
2033+
ver)
2034+
except KeyError:
2035+
_log.warning(f"Unable to resolve template value {value} with dict {tmpl_dict}")
2036+
value = raw_value # Undo "%"-escaping
2037+
2038+
for key in tmpl_dict:
2039+
if key in DEPRECATED_TEMPLATES:
2040+
new_key, ver = DEPRECATED_TEMPLATES[key]
2041+
_log.deprecated(f"Easyconfig template '{key}' is deprecated, use '{new_key}' instead", ver)
2042+
20002043
else:
20012044
# this block deals with references to objects and returns other references
20022045
# for reading this is ok, but for self['x'] = {}
@@ -2050,10 +2093,10 @@ def process_easyconfig(path, build_specs=None, validate=True, parse_only=False,
20502093
ec = EasyConfig(spec, build_specs=build_specs, validate=validate, hidden=hidden)
20512094
except EasyBuildError as err:
20522095
try:
2053-
err.exit_code
2096+
exit_code = err.exit_code
20542097
except AttributeError:
2055-
err.exit_code = 1
2056-
raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg, exit_code=err.exit_code)
2098+
exit_code = EasyBuildExit.EASYCONFIG_ERROR
2099+
raise EasyBuildError("Failed to process easyconfig %s: %s", spec, err.msg, exit_code=exit_code)
20572100

20582101
name = ec['name']
20592102

easybuild/framework/easyconfig/format/pyheaderconfigobj.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from easybuild.framework.easyconfig.constants import EASYCONFIG_CONSTANTS
4040
from easybuild.framework.easyconfig.format.format import get_format_version, EasyConfigFormat
4141
from easybuild.framework.easyconfig.licenses import EASYCONFIG_LICENSES_DICT
42+
from easybuild.framework.easyconfig.templates import ALTERNATE_TEMPLATE_CONSTANTS, DEPRECATED_TEMPLATE_CONSTANTS
4243
from easybuild.framework.easyconfig.templates import TEMPLATE_CONSTANTS
4344
from easybuild.tools.build_log import EasyBuildError
4445
from easybuild.tools.configobj import ConfigObj
@@ -86,6 +87,58 @@ def build_easyconfig_variables_dict():
8687
return vars_dict
8788

8889

90+
def handle_deprecated_constants(method):
91+
"""Decorator to handle deprecated easyconfig template constants"""
92+
def wrapper(self, key, *args, **kwargs):
93+
"""Check whether any deprecated constants are used"""
94+
alternate = ALTERNATE_TEMPLATE_CONSTANTS
95+
deprecated = DEPRECATED_TEMPLATE_CONSTANTS
96+
if key in alternate:
97+
key = alternate[key]
98+
elif key in deprecated:
99+
depr_key = key
100+
key, ver = deprecated[depr_key]
101+
_log.deprecated(f"Easyconfig template constant '{depr_key}' is deprecated, use '{key}' instead", ver)
102+
return method(self, key, *args, **kwargs)
103+
return wrapper
104+
105+
106+
class DeprecatedDict(dict):
107+
"""Custom dictionary that handles deprecated easyconfig template constants gracefully"""
108+
109+
def __init__(self, *args, **kwargs):
110+
self.clear()
111+
self.update(*args, **kwargs)
112+
113+
@handle_deprecated_constants
114+
def __contains__(self, key):
115+
return super().__contains__(key)
116+
117+
@handle_deprecated_constants
118+
def __delitem__(self, key):
119+
return super().__delitem__(key)
120+
121+
@handle_deprecated_constants
122+
def __getitem__(self, key):
123+
return super().__getitem__(key)
124+
125+
@handle_deprecated_constants
126+
def __setitem__(self, key, value):
127+
return super().__setitem__(key, value)
128+
129+
def update(self, *args, **kwargs):
130+
if args:
131+
if isinstance(args[0], dict):
132+
for key, value in args[0].items():
133+
self.__setitem__(key, value)
134+
else:
135+
for key, value in args[0]:
136+
self.__setitem__(key, value)
137+
138+
for key, value in kwargs.items():
139+
self.__setitem__(key, value)
140+
141+
89142
class EasyConfigFormatConfigObj(EasyConfigFormat):
90143
"""
91144
Extended EasyConfig format, with support for a header and sections that are actually parsed (as opposed to exec'ed).
@@ -176,7 +229,7 @@ def parse_header(self, header):
176229

177230
def parse_pyheader(self, pyheader):
178231
"""Parse the python header, assign to docstring and cfg"""
179-
global_vars = self.pyheader_env()
232+
global_vars = DeprecatedDict(self.pyheader_env())
180233
self.log.debug("pyheader initial global_vars %s", global_vars)
181234
self.log.debug("pyheader text being exec'ed: %s", pyheader)
182235

easybuild/framework/easyconfig/parser.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,64 @@
4242
from easybuild.tools.filetools import read_file, write_file
4343

4444

45+
# alternate easyconfig parameters, and their non-deprecated equivalents
46+
ALTERNATE_PARAMETERS = {
47+
# <new_param>: <equivalent_param>,
48+
'build_deps': 'builddependencies',
49+
'build_in_install_dir': 'buildininstalldir',
50+
'build_opts': 'buildopts',
51+
'build_stats': 'buildstats',
52+
'clean_up_old_build': 'cleanupoldbuild',
53+
'clean_up_old_install': 'cleanupoldinstall',
54+
'configure_opts': 'configopts',
55+
'deps': 'dependencies',
56+
'doc_paths': 'docpaths',
57+
'doc_urls': 'docurls',
58+
'do_not_create_install_dir': 'dontcreateinstalldir',
59+
'exts_class_map': 'exts_classmap',
60+
'exts_default_class': 'exts_defaultclass',
61+
'exts_default_opts': 'exts_default_options',
62+
'hidden_deps': 'hiddendependencies',
63+
'include_modulepath_exts': 'include_modpath_extensions',
64+
'install_opts': 'installopts',
65+
'keep_previous_install': 'keeppreviousinstall',
66+
'keep_symlinks': 'keepsymlinks',
67+
'max_parallel': 'maxparallel',
68+
'env_mod_aliases': 'modaliases',
69+
'env_mod_alt_soft_name': 'modaltsoftname',
70+
'modulepath_prepend_paths': 'moddependpaths',
71+
'env_mod_extra_paths_append': 'modextrapaths_append',
72+
'env_mod_extra_paths': 'modextrapaths',
73+
'env_mod_extra_vars': 'modextravars',
74+
'env_mod_load_msg': 'modloadmsg',
75+
'env_mod_lua_footer': 'modluafooter',
76+
'env_mod_tcl_footer': 'modtclfooter',
77+
'env_mod_category': 'moduleclass',
78+
'env_mod_depends_on': 'module_depends_on',
79+
'env_mod_force_unload': 'moduleforceunload',
80+
'env_mod_load_no_conflict': 'moduleloadnoconflict',
81+
'env_mod_unload_msg': 'modunloadmsg',
82+
'only_toolchain_env_mod': 'onlytcmod',
83+
'os_deps': 'osdependencies',
84+
'post_install_cmds': 'postinstallcmds',
85+
'post_install_msgs': 'postinstallmsgs',
86+
'post_install_patches': 'postinstallpatches',
87+
'pre_build_opts': 'prebuildopts',
88+
'pre_configure_opts': 'preconfigopts',
89+
'pre_install_opts': 'preinstallopts',
90+
'pre_test_opts': 'pretestopts',
91+
'recursive_env_mod_unload': 'recursive_module_unload',
92+
'run_test': 'runtest',
93+
'sanity_check_cmds': 'sanity_check_commands',
94+
'skip_fortran_mod_files_sanity_check': 'skip_mod_files_sanity_check',
95+
'skip_steps': 'skipsteps',
96+
'test_opts': 'testopts',
97+
'toolchain_opts': 'toolchainopts',
98+
'unpack_opts': 'unpack_options',
99+
'version_prefix': 'versionprefix',
100+
'version_suffix': 'versionsuffix',
101+
}
102+
45103
# deprecated easyconfig parameters, and their replacements
46104
DEPRECATED_PARAMETERS = {
47105
# <old_param>: (<new_param>, <deprecation_version>),

0 commit comments

Comments
 (0)