Conversation
Co-authored-by: Claude <noreply@anthropic.com>
37b18b6 to
427a995
Compare
…/SemTiOne/pytest into fix-2388-syntax-error-crash-line
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
|
Hello @RonnyPfannschmidt, could you please review this PR when you have a moment? Thank you for your time. |
| repr_ = excinfo.getrepr(style="short") | ||
| reprcrash = excinfo._getreprcrash() | ||
| msg = str(repr_) | ||
| if reprcrash is not None and reprcrash.column is not None: |
There was a problem hiding this comment.
Why the reprcrash.column guard? If not None, we call str(reprcrash) anyway.
There was a problem hiding this comment.
You are right. str(reprcrash) is safe with column=None, but the guard isn't about safety. Dropping the guard here would append a near-duplicate location line in the no-column case. The column is the signal that we have the SyntaxError's own precise location worth appending, without it we keep the existing traceback line. What do you think?
There was a problem hiding this comment.
We should make the intent more explicit then, preferably without just slapping a comment there... 🤔
There was a problem hiding this comment.
Of course, here's my code proposal, wdyt?
msg = str(repr_)
syntax_error_has_precise_location = reprcrash is not None and reprcrash.column is not None
if syntax_error_has_precise_location:
msg += "\n" + str(reprcrash) | f() | ||
| assert excinfo._getreprcrash() is None | ||
|
|
||
| def test_getreprcrash_syntax_error(self): |
There was a problem hiding this comment.
Most of the tests are constructing a SyntexError manually... can we construct them instead via actual code and exec()? Seems more resilient/reliable that way.
There was a problem hiding this comment.
Yes I agree. But manual tests here have some purposes. One, they pin the exact file:line:col format. Two, without_filename is unreachable here via compile() which will raise TypeError before the parsing starts, also for without_lineno, it has no known practicable path that produces it either, they test the defensive fallback logic in _syntax_error_location. And yes, without_offset can be produced via exec() (null byte), but I think it's an obscure trigger, because that ties to a CPython implementation detail without adding resilience over the manual construction. So I suggest... that we combine it, exec() is for happy-path but keep the manual tests. How about that?
| assert repr.reprcrash.message == "SyntaxError: bad syntax" | ||
| assert str(repr.reprcrash) == "file.py:1:5: SyntaxError: bad syntax" | ||
|
|
||
| def test_syntax_error_default_tb_long(self, pytester: Pytester) -> None: |
There was a problem hiding this comment.
Why are we raising manual SyntaxErrors here, instead of just writing code with syntax errors?
There was a problem hiding this comment.
So that it stays a runtime failure pinned to file.py:1:5. Broken code either collects it differently or can't be relied on to give offset 5.
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
|
Thanks for picking this up — the The Reproducer (validated against the current head, 4d8030b, and against # test_pr14899_repro.py
def helper():
compile("def broken(:", "gen.py", "exec")
def test_issue1_wrong_frame_attribution():
helper()
def test_issue3_chained_cause_link():
try:
compile("def broken(:", "gen.py", "exec")
except SyntaxError as e:
raise RuntimeError("wrapper") from e# test_issue2_broken_syntax.py (a real collection-time SyntaxError)
def broken(:
pass# test_issue7_import_no_offset.py (import-time SyntaxError without offset)
raise SyntaxError("custom import error", ("weird.py", 3, None, "src", 3, None))Issue 1 — frame attribution (
|
|
Thanks, these are valid and will be addressed. But for issue 3, I changed the guard to Not |
|
@SemTiOne Would you be happy for me to take the missing-offset case as a separate follow-up after this PR lands? The proposed scope is to retain a SyntaxError's filename and line when offset is None, append that location during collection, and leave traceback frame locations unchanged. I'll wait for confirmation before preparing a separate patch. |
|
Sure, feel free to open it. I agree with the proposed scope. |
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
@nicoddemus @RonnyPfannschmidt PTAL when you have a moment. 🙏 |
RonnyPfannschmidt
left a comment
There was a problem hiding this comment.
I prompted an AI agent (Claude, via Claude Code) to re-check this PR against my earlier review; the verification runs, the reproducers and the prose below are its work. I have read it and I am approving on the strength of it. — Ronny
Every blocking point from my earlier review is fixed at head (7489cf7). Dropping the repr_traceback_entry rewrite was the right call — the diff is now just three things, and each of them earns its place.
Re-verified in a clean worktree, Python 3.14.3:
- Frame attribution —
--tb=shortgivestest_repro.py:2: in helper, the real frame. The chained-cause section is correct too. - Collection — entries keep their real frames (
ast.py:46: in parse), with one appended crash line at the end instead of three copies of a fused location. - Zero offset — handled by the
exc.offsettruthiness check, pinned by a test. msg=None— rendersSyntaxError: <no detail available>.filename or path— gone with the rewrite.
The win, measured against main on a real file:
main: test_relpath.py:6: File "sub/mod.py", line 1
PR: sub/mod.py:1:12: SyntaxError: invalid syntax
The short summary line improves identically, which is the part I care most about — FAILED ... - File "sub/mod.py", line 1 was actively misleading. --tb=native is untouched.
testing/code/ test_reports.py test_collection.py test_terminal.py test_junitxml.py → 784 passed, 14 skipped, 1 xfailed. pre-commit run -a clean, mypy included.
The missing-offset case (weird.py:3: with no column) is still absent; that is a non-regression — main prints nothing there either — and @user01010111 has it as an agreed follow-up.
Approving. The inline comments are nits only; take them or leave them, none of them should hold this up.
Co-authored-by: Claude <noreply@anthropic.com>
|
@RonnyPfannschmidt All nits addressed except nit 2 & 4. |
SyntaxError crash lines now use the error's own file, line, and column instead of the location of the traceback entry where it was raised.
Before:
After:
Collection errors append the same
file:line:colcrash line when precise location is available. TheReprFileLocationdataclass gains an optionalcolumnfield (column: int | None = None); existing output is unchanged when the field is absent.Note: This PR doesn't solve the removal of the now-redundant File..., line N block. This is a deliberate design decision; it comes from stdlib
format_exception_only(); deferred to a follow-up issue/discussion.Co-authored-bycommit trailers.changelogdirectory, with a name like<ISSUE NUMBER>.<TYPE>.rst. See changelog/README.rst for details.AUTHORSin alphabetical order.