Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions src/datajoint/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.)
70 changes: 70 additions & 0 deletions tests/unit/test_logging.py
Original file line number Diff line number Diff line change
@@ -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
Loading