From aaa535d2c287c56822e6d76ec541f578d685f6e6 Mon Sep 17 00:00:00 2001 From: AC Date: Tue, 13 Oct 2026 10:43:40 +0800 Subject: [PATCH] Preserve SyntaxError attributes when formatting interpreter errors --- bpython/repl.py | 11 +++---- bpython/test/test_interpreter.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 93ce5cbc..85a6e5b1 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -161,14 +161,11 @@ def showsyntaxerror(self, filename: str | None = None, **kwargs) -> None: exc_type, value, sys.last_traceback = sys.exc_info() sys.last_type = exc_type sys.last_value = value - if filename and exc_type is SyntaxError and value is not None: - msg = value.args[0] - args = list(value.args[1]) - # strip linechache line number + if filename and isinstance(value, SyntaxError): + # Hide the linecache identifier without rebuilding the exception: + # compiler errors can have location attributes but only one arg. if self.bpython_input_re.match(filename): - args[0] = "" - value = SyntaxError(msg, tuple(args)) - sys.last_value = value + value.filename = "" exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 3d40d198..668f3aa0 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,4 +1,6 @@ import unittest +import sys +from unittest.mock import Mock from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain @@ -34,6 +36,57 @@ def test_syntaxerror(self): self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) + def test_semantic_syntaxerrors_leave_interpreter_usable(self): + for source in ("nonlocal a", "return", "break", "continue"): + with self.subTest(source=source): + i = Interpreter() + callback = Mock() + i.syntaxerror_callback = callback + self.assertFalse(i.runsource(source)) + callback.assert_called_once_with() + output = str(plain("").join(i.a)) + self.assertIn("SyntaxError", output) + self.assertIn('""', output) + self.assertNotIn("") + self.assertFalse(i.runsource("answer = 6 * 7")) + self.assertEqual(i.locals["answer"], 42) + + def test_syntaxerror_keeps_original_exception_and_details(self): + i = Interpreter() + error = SyntaxError("message") + error.filename = "" + error.lineno = 3 + error.offset = 2 + error.end_lineno = 3 + error.end_offset = 5 + error.text = "bad source" + error.add_note("additional information") + try: + raise error + except SyntaxError: + i.showsyntaxerror(error.filename) + self.assertIs(sys.last_value, error) + self.assertEqual(error.filename, "") + self.assertEqual((error.lineno, error.offset), (3, 2)) + self.assertEqual((error.end_lineno, error.end_offset), (3, 5)) + self.assertIn("additional information", str(plain("").join(i.a))) + + def test_explicit_syntaxerror_filename(self): + i = Interpreter() + i.runsource("1.1.1.1", filename="example.py") + self.assertIn('"example.py"', str(plain("").join(i.a))) + self.assertEqual(sys.last_value.filename, "example.py") + + def test_indentationerror_filename(self): + i = Interpreter() + i.runsource(" answer = 42") + output = str(plain("").join(i.a)) + self.assertIn("IndentationError", output) + self.assertIn('""', output) + self.assertIsInstance(sys.last_value, IndentationError) + def test_traceback(self): i = Interpreter()