Skip to content

Commit aaf37ae

Browse files
Merge pull request #1517 from datajoint/fix/remove-global-excepthook
Remove process-wide sys.excepthook; harden log formatter (#1516)
2 parents 52d252e + 97500ff commit aaf37ae

2 files changed

Lines changed: 88 additions & 13 deletions

File tree

src/datajoint/logging.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,22 @@ class LevelAwareFormatter(logging.Formatter):
2828
def format(self, record):
2929
timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S")
3030
if record.levelno >= logging.WARNING:
31-
return f"[{timestamp}][{record.levelname}]: {record.getMessage()}"
31+
message = f"[{timestamp}][{record.levelname}]: {record.getMessage()}"
3232
elif record.levelno == JOBS:
33-
return f"[{timestamp}][JOBS]: {record.getMessage()}"
33+
message = f"[{timestamp}][JOBS]: {record.getMessage()}"
3434
else:
35-
return f"[{timestamp}] {record.getMessage()}"
35+
message = f"[{timestamp}] {record.getMessage()}"
36+
37+
# Render exception/stack info like the base logging.Formatter does, so
38+
# that a logger.exception(...)/exc_info=... call never silently drops
39+
# the traceback (see #1516).
40+
if record.exc_info and not record.exc_text:
41+
record.exc_text = self.formatException(record.exc_info)
42+
if record.exc_text:
43+
message = f"{message}\n{record.exc_text}"
44+
if record.stack_info:
45+
message = f"{message}\n{self.formatStack(record.stack_info)}"
46+
return message
3647

3748

3849
# Select output stream: stdout (default, no red highlighting) or stderr
@@ -44,13 +55,7 @@ def format(self, record):
4455
logger.setLevel(level=log_level)
4556
logger.handlers = [stream_handler]
4657

47-
48-
def excepthook(exc_type, exc_value, exc_traceback):
49-
if issubclass(exc_type, KeyboardInterrupt):
50-
sys.__excepthook__(exc_type, exc_value, exc_traceback)
51-
return
52-
53-
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
54-
55-
56-
sys.excepthook = excepthook
58+
# NOTE: DataJoint intentionally does NOT install a process-wide sys.excepthook.
59+
# Importing a library must not change how *unrelated* uncaught exceptions are
60+
# reported in the host process. Uncaught exceptions are left to Python's default
61+
# handler, which prints the full type/message/traceback to stderr. (See #1516.)

tests/unit/test_logging.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""
2+
Tests for datajoint.logging.
3+
4+
Regression coverage for #1516: importing datajoint must not install a
5+
process-wide sys.excepthook, and LevelAwareFormatter must not discard
6+
exc_info/stack_info.
7+
"""
8+
9+
import logging
10+
import sys
11+
12+
13+
def test_import_does_not_replace_excepthook():
14+
"""Importing datajoint must leave sys.excepthook untouched (#1516)."""
15+
original = sys.excepthook
16+
try:
17+
modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")]
18+
for mod in modules_to_remove:
19+
del sys.modules[mod]
20+
21+
import datajoint # noqa: F401
22+
23+
assert sys.excepthook is original, "importing datajoint replaced sys.excepthook"
24+
finally:
25+
sys.excepthook = original
26+
27+
28+
def test_formatter_renders_exception_info():
29+
"""LevelAwareFormatter must append the traceback when exc_info is set (#1516)."""
30+
from datajoint.logging import LevelAwareFormatter
31+
32+
formatter = LevelAwareFormatter()
33+
try:
34+
raise ValueError("something specific and diagnosable went wrong")
35+
except ValueError:
36+
record = logging.LogRecord(
37+
name="datajoint",
38+
level=logging.ERROR,
39+
pathname=__file__,
40+
lineno=0,
41+
msg="Uncaught exception",
42+
args=(),
43+
exc_info=sys.exc_info(),
44+
)
45+
46+
output = formatter.format(record)
47+
assert "Uncaught exception" in output
48+
assert "ValueError: something specific and diagnosable went wrong" in output
49+
assert "Traceback (most recent call last)" in output
50+
51+
52+
def test_formatter_renders_stack_info():
53+
"""LevelAwareFormatter must append stack_info when present (#1516)."""
54+
from datajoint.logging import LevelAwareFormatter
55+
56+
formatter = LevelAwareFormatter()
57+
record = logging.LogRecord(
58+
name="datajoint",
59+
level=logging.WARNING,
60+
pathname=__file__,
61+
lineno=0,
62+
msg="with stack",
63+
args=(),
64+
exc_info=None,
65+
sinfo="Stack (most recent call last):\n fake stack frame",
66+
)
67+
68+
output = formatter.format(record)
69+
assert "with stack" in output
70+
assert "fake stack frame" in output

0 commit comments

Comments
 (0)