Skip to content

Introduce exception records and traceback renderers - #15332

Closed
doubledare704 wants to merge 12 commits into
pytest-dev:mainfrom
doubledare704:excinfo-capture-model-renderers
Closed

doubledare704 wants to merge 12 commits into
pytest-dev:mainfrom
doubledare704:excinfo-capture-model-renderers

Conversation

@doubledare704

Copy link
Copy Markdown

Refs #15072.

Exception capture, traceback formatting, and report serialization currently share the legacy ExceptionInfo/Repr* pipeline. This draft introduces a plain-data exception snapshot and renderers so consumers can retain structured frames and exception groups even when terminal output is shortened with --tb=line or suppressed with --tb=no.

I would like feedback on the model and migration boundaries before treating this as a complete implementation of the proposal.

Implemented

  • Add ExceptionInfo.record() and frozen record dataclasses for exceptions, frames, locations, and syntax errors, with versioned JSON serialization, eager source capture, notes, cause/context links, and group children.
  • Add PytestTraceback and --tb=rich, record-backed legacy rendering, pytest-style exception-group rendering, and PEP 657 positions/carets.
  • Preserve captured source in the Rich renderer after files are edited or removed, without changing global linecache state. Honor chain=False across rendering entry points.
  • Preserve repeated group members and shared causes while cutting actual cycles.
  • Share exception-matching primitives and structured mismatch descriptions between ExceptionInfo and raisers.
  • Add structured serialization for doctest, fixture-lookup, and collection failures; introduce tuple-compatible Location for skips.
  • Export and document the record/rendering API and add migration/deprecation documentation for legacy representations.

Remaining migration work

This draft does not complete #15072:

  • Normal exception reports still contain legacy ExceptionChainRepr/ReprExceptionInfo objects with an attached record. Their wire payload still includes rendered legacy data alongside the record.
  • Default pytest-style Rich rendering still builds legacy entries and parses their markers.
  • pytest_internalerror still receives a legacy representation; forwarding structured internal errors through xdist remains pending.
  • Reporting consumers, including JUnit, still use compatibility attributes such as reprcrash.
  • The timing of public API stabilization and deprecation warnings needs agreement. The draft currently includes those changes.
  • Add changelog entries and an AUTHORS entry before marking ready; broader plugin compatibility and the supported Python-version matrix remain to be validated.

Local validation

On macOS with Python 3.13.3:

  • Focused record, report, and skipping tests: 215 passed.
  • Full-suite run: 4,837 passed, 3 failed, 52 skipped, 15 xfailed, 5 xpassed.
  • Real xdist runs with passing, failing, and skipped tests, locals, chained exceptions, and deprecations treated as errors behaved as expected in both long and Rich styles.
  • Ruff check/format (0.15.2) and git diff --check passed for the latest patch's files.
  • Mypy 2.3.1 reported 12 errors, identical to the same check against 36a1c7d01, before the latest regression fixes. These local tool versions differ from the pinned pre-commit versions.

The three full-suite failures are:

  • testing/acceptance_test.py::TestInvocationVariants::test_doctest_id
  • testing/test_assertion.py::test_raise_assertion_error_raising_repr
  • testing/test_doctest.py::TestDoctests::test_fixture_doctest_skip_has_line_number

All three also reproduce at 36a1c7d01; that establishes they predate the latest fixes, not that they are unrelated to this PR. They still need resolution or comparison with upstream before this is ready. CI and the complete xdist integration suite are not claimed green.

Feedback requested

Would maintainers prefer this split into the proposal's independently reviewable increments, starting with the model/capture layer? The main remaining decisions are when to switch longrepr and the wire format, when to start deprecations, and whether the tree/source-capture/Rich-adapter choices are suitable.

AI assistance: Codex was used for code review, regression fixes, and local validation.

  • Include documentation for new features.
  • Include regression tests.
  • Allow maintainer edits.
  • Add changelog entries.
  • Add an AUTHORS entry.
  • Add AI co-author commit trailers (suggested by the contribution policy; not currently present).

doubledare704 and others added 10 commits November 3, 2026 11:42
…st-dev#15072)

ExceptionInfo.getrepr() and the Repr* classes currently bake display
style, path resolution, and syntax markers into pre-rendered strings at
capture time, while report serialization reconstructs only a subset of
those dataclasses by hand and degrades other representations across
process boundaries.

Introduce the first increment of pytest-dev#15072 alongside existing code without
changing current consumers:

- Add frozen, schema-versioned data model classes (Location,
  SyntaxErrorDetails, FrameRecord, ExceptionRecord) in
  _pytest._code.record with JSON-compatible to_dict()/from_dict()
  serialization and a lossless to_rich_trace() adapter.
- Model exception chains and BaseExceptionGroup sub-exceptions as a tree
  (cause, context, children) while breaking reference cycles eagerly at
  capture time so all downstream traversal and serialization operate on
  a finite acyclic structure.
- Capture raw frame statement lines eagerly within generous bounds
  (MAX_STATEMENT_LINES, MAX_STATEMENT_LINE_CHARS) anchored at
  statement_lineno so pathological frames cannot bloat wire payloads
  while preserving original indentation for PEP 657 byte-offset column
  spans and render-time deindenting.
- Add ExceptionInfo.record() to snapshot full tracebacks, hidden-frame
  flags, per-entry style overrides, assertion striptext, notes, syntax
  error details, and RecursionError indices.
Introduce `PytestTraceback` as the presentation layer over the pure-data
`ExceptionRecord` model so traceback formatting is decoupled from live Python
frames and pre-baked `Repr*` strings (Step 2 of pytest-dev#15072).

`PytestTraceback` supports all existing traceback styles (`long`, `short`,
`auto`, `line`, `no`, `value`, `native`) as well as a new `rich` style powered
by `ExceptionRecord.to_rich_trace()`, and implements both pytest's
`toterminal(tw)` protocol (`to_repr()` compatibility shim) and Rich's
`__rich_console__` protocol. Wire `--tb=rich` through CLI options,
`Node._repr_failure_py`, and `ExceptionInfo.getrepr()`.

Refs pytest-dev#15072
…d-then-render

Previously, ExceptionInfo.getrepr() baked the requested --tb style directly into
the constructed Repr* objects, dropping per-frame file locations under --tb=line
and --tb=no (pytest-dev#14720) and preventing reporting plugins or cross-process consumers
from accessing the full structured traceback when terminal output was suppressed
or shortened.

Route ExceptionInfo.getrepr() through ExceptionInfo.record() and
PytestTraceback.to_repr() across all traceback styles so that exception capture
always records the complete structured ExceptionRecord before rendering:

- Attach the underlying ExceptionRecord to ExceptionRepr.record and preserve
  ReprEntry.reprfileloc for all frames even when rendering --tb=line or --tb=no,
  while keeping terminal output for those styles compact.
- Add compatibility properties (reprcrash, reprtraceback, chain) on
  ExceptionRecord and make FrameRecord hashable when locals are captured so
  records can serve as xdist collection-error dictionary keys.
- Preserve None syntax error messages in SyntaxErrorDetails so bare SyntaxError()
  instances format identically to stdlib (<no detail available>).
- Extend report JSON serialization (_report_to_json / _report_kwargs_from_json)
  to round-trip ExceptionRecord payloads both when attached to ExceptionRepr and
  when stored directly as report.longrepr.

Fixes pytest-dev#14720
Refs pytest-dev#15072
…4 of pytest-dev#15072)

Separate exception capture from formatting by introducing a structured,
serializable data model (`Location`, `SyntaxErrorDetails`, `FrameRecord`,
`ExceptionRecord`, and the `FailureRecord` protocol) alongside a dedicated
`PytestTraceback` renderer:

- Add `ExceptionInfo.record()` to capture traceback frames, chain topology
  (`cause`, `context`, `suppress_context`, `children`), bounded source
  statements with PEP 657 byte spans, and formatted locals into an
  immutable `ExceptionRecord` tree with `to_dict()`/`from_dict()` and
  `to_rich_trace()` support.
- Introduce `PytestTraceback` to render `ExceptionRecord` across all pytest
  traceback styles (`auto`, `long`, `short`, `line`, `native`, `value`, `no`)
  and add `--tb=rich` backed by Rich's `Traceback` renderer.
- Switch `ExceptionInfo.getrepr()` and report JSON serialization to a
  record-then-render pipeline, preserving `longrepr.record` and crash
  locations even under `--tb=line` and `--tb=no` (fixes pytest-dev#14720).
- Migrate `ReprFailDoctest`, `FixtureLookupErrorRepr`, and `CollectErrorRepr`
  to implement the `FailureRecord` protocol (`sections`, `crash`,
  `to_dict()`/`from_dict()`, `__rich_console__()`), and replace raw skip
  3-tuples on `report.longrepr` with `Location` while maintaining
  tuple-unpacking and xdist wire compatibility.

Refs pytest-dev#15072
Fixes pytest-dev#14720
Previously, non-native traceback styles fell back to Python's stdlib
`TracebackException.format()` whenever an `ExceptionGroup` or
`BaseExceptionGroup` appeared anywhere in the exception chain (pytest-dev#9159).
That workaround bypassed pytest's traceback formatting—losing `E   `
error prefixes, statement range extraction, locals inspection, and
style-specific rendering (`long`, `short`, `auto`, `no`, `value`)—and
required special-case filtering on live `TracebackException` instances.

With `ExceptionRecord.children` capturing sub-exceptions as part of the
recursive exception model, `PytestTraceback` now renders exception
groups directly in the active pytest traceback style:

- Recursively format each sub-exception (including its own cause/context
  chain and nested subgroups) using the configured pytest style, boxed
  with tree prefixes (`| `) and numbered group separators matching
  stdlib header widths.
- Keep full `ExceptionGroup: <msg> (N sub-exception[s])` headers on
  group frames while preserving `tryshort` crash summaries for leaf
  exceptions.
- Preserve the outermost exception's `reprtraceback` and `reprcrash` on
  `ExceptionChainRepr` when converting or deserializing exception group
  records.
- Delegate `ExceptionInfoFormatter.repr_excinfo()` to `PytestTraceback`
  and remove the `pytest-dev#9159` `TracebackException` fallback and
  `_filter_tracebackexception` helper.

Part of pytest-dev#15072 (Step 5).
…tured failures (Step 6 of pytest-dev#15072)

Add _pytest._code.matching with check_type/check_regex primitives and plain-data MatchFailure objects that render to the legacy text or a rich Tree. ExceptionInfo.match, group_contains and RaisesExc/RaisesGroup now use them; fail_reason is derived from the structured failure.

Add regression tests for pytest-dev#12175 (striptext captured in the record).
…5072)

- Use co_positions() range in Python >= 3.11 when slicing statement range, keeping getstatementrange_ast fallback for Python 3.10 and dynamic frames without line positions.
- Capture exact line and column ranges in FrameRecord eagerly at capture time.
- Update PytestTraceback to render PEP 657 carets across long, short, auto, and native styles, and underline error spans in rich style.
- Strip trailing newlines and handle multibyte UTF-8 offsets in _highlight_arrows_for_line.
- Add comprehensive TestPep657Carets suite in testing/code/test_excinfo.py covering position capture, serialization round-trip, all styles, multibyte UTF-8, multiline expressions, and Python 3.10 position-free fallback.
… classes and getrepr() (Step 8 of pytest-dev#15072)

- Export ExceptionRecord, FrameRecord, Location, SyntaxErrorDetails, FailureRecord,
  and PytestTraceback in top-level pytest namespace (__all__).
- Add autoclass documentation for data model classes in doc/en/reference/reference.rst.
- Deprecate ExceptionInfo.getrepr() and its formatting parameters in favor of
  ExceptionInfo.record() and PytestTraceback.
- Deprecate legacy Repr* classes (TerminalRepr, ExceptionRepr, ExceptionChainRepr,
  ReprExceptionInfo, ReprTraceback, ReprTracebackNative, ReprEntryNative, ReprEntry,
  ReprFileLocation, ReprLocals, ReprFuncArgs) and compatibility properties on
  ExceptionRecord (reprcrash, reprtraceback, chain).
- Document deprecations and migration guides in doc/en/deprecations.rst.
- Update internal callers to pass _warn_deprecated=False or _ispytest=True to avoid
  spurious deprecation warnings during test execution.
- Add comprehensive deprecation and public API tests in testing/code/test_record.py.
@pyba-chronographer pyba-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Nov 3, 2026
@RonnyPfannschmidt

Copy link
Copy Markdown
Member

just no - this is a deep topic with missing discussion and everything here doesnt even tryto understand/mess around with it -

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants