diff --git a/src/datajoint/logging.py b/src/datajoint/logging.py index 280d08167..b4931c7ca 100644 --- a/src/datajoint/logging.py +++ b/src/datajoint/logging.py @@ -28,11 +28,22 @@ class LevelAwareFormatter(logging.Formatter): def format(self, record): timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S") if record.levelno >= logging.WARNING: - return f"[{timestamp}][{record.levelname}]: {record.getMessage()}" + message = f"[{timestamp}][{record.levelname}]: {record.getMessage()}" elif record.levelno == JOBS: - return f"[{timestamp}][JOBS]: {record.getMessage()}" + message = f"[{timestamp}][JOBS]: {record.getMessage()}" else: - return f"[{timestamp}] {record.getMessage()}" + message = f"[{timestamp}] {record.getMessage()}" + + # Render exception/stack info like the base logging.Formatter does, so + # that a logger.exception(...)/exc_info=... call never silently drops + # the traceback (see #1516). + if record.exc_info and not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + message = f"{message}\n{record.exc_text}" + if record.stack_info: + message = f"{message}\n{self.formatStack(record.stack_info)}" + return message # Select output stream: stdout (default, no red highlighting) or stderr @@ -44,13 +55,7 @@ def format(self, record): logger.setLevel(level=log_level) logger.handlers = [stream_handler] - -def excepthook(exc_type, exc_value, exc_traceback): - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(exc_type, exc_value, exc_traceback) - return - - logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) - - -sys.excepthook = excepthook +# NOTE: DataJoint intentionally does NOT install a process-wide sys.excepthook. +# Importing a library must not change how *unrelated* uncaught exceptions are +# reported in the host process. Uncaught exceptions are left to Python's default +# handler, which prints the full type/message/traceback to stderr. (See #1516.) diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py new file mode 100644 index 000000000..7a2bb409b --- /dev/null +++ b/tests/unit/test_logging.py @@ -0,0 +1,70 @@ +""" +Tests for datajoint.logging. + +Regression coverage for #1516: importing datajoint must not install a +process-wide sys.excepthook, and LevelAwareFormatter must not discard +exc_info/stack_info. +""" + +import logging +import sys + + +def test_import_does_not_replace_excepthook(): + """Importing datajoint must leave sys.excepthook untouched (#1516).""" + original = sys.excepthook + try: + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + import datajoint # noqa: F401 + + assert sys.excepthook is original, "importing datajoint replaced sys.excepthook" + finally: + sys.excepthook = original + + +def test_formatter_renders_exception_info(): + """LevelAwareFormatter must append the traceback when exc_info is set (#1516).""" + from datajoint.logging import LevelAwareFormatter + + formatter = LevelAwareFormatter() + try: + raise ValueError("something specific and diagnosable went wrong") + except ValueError: + record = logging.LogRecord( + name="datajoint", + level=logging.ERROR, + pathname=__file__, + lineno=0, + msg="Uncaught exception", + args=(), + exc_info=sys.exc_info(), + ) + + output = formatter.format(record) + assert "Uncaught exception" in output + assert "ValueError: something specific and diagnosable went wrong" in output + assert "Traceback (most recent call last)" in output + + +def test_formatter_renders_stack_info(): + """LevelAwareFormatter must append stack_info when present (#1516).""" + from datajoint.logging import LevelAwareFormatter + + formatter = LevelAwareFormatter() + record = logging.LogRecord( + name="datajoint", + level=logging.WARNING, + pathname=__file__, + lineno=0, + msg="with stack", + args=(), + exc_info=None, + sinfo="Stack (most recent call last):\n fake stack frame", + ) + + output = formatter.format(record) + assert "with stack" in output + assert "fake stack frame" in output