Skip to content

v1.3.1

Choose a tag to compare

@kidoz kidoz released this 17 Sep 17:53
· 65 commits to main since this release

structguru v1.3.1

A performance release for structured exceptions. One defect is fixed: with structured_exceptions=True, every logger.exception() call read and dedented source for the whole traceback before discarding all but the last exception_max_frames frames, so a deep traceback such as a RecursionError cost 1.7 ms to report twenty frames; and frames captured with exception_include_locals=True reported "line": null where every other frame carried its source line. No breaking changes; upgrade in place from any v1.x.

Fixes

  • Structured exceptions extract source only for the frames they keep. The frame builder ran traceback.extract_tb() over the entire traceback — linecache reads and dedents the source line for each frame it visits — and then sliced the result to the last exception_max_frames. The cost therefore scaled with the depth of the traceback rather than with the number of frames reported: a RecursionError a thousand frames deep paid for a thousand source lookups to produce twenty. The traceback is now walked to the first kept frame before extraction runs, so the same call costs 64 µs instead of 1.7 ms, and a shallow traceback is unaffected.

    Frames captured with exception_include_locals=True now carry the line field like every other frame. That path walked the raw traceback itself to reach f_locals and never looked up source, leaving line as null; locals now come from the same selected frames that source is extracted for, so the two modes produce identical frames apart from locals.

    configure(structured_exceptions=True, exception_include_locals=True)
    # before: {"filename": "app.py", "lineno": 42, "name": "handle", "line": null, "locals": {"n": "0"}}
    # after:  {"filename": "app.py", "lineno": 42, "name": "handle", "line": "raise ValueError(\"boom\")", "locals": {"n": "0"}}

    Filling in line makes the locals path pay for source lookup too — about 50 µs instead of 10 µs for twenty frames — which is the price of a consistent schema. Formatted tracebacks, exception_max_frames, and the locals redaction and repr limits are unchanged.

Test coverage

  • 2 new Python test cases pin the fix: extract_tb() is called on the first kept frame only, and locals and the source line are captured together on the same frame — 732 Python tests and 79 Rust tests in total, with line coverage at 94%. The in-process writer fork test now filters CPython's multi-threaded fork() deprecation warning, which it triggers on purpose, so the suite runs warning-free on 3.12+.