From 27934e774a8433afd645cadad7ab133f4919296c Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 14:22:30 +0100 Subject: [PATCH 1/8] Replace `print_error` by `print_error_and_exit` There is a `print_error` function in `output` and this one additionally exits which is not reflected in the name. Also the handling of the parameters is confusing, e.g. using `log` to determine whether to throw an `EasyBuildError`. Deprecate it and replace by `print_error_and_exit`, translating the arguments from one to the other. --- easybuild/base/testing.py | 9 ++++++ easybuild/framework/easyblock.py | 6 ++-- easybuild/framework/easyconfig/tools.py | 4 +-- easybuild/main.py | 23 +++++++-------- easybuild/tools/build_log.py | 37 +++++++++++++++++-------- test/framework/build_log.py | 36 ++++++++++++++---------- 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/easybuild/base/testing.py b/easybuild/base/testing.py index a714e0f2e3..071fb1959e 100644 --- a/easybuild/base/testing.py +++ b/easybuild/base/testing.py @@ -210,6 +210,15 @@ def mocked_stdout(self): finally: self.mock_stdout(False) + @contextmanager + def mocked_stderr(self): + """Context manager to mock stdout""" + self.mock_stderr(True) + try: + yield sys.stderr + finally: + self.mock_stderr(False) + @contextmanager def mocked_stdout_stderr(self, mock_stdout=True, mock_stderr=True): """Context manager to mock stdout and stderr""" diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index c6fade8006..ad91dc7d3a 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -81,7 +81,7 @@ from easybuild.tools import LooseVersion, config from easybuild.tools.build_details import get_build_stats from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, dry_run_msg, dry_run_warning, dry_run_set_dirs -from easybuild.tools.build_log import print_error, print_msg, print_warning +from easybuild.tools.build_log import print_error_and_exit, print_msg, print_warning from easybuild.tools.config import CHECKSUM_PRIORITY_JSON, DEFAULT_ENVVAR_USERS_MODULES from easybuild.tools.config import EASYBUILD_SOURCES_URL, EBPYTHONPREFIXES # noqa from easybuild.tools.config import FORCE_DOWNLOAD_ALL, FORCE_DOWNLOAD_PATCHES, FORCE_DOWNLOAD_SOURCES @@ -5064,8 +5064,8 @@ def build_and_install_one(ecdict, init_env): app = app_class(ecdict['ec']) _log.info("Obtained application instance for %s (easyblock: %s)" % (name, easyblock)) except EasyBuildError as err: - print_error("Failed to get application instance for %s (easyblock: %s): %s" % (name, easyblock, err.msg), - silent=silent) + print_error_and_exit("Failed to get application instance for %s (easyblock: %s): %s", name, easyblock, err.msg, + silent=silent, exit_code=err.exit_code) # application settings stop = build_option('stop') diff --git a/easybuild/framework/easyconfig/tools.py b/easybuild/framework/easyconfig/tools.py index 2e548298ff..c655721242 100644 --- a/easybuild/framework/easyconfig/tools.py +++ b/easybuild/framework/easyconfig/tools.py @@ -54,7 +54,7 @@ from easybuild.framework.easyconfig.easyconfig import process_easyconfig from easybuild.framework.easyconfig.style import cmdline_easyconfigs_style_check from easybuild.tools import LooseVersion -from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_error, print_msg, print_warning +from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, print_error_and_exit, print_msg, print_warning from easybuild.tools.config import build_option from easybuild.tools.environment import restore_env from easybuild.tools.filetools import EASYBLOCK_CLASS_PREFIX, get_cwd, find_easyconfigs, is_patch_file @@ -216,7 +216,7 @@ def mk_node_name(spec): if _dep_graph_dump(dgr, filename): print_msg("Wrote " + what, silent=silent) else: - print_error("Failed writing " + what, silent=silent) + print_error_and_exit("Failed writing " + what, silent=silent) @only_if_module_is_available('pygraph.readwrite.dot', pkgname='python-graph-dot') diff --git a/easybuild/main.py b/easybuild/main.py index 034a9e9dd2..e0eb6ba8c9 100755 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -49,7 +49,7 @@ # IMPORTANT this has to be the first easybuild import as it customises the logging # expect missing log output when this not the case! -from easybuild.tools.build_log import EasyBuildError, print_error, print_msg, print_warning, stop_logging +from easybuild.tools.build_log import EasyBuildError, print_error_and_exit, print_msg, print_warning, stop_logging from easybuild.tools.build_log import EasyBuildExit from easybuild.framework.easyblock import build_and_install_one, inject_checksums, inject_checksums_to_json @@ -428,9 +428,12 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session elif any(no_ec_opts): paths = determined_paths else: - print_error("Please provide one or multiple easyconfig files, or use software build " + - "options to make EasyBuild search for easyconfigs", - log=_log, opt_parser=eb_go.parser, exit_on_error=not testing) + msg = ("Please provide one or multiple easyconfig files, or use software build " + "options to make EasyBuild search for easyconfigs") + if testing: + raise EasyBuildError(msg) + eb_go.parser.print_shorthelp() + print_error_and_exit(msg, log=_log) _log.debug("Paths: %s", paths) # run regtest @@ -560,10 +563,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session elif options.check_conflicts: if check_conflicts(easyconfigs, modtool): - print_error("One or more conflicts detected!") - sys.exit(1) - else: - print_msg("\nNo conflicts detected!\n", prefix=False) + print_error_and_exit("One or more conflicts detected!") + print_msg("\nNo conflicts detected!\n", prefix=False) # dump source script to set up build environment elif options.dump_env_script: @@ -836,7 +837,7 @@ def main_with_hooks(args=None): try: init_session_state, eb_go, cfg_settings = prepare_main(args=args) except EasyBuildError as err: - print_error(err.msg, exit_code=err.exit_code) + print_error_and_exit(err.msg, exit_code=err.exit_code) hooks = load_hooks(eb_go.options.hooks) @@ -845,10 +846,10 @@ def main_with_hooks(args=None): sys.exit(int(exit_code)) except EasyBuildError as err: run_hook(FAIL, hooks, args=[err]) - print_error(err.msg, exit_on_error=True, exit_code=err.exit_code) + print_error_and_exit(err.msg, exit_code=err.exit_code) except KeyboardInterrupt as err: run_hook(CANCEL, hooks, args=[err]) - print_error("Cancelled by user: %s" % err) + print_error_and_exit("Cancelled by user: %s", err) except Exception as err: run_hook(CRASH, hooks, args=[err]) sys.stderr.write("EasyBuild crashed! Please consider reporting a bug, this should not happen...\n\n") diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index 64a3d5fafb..02ccff7f1b 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -394,8 +394,8 @@ def print_error(msg, *args, **kwargs): """ Print error message and exit EasyBuild """ - if args: - msg = msg % args + + _init_easybuildlog.deprecated("Function 'build_log.print_error' is replaced with 'print_error_and_exit'", '6.0') # grab exit code, if specified; # also consider deprecated 'exitCode' option @@ -404,9 +404,6 @@ def print_error(msg, *args, **kwargs): if exitCode is not None: _init_easybuildlog.deprecated("'exitCode' option in print_error function is replaced with 'exit_code'", '6.0') - if exit_code is None: - exit_code = EasyBuildExit.ERROR - log = kwargs.pop('log', None) opt_parser = kwargs.pop('opt_parser', None) exit_on_error = kwargs.pop('exit_on_error', True) @@ -415,13 +412,31 @@ def print_error(msg, *args, **kwargs): raise EasyBuildError("Unknown named arguments passed to print_error: %s", kwargs) if exit_on_error: - if not silent: - if opt_parser: - opt_parser.print_shorthelp() - sys.stderr.write("ERROR: %s\n" % msg) - sys.exit(int(exit_code)) + if not silent and opt_parser: + opt_parser.print_shorthelp() + if exit_code is None: + exit_code = EasyBuildExit.ERROR + print_error_and_exit(msg, *args, exit_code=exit_code, log=log, silent=silent) elif log is not None: - raise EasyBuildError(msg) + raise EasyBuildError(msg) # Handle legacy weirdness + + +def print_error_and_exit(msg, *args, exit_code=EasyBuildExit.ERROR, log=None, silent=False): + """ + Print error message and exit EasyBuild, supports format strings + + :param msg: Message to show + :exit_code: EasyBuildExit or integer to exit with + :log: When set also log the error + :silent: When True don't print to stderr + """ + if args: + msg = msg % args + if log: + log.error(msg) + if not silent: + print("ERROR: %s" % msg, file=sys.stderr) + sys.exit(int(exit_code)) def print_warning(msg, *args, **kwargs): diff --git a/test/framework/build_log.py b/test/framework/build_log.py index 19ceb99440..724c5ee9fc 100644 --- a/test/framework/build_log.py +++ b/test/framework/build_log.py @@ -38,8 +38,8 @@ from easybuild.base.fancylogger import getLogger, logToFile, setLogFormat from easybuild.framework.easyconfig.tweak import tweak_one from easybuild.tools.build_log import ( - LOGGING_FORMAT, EasyBuildError, EasyBuildLog, dry_run_msg, dry_run_warning, init_logging, print_error, print_msg, - print_warning, stop_logging, time_str_since, raise_nosupport) + LOGGING_FORMAT, EasyBuildError, EasyBuildLog, dry_run_msg, dry_run_warning, init_logging, print_error, + print_error_and_exit, print_msg, print_warning, stop_logging, time_str_since, raise_nosupport) from easybuild.tools.filetools import read_file, write_file @@ -275,19 +275,26 @@ def run_check(args, silent=False, expected_stderr='', **kwargs): log_txt = read_file(tmp_logfile) self.assertIn("WARNING Test log message with a logger involved.", log_txt) - def test_print_error(self): + def test_print_error_and_exit(self): """Test print_error""" - def run_check(args, silent=False, expected_stderr=''): + def run_check(args, silent=False, expected_stderr=None): """Helper function to check stdout/stderr produced via print_error.""" - self.mock_stderr(True) - self.mock_stdout(True) - self.assertErrorRegex(SystemExit, '1', print_error, *args, silent=silent) - stderr = self.get_stderr() - stdout = self.get_stdout() - self.mock_stdout(False) - self.mock_stderr(False) - self.assertEqual(stdout, '') - self.assertTrue(stderr.startswith(expected_stderr)) + for func in ("print_error_and_exit", "print_error"): + with self.subTest(f"Function {func}"): + with self.mocked_stdout_stderr(): + if func == "print_error": # Deprecated variant + with self.temporarily_allow_deprecated_behaviour(): + self.assertRaisesRegex(SystemExit, '1', print_error, *args, silent=silent) + stderr = re.sub(r'\nWARNING: Deprecated.*\n\n', '', self.get_stderr()) + else: + self.assertRaisesRegex(SystemExit, '1', print_error_and_exit, *args, silent=silent) + stderr = self.get_stderr() + stdout = self.get_stdout() + self.assertEqual(stdout, '') + if expected_stderr: + self.assertTrue(stderr.endswith(expected_stderr)) + else: + self.assertEqual(stderr, '') run_check(['You have failed.'], expected_stderr="ERROR: You have failed.\n") run_check(['You have %s.', 'failed'], expected_stderr="ERROR: You have failed.\n") @@ -296,7 +303,8 @@ def run_check(args, silent=False, expected_stderr=''): run_check(['You have %s.', 'failed'], silent=True) run_check(['%s %s %s.', 'You', 'have', 'failed'], silent=True) - self.assertErrorRegex(EasyBuildError, "Unknown named arguments", print_error, 'foo', unknown_arg='bar') + with self.temporarily_allow_deprecated_behaviour(), self.mocked_stderr(): + self.assertErrorRegex(EasyBuildError, "Unknown named arguments", print_error, 'foo', unknown_arg='bar') def test_print_msg(self): """Test print_msg""" From 2b82be563dc97f97f0dcfaf8b1e884d262f09d11 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 14:25:47 +0100 Subject: [PATCH 2/8] Fix wrong variable used in error --- easybuild/framework/easyblock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index ad91dc7d3a..2582a27540 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2982,7 +2982,8 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True): if os.path.isabs(self.rpath_wrappers_dir): _log.info(f"Using {self.rpath_wrappers_dir} to store/use RPATH wrappers") else: - raise EasyBuildError(f"Path used for rpath_wrappers_dir is not an absolute path: {path}") + raise EasyBuildError("Path used for rpath_wrappers_dir is not an absolute path: %s", + self.rpath_wrappers_dir) if self.iter_idx > 0: # reset toolchain for iterative runs before preparing it again From caf4d3ded9c0aeab543bf21f51b7d833d66475cc Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 15:31:04 +0100 Subject: [PATCH 3/8] Remove `log` parameter from `print_error_and_exit` Only used in one place and all error logging should happen in the constructor of `EasyBuildError` since #1218 --- easybuild/main.py | 2 +- easybuild/tools/build_log.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/easybuild/main.py b/easybuild/main.py index e0eb6ba8c9..af3d4fc514 100755 --- a/easybuild/main.py +++ b/easybuild/main.py @@ -433,7 +433,7 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session if testing: raise EasyBuildError(msg) eb_go.parser.print_shorthelp() - print_error_and_exit(msg, log=_log) + print_error_and_exit(msg) _log.debug("Paths: %s", paths) # run regtest diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index 02ccff7f1b..cb2b3dc4a2 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -416,24 +416,21 @@ def print_error(msg, *args, **kwargs): opt_parser.print_shorthelp() if exit_code is None: exit_code = EasyBuildExit.ERROR - print_error_and_exit(msg, *args, exit_code=exit_code, log=log, silent=silent) + print_error_and_exit(msg, *args, exit_code=exit_code, silent=silent) elif log is not None: raise EasyBuildError(msg) # Handle legacy weirdness -def print_error_and_exit(msg, *args, exit_code=EasyBuildExit.ERROR, log=None, silent=False): +def print_error_and_exit(msg, *args, exit_code=EasyBuildExit.ERROR, silent=False): """ Print error message and exit EasyBuild, supports format strings :param msg: Message to show :exit_code: EasyBuildExit or integer to exit with - :log: When set also log the error :silent: When True don't print to stderr """ if args: msg = msg % args - if log: - log.error(msg) if not silent: print("ERROR: %s" % msg, file=sys.stderr) sys.exit(int(exit_code)) From 658e7f16e08125ac064d8a6b2c633a53086cdc53 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 16:54:45 +0100 Subject: [PATCH 4/8] Enhance test description Co-authored-by: Kenneth Hoste --- test/framework/build_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/framework/build_log.py b/test/framework/build_log.py index 724c5ee9fc..5b1229e9b7 100644 --- a/test/framework/build_log.py +++ b/test/framework/build_log.py @@ -276,7 +276,7 @@ def run_check(args, silent=False, expected_stderr='', **kwargs): self.assertIn("WARNING Test log message with a logger involved.", log_txt) def test_print_error_and_exit(self): - """Test print_error""" + """Test print_error_and_exit and (deprecated) print_error functions""" def run_check(args, silent=False, expected_stderr=None): """Helper function to check stdout/stderr produced via print_error.""" for func in ("print_error_and_exit", "print_error"): From fdf950ffeadb5fb85eb9ec488911114735ce8733 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 16:53:35 +0100 Subject: [PATCH 5/8] Add empty lines around error Use `easybuild.tools.print_error` for a uniform appearance --- easybuild/tools/build_log.py | 3 ++- easybuild/tools/output.py | 5 +++-- test/framework/build_log.py | 15 ++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index cb2b3dc4a2..b09d27e30d 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -432,7 +432,8 @@ def print_error_and_exit(msg, *args, exit_code=EasyBuildExit.ERROR, silent=False if args: msg = msg % args if not silent: - print("ERROR: %s" % msg, file=sys.stderr) + from easybuild.tools.output import print_error as show_error + show_error("ERROR: " + msg, rich_highlight=False) sys.exit(int(exit_code)) diff --git a/easybuild/tools/output.py b/easybuild/tools/output.py index a7284c3600..b2f66010a2 100644 --- a/easybuild/tools/output.py +++ b/easybuild/tools/output.py @@ -399,11 +399,12 @@ def print_error(error_msg, rich_highlight=True): :param rich_highlight: boolean indicating whether automatic highlighting by Rich should be enabled """ + error_msg = f'\n\n{error_msg}\n' if use_rich(): console = Console(stderr=True) - console.print('\n\n' + error_msg + '\n', highlight=rich_highlight) + console.print(error_msg, highlight=rich_highlight) else: - sys.stderr.write('\n' + error_msg + '\n\n') + print(error_msg, file=sys.stderr) # this constant must be defined at the end, since functions used as values need to be defined diff --git a/test/framework/build_log.py b/test/framework/build_log.py index 5b1229e9b7..eb24dd1082 100644 --- a/test/framework/build_log.py +++ b/test/framework/build_log.py @@ -277,7 +277,7 @@ def run_check(args, silent=False, expected_stderr='', **kwargs): def test_print_error_and_exit(self): """Test print_error_and_exit and (deprecated) print_error functions""" - def run_check(args, silent=False, expected_stderr=None): + def run_check(args, silent=False, expected_stderr=''): """Helper function to check stdout/stderr produced via print_error.""" for func in ("print_error_and_exit", "print_error"): with self.subTest(f"Function {func}"): @@ -291,14 +291,11 @@ def run_check(args, silent=False, expected_stderr=None): stderr = self.get_stderr() stdout = self.get_stdout() self.assertEqual(stdout, '') - if expected_stderr: - self.assertTrue(stderr.endswith(expected_stderr)) - else: - self.assertEqual(stderr, '') - - run_check(['You have failed.'], expected_stderr="ERROR: You have failed.\n") - run_check(['You have %s.', 'failed'], expected_stderr="ERROR: You have failed.\n") - run_check(['%s %s %s.', 'You', 'have', 'failed'], expected_stderr="ERROR: You have failed.\n") + self.assertEqual(stderr, expected_stderr) + + run_check(['You have failed.'], expected_stderr="\n\nERROR: You have failed.\n\n") + run_check(['You have %s.', 'failed'], expected_stderr="\n\nERROR: You have failed.\n\n") + run_check(['%s %s %s.', 'You', 'have', 'failed'], expected_stderr="\n\nERROR: You have failed.\n\n") run_check(['You have failed.'], silent=True) run_check(['You have %s.', 'failed'], silent=True) run_check(['%s %s %s.', 'You', 'have', 'failed'], silent=True) From f2f004106c3e13d6f9ae112fcbb0b08011017624 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 14 Jan 2026 16:57:41 +0100 Subject: [PATCH 6/8] Don't try using `rich` before exit This is called in error handlers and using `rich` can cause errors. --- easybuild/tools/build_log.py | 2 +- easybuild/tools/output.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index b09d27e30d..4a414c5bb9 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -433,7 +433,7 @@ def print_error_and_exit(msg, *args, exit_code=EasyBuildExit.ERROR, silent=False msg = msg % args if not silent: from easybuild.tools.output import print_error as show_error - show_error("ERROR: " + msg, rich_highlight=False) + show_error("ERROR: " + msg, disable_rich=True) sys.exit(int(exit_code)) diff --git a/easybuild/tools/output.py b/easybuild/tools/output.py index b2f66010a2..b53785a035 100644 --- a/easybuild/tools/output.py +++ b/easybuild/tools/output.py @@ -392,15 +392,15 @@ def print_checks(checks_data): print('\n'.join(lines)) -def print_error(error_msg, rich_highlight=True): +def print_error(error_msg, rich_highlight=True, disable_rich=False): """ - Print error message, using a Rich Console instance if possible. + Print error message, using a Rich Console instance if possible unless disable_rich=True. Newlines before/after message are automatically added. :param rich_highlight: boolean indicating whether automatic highlighting by Rich should be enabled """ error_msg = f'\n\n{error_msg}\n' - if use_rich(): + if not disable_rich and use_rich(): console = Console(stderr=True) console.print(error_msg, highlight=rich_highlight) else: From 6d80e0261fa53652550fab3c9a71943c0c3aacd5 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 15 Jan 2026 08:59:32 +0100 Subject: [PATCH 7/8] Improve deprecation message Co-authored-by: Kenneth Hoste --- easybuild/tools/build_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index 4a414c5bb9..979dc4695c 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -395,7 +395,7 @@ def print_error(msg, *args, **kwargs): Print error message and exit EasyBuild """ - _init_easybuildlog.deprecated("Function 'build_log.print_error' is replaced with 'print_error_and_exit'", '6.0') + _init_easybuildlog.deprecated("Function 'print_error' from easybuild.tools.build_log is replaced with 'print_error_and_exit'", '6.0') # grab exit code, if specified; # also consider deprecated 'exitCode' option From 396dbeb3ab122031c7702bd9014842cc34f7ebca Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 15 Jan 2026 09:04:49 +0100 Subject: [PATCH 8/8] Fix line length --- easybuild/tools/build_log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/easybuild/tools/build_log.py b/easybuild/tools/build_log.py index 979dc4695c..e85cef0933 100644 --- a/easybuild/tools/build_log.py +++ b/easybuild/tools/build_log.py @@ -395,7 +395,8 @@ def print_error(msg, *args, **kwargs): Print error message and exit EasyBuild """ - _init_easybuildlog.deprecated("Function 'print_error' from easybuild.tools.build_log is replaced with 'print_error_and_exit'", '6.0') + _init_easybuildlog.deprecated("Function 'print_error' from easybuild.tools.build_log is replaced " + "with 'print_error_and_exit'", '6.0') # grab exit code, if specified; # also consider deprecated 'exitCode' option