Skip to content

Remove process-wide sys.excepthook; harden log formatter (#1516)#1517

Merged
dimitri-yatsenko merged 1 commit into
masterfrom
fix/remove-global-excepthook
Jul 21, 2026
Merged

Remove process-wide sys.excepthook; harden log formatter (#1516)#1517
dimitri-yatsenko merged 1 commit into
masterfrom
fix/remove-global-excepthook

Conversation

@dimitri-yatsenko

Copy link
Copy Markdown
Member

Fixes #1516.

Problem

Importing datajoint installed a process-wide sys.excepthook as an import side effect, routing every uncaught exception in the host process — DataJoint-related or not — through DataJoint's logger. Combined with LevelAwareFormatter overriding format() without rendering exc_info, every uncaught exception collapsed to a contentless line:

[2026-07-21 16:00:13][ERROR]: Uncaught exception

with no type, message, or traceback — strictly worse than Python's default handler, which would have printed the full traceback for free.

History (this is a regression, not the original design)

The hook was originally non-lossy: it inlined the exception message (Uncaught exception: {exc_value}) and rendered a full traceback under DJ_LOG_LEVEL=DEBUG via the stdlib formatter. Two later changes compounded into the silent-discard behavior:

  • a9dee8d9 — simplified the excepthook to rely solely on exc_info, dropping the inline message.
  • 41dad5d2 — introduced the custom LevelAwareFormatter, which overrides format() and never renders exc_info.

Change

  • Remove the sys.excepthook installation. A library must not change how unrelated exceptions surface in the importing process. Uncaught exceptions now fall through to Python's default handler (full traceback to stderr). Nothing in the codebase reads the hook, and no internal code path attaches exc_info to a log record, so removal is self-contained.
  • Harden LevelAwareFormatter to append exc_info/stack_info like the base logging.Formatter, so any future logger.exception(...) / exc_info=… call never silently drops a traceback.
  • Add regression tests (tests/unit/test_logging.py): import no longer replaces sys.excepthook; the formatter renders exception and stack info.

The centralized datajoint logger and its handler/level config are unchanged — only the process-global excepthook is removed.

Compatibility

Behavior change worth a release note: fatal uncaught tracebacks now print to stderr (Python's default handler) rather than as a single line on stdout. This is the correct destination for a crash, and restores the full type/message/traceback that the previous hook discarded.

Importing datajoint installed a global sys.excepthook that routed every
uncaught exception in the host process through datajoint's logger. Combined
with LevelAwareFormatter overriding format() without rendering exc_info,
this reduced every uncaught exception to a contentless
'[ERROR]: Uncaught exception' line, discarding the type, message, and
traceback -- strictly worse than Python's default handler.

- Remove the sys.excepthook installation. A library must not change how
  unrelated exceptions surface in the importing process; uncaught
  exceptions now fall through to Python's default handler (full traceback
  to stderr).
- Harden LevelAwareFormatter to append exc_info/stack_info like the base
  logging.Formatter, so internal logger.exception()/exc_info=... calls
  never silently drop tracebacks.
- Add regression tests.
@dimitri-yatsenko dimitri-yatsenko added the bug Indicates an unexpected problem or unintended behavior label Jul 21, 2026
@dimitri-yatsenko dimitri-yatsenko added this to the v2.3.2 milestone Jul 21, 2026

@MilagrosMarin MilagrosMarin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Two small non-blocking suggestions:

  1. Formatter tests only exercise WARNING/ERROR levels. exc_info/stack_info handling is appended after the level branches, so it applies uniformly at runtime — but a pytest.parametrize on level across INFO, JOBS, WARNING, ERROR would catch a future regression that adds an early return message inside the JOBS or INFO branch.

  2. Out of scope, follow-up thought: logging.py:17 still does logging.Logger.jobs = jobs — same architectural pattern as the excepthook you're removing (library patching global state at import time). A DataJointLogger(logging.Logger) subclass would be the polite version, but separate PR.

@dimitri-yatsenko
dimitri-yatsenko merged commit aaf37ae into master Jul 21, 2026
13 checks passed
@dimitri-yatsenko
dimitri-yatsenko deleted the fix/remove-global-excepthook branch July 21, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Indicates an unexpected problem or unintended behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: DataJoint silently discards exception info for every uncaught exception

2 participants