[BUG] Traceback rendering fails on ParseError with a non-string message

#4225 · open · 4 comments

View on GitHub ↗

saitakarcesme

- [x] Checked the traceback documentation and searched open/closed issues for `SyntaxError`, `ParseError`, and `ExpatError`. - [x] Checked the FAQ. **Describe the bug** Rendering an `xml.etree.ElementTree.ParseError` whose message is an `ExpatError` raises a secondary `TypeError`, masking the original XML parse error. This occurs with the pure-Python ElementTree parser used by defusedxml (see tiran/defusedxml#105). The reproduction below needs only Rich and the standard library: ```python import io import sys from xml.etree.ElementTree import ParseError from xml.parsers.expat import ExpatError from rich.console import Console from rich.traceback import Traceback try: raise ParseError(ExpatError("syntax error")) except ParseError: Console(file=io.StringIO()).print(Traceback.from_exception(*sys.exc_info())) ``` Actual result: ```text TypeError: str or Text instance required, not ExpatError('syntax error') ``` Expected: render the original exception without raising a new exception. `ParseError` subclasses `SyntaxError`. `Traceback.extract()` currently stores `exc_value.msg` directly in `_SyntaxError`, although it already has a `safe_str()` helper for the main exception value. Would normalizing this field with that helper, with tests for non-string messages and a failing `__str__`, be an acceptable fix? **Platform** macOS arm64, CPython 3.13.5, Rich 15.0.0. Also reproduced using the current `main` checkout. Output is directed to `StringIO`, so no terminal-specific rendering is involved. The original end-to-end example also fails with defusedxml 0.8.0rc2 and structlog 26.1.0. Explicitly selecting `structlog.dev.plain_traceback` is a verified workaround for that integration. This report and local reproduction were prepared with OpenAI Codex. Per the AI policy, no PR is being submitted without maintainer approval of the proposed solution.

Comments

5h4d0wn1k

"Confirmed and reproduced locally (rich 15.0.0, CPython 3.14): Console().print(Traceback.from_exception(*sys.exc_info())) under ParseError(ExpatError(...)) raises TypeError: str or Text instance required, not ExpatError(\"syntax error\"). Root cause is a bit narrower than the title: the SyntaxError branch stores the raw message at rich/traceback.py:517 (msg=exc_value.msg, unnormalized), and render_stack feeds that field into ReprHighlighter at traceback.py:687, which only accepts str/Text. Normalizing at extraction is sufficient and minimal: msg=safe_str(exc_value.msg), safe_str already exists and is used for exc_value at line 486; for a standard SyntaxError the message is already a str, so this is a no-op. I also tested the adjacent sub-cases: an embedded-newlines message renders fine (no crash), while None and other non-str messages crash exactly as above. Per AI_POLICY.md an AI-assisted PR needs a maintainer to approve the solution on the issue first. Maintainers, would you be open to this one-line fix with a regression test alongside test_syntax_error in tests/test_traceback.py? Happy to submit once approved. @willmcgugan (Note: this reproduction/analysis was prepared with AI assistance)"

alexprengere

For the record I encountered the bug using PyPy on this MRE: ```python import xml.etree.ElementTree as etree from rich.console import Console console = Console() try: xml = etree.XML("<") except Exception as e: console.print_exception() # TypeError: str or Text instance required, # not ExpatError('unclosed token: line 1, column 0') ``` The reason is that in PyPy, the pure-Python version `lxml` sets the first argument of `ParseError` to a non-str, while in CPython, the C version of `lxml` casts it to a `str`. I opened a [CPython issue](https://github.com/python/cpython/issues/158166) to fix this upstream (as PyPy use CPython stdlib), but I still believe rich should not fail here, as you generally do not want your error path to error itself.

alexprengere

I pushed a fix in my fork [here](https://github.com/alexprengere/rich/commit/86f430f0189eea6e208730378593eec3ea9e4125), but I cannot create a PR as I think those are limited to collaborators.

EDfwy1

Hi! I ran into this bug and dug into it. Root cause found, along with a minimal fix and a regression test — ready on my fork if a PR would be welcome. ## Root cause In `rich/traceback.py`, the `Traceback._render_stack()` handler builds a `_SyntaxError` from any `SyntaxError` subclass: ```python stack.syntax_error = _SyntaxError( ... msg=exc_value.msg, ... ) ``` But `SyntaxError.msg` is **not guaranteed to be a `str`**. `xml.etree.ElementTree.ParseError` (and `xml.parsers.expat.ExpatError`) carry exception-specific `msg` values — when a `ParseError` is constructed from an `ExpatError` instance, `msg` is the `ExpatError` object itself, not a string. Rich's `highlighter()` requires `str`, so rendering crashes with: ``` TypeError: str or Text instance required, not ExpatError('syntax error') ``` ## Minimal repro ```python from xml.etree.ElementTree import ParseError from xml.parsers.expat import ExpatError from rich.console import Console console = Console() try: raise ParseError(ExpatError("syntax error")) except ParseError: console.print_exception() # TypeError ``` ## Fix (one line) Coerce the message to `str` in `rich/traceback.py`: ```diff - msg=exc_value.msg, + msg=str(exc_value.msg), ``` ## Regression test Added `tests/test_traceback.py::test_syntax_error_non_str_message` (raises a `ParseError` wrapping an `ExpatError`, prints the exception, asserts `"ParseError"` appears in the output). Passes locally; the traceback test suite is green with the change. ## Ready-to-merge branch Since PR creation on this repo is currently limited to collaborators, the complete fix (fix + regression test + CHANGELOG entry) is on my fork: **Branch:** [`EDfwy1:fix/traceback-non-str-syntax-message`](https://github.com/EDfwy1/rich/tree/fix/traceback-non-str-syntax-message) (commit `90a4936`) If a maintainer could either open the PR from that branch, or grant me PR access, I'd be happy to submit it properly. Thanks!