From d590ab81fe38be67b6009b5a91d4c711540bcff7 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 3 Apr 2026 11:24:24 +0200 Subject: [PATCH 1/5] Introduce _util.log_exception This patch introduces the log_exception function for potential reuse of the logic that was formerly in log_traceback. In subsequent commits the log_exception function will be used to replace ad hoc error messages. This in particular improves the rendering of AGS errors, which as of version 0.3.2 uses exception notes to provide error context. --- src/nutils/_util.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/nutils/_util.py b/src/nutils/_util.py index 2097820ce..0dd031a01 100644 --- a/src/nutils/_util.py +++ b/src/nutils/_util.py @@ -24,6 +24,7 @@ import re import linecache import hashlib +import traceback from typing import Iterable, Sequence, Tuple supports_outdirfd = os.open in os.supports_dir_fd and os.listdir in os.supports_fd @@ -525,6 +526,22 @@ def post_mortem(pdb: bool = False): # pragma: no cover raise +def log_exception(e): + tbexc = traceback.TracebackException.from_exception(e) + prefix = '' + while True: + treelog.error(prefix + ''.join(tbexc.format_exception_only()).rstrip()) + treelog.debug('Traceback (most recent call first):\n' + ''.join(reversed(tbexc.stack.format())).rstrip()) + if tbexc.__cause__ is not None: + tbexc = tbexc.__cause__ + prefix = '.. caused by ' + elif tbexc.__context__ is not None and not tbexc.__suppress_context__: + tbexc = tbexc.__context__ + prefix = '.. while handling ' + else: + break + + @contextlib.contextmanager @defaults_from_env def log_traceback(gracefulexit: bool = True): @@ -533,31 +550,15 @@ def log_traceback(gracefulexit: bool = True): Afterwards ``SystemExit`` is raised to avoid reprinting of the traceback by Python's default error handler.''' - import traceback - if not gracefulexit: yield return try: yield - except SystemExit: - raise - except: - exc = traceback.TracebackException(*sys.exc_info()) - prefix = '' - while True: - treelog.error(prefix + ''.join(exc.format_exception_only()).rstrip()) - treelog.debug('Traceback (most recent call first):\n' + ''.join(reversed(exc.stack.format())).rstrip()) - if exc.__cause__ is not None: - exc = exc.__cause__ - prefix = '.. caused by ' - elif exc.__context__ is not None and not exc.__suppress_context__: - exc = exc.__context__ - prefix = '.. while handling ' - else: - break - raise SystemExit(1) + except Exception as e: + log_exception(e) + raise SystemExit(1) from None @contextlib.contextmanager From 8d3728dfe1a301408a35d5629fa576cb99d36007 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 3 Apr 2026 11:26:58 +0200 Subject: [PATCH 2/5] Use log_exception in log_arguments --- src/nutils/_util.py | 2 +- tests/test_util.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/nutils/_util.py b/src/nutils/_util.py index 0dd031a01..7f62b0d3e 100644 --- a/src/nutils/_util.py +++ b/src/nutils/_util.py @@ -505,7 +505,7 @@ def log_arguments(*args, **kwargs): bound.apply_defaults() treelog.info(yaml.dumps(bound, sig).rstrip()) except Exception as e: - treelog.error("failed to serialize arguments:", e) + log_exception(e) return f(*args, **kwargs) return log_arguments diff --git a/tests/test_util.py b/tests/test_util.py index 549ac958b..9ce7aab6d 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -230,7 +230,10 @@ def f(foo, bar): with self.assertLogs('nutils') as cm: f('x', 10) - self.assertEqual(cm.output, ['ERROR:nutils:arguments > failed to serialize arguments: in .foo: cannot establish type for parameter foo']) + if sys.version_info >= (3, 11): + self.assertEqual(cm.output, ['ERROR:nutils:arguments > TypeError: cannot establish type for parameter foo\nIn: .foo']) + else: + self.assertEqual(cm.output, ['ERROR:nutils:arguments > TypeError: cannot establish type for parameter foo']) class log_traceback(TestCase): From 7f7ad221caea346a77e8847e6c03c699da95af3b Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 3 Apr 2026 11:51:40 +0200 Subject: [PATCH 3/5] Use log_exception in defaults_from_env --- src/nutils/_util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nutils/_util.py b/src/nutils/_util.py index 7f62b0d3e..ffe540569 100644 --- a/src/nutils/_util.py +++ b/src/nutils/_util.py @@ -385,7 +385,8 @@ def defaults_from_env(f): try: v = ucsl.loads(os.environ[envname], param.annotation) except Exception as e: - warnings.warn(f'ignoring environment variable {envname}: {e}') + warnings.warn(f'ignoring environment variable {envname}') + log_exception(e) else: param = param.replace(default=v) changed = True From edae791a8f1b4100e479a08c6456d3a56fd84efb Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Fri, 3 Apr 2026 12:01:08 +0200 Subject: [PATCH 4/5] Use log_exception in add_htmllog --- src/nutils/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nutils/_util.py b/src/nutils/_util.py index ffe540569..a594cf3a5 100644 --- a/src/nutils/_util.py +++ b/src/nutils/_util.py @@ -679,7 +679,7 @@ def add_htmllog(outrootdir: str = '~/public_html', outrooturi: str = '', scriptn yield except Exception as e: with treelog.set(htmllog): - treelog.error(f'{e.__class__.__name__}: {e}') + log_exception(e) raise finally: treelog.info(f'log written to: {loguri}') From c0e9514a9d4ba4b93f5285d837e012aeddac3365 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Sat, 11 Apr 2026 00:07:31 +0200 Subject: [PATCH 5/5] Bump ags dependency --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99deb8d5f..08c0b6f9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "appdirs >=1,<2", "numpy >=2.0.1,<3", "nutils-poly >=1,<2", - "ags[yaml] >=0.3,<0.4", + "ags[yaml] >=0.4,<0.5", "stringly", "treelog >=2,<3", ]