Skip to content

Commit aaa535d

Browse files
committed
Preserve SyntaxError attributes when formatting interpreter errors
1 parent 4a21ac0 commit aaa535d

2 files changed

Lines changed: 57 additions & 7 deletions

File tree

‎bpython/repl.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,11 @@ def showsyntaxerror(self, filename: str | None = None, **kwargs) -> None:
161161
exc_type, value, sys.last_traceback = sys.exc_info()
162162
sys.last_type = exc_type
163163
sys.last_value = value
164-
if filename and exc_type is SyntaxError and value is not None:
165-
msg = value.args[0]
166-
args = list(value.args[1])
167-
# strip linechache line number
164+
if filename and isinstance(value, SyntaxError):
165+
# Hide the linecache identifier without rebuilding the exception:
166+
# compiler errors can have location attributes but only one arg.
168167
if self.bpython_input_re.match(filename):
169-
args[0] = "<input>"
170-
value = SyntaxError(msg, tuple(args))
171-
sys.last_value = value
168+
value.filename = "<input>"
172169
exc_formatted = traceback.format_exception_only(exc_type, value)
173170
self.writetb(exc_formatted)
174171

‎bpython/test/test_interpreter.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import unittest
2+
import sys
3+
from unittest.mock import Mock
24

35
from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain
46

@@ -34,6 +36,57 @@ def test_syntaxerror(self):
3436
self.assertMultiLineEqual(str(plain("").join(a)), str(expected))
3537
self.assertEqual(plain("").join(a), expected)
3638

39+
def test_semantic_syntaxerrors_leave_interpreter_usable(self):
40+
for source in ("nonlocal a", "return", "break", "continue"):
41+
with self.subTest(source=source):
42+
i = Interpreter()
43+
callback = Mock()
44+
i.syntaxerror_callback = callback
45+
self.assertFalse(i.runsource(source))
46+
callback.assert_called_once_with()
47+
output = str(plain("").join(i.a))
48+
self.assertIn("SyntaxError", output)
49+
self.assertIn('"<input>"', output)
50+
self.assertNotIn("<bpython-input-", output)
51+
self.assertIsInstance(sys.last_value, SyntaxError)
52+
self.assertEqual(sys.last_value.filename, "<input>")
53+
self.assertFalse(i.runsource("answer = 6 * 7"))
54+
self.assertEqual(i.locals["answer"], 42)
55+
56+
def test_syntaxerror_keeps_original_exception_and_details(self):
57+
i = Interpreter()
58+
error = SyntaxError("message")
59+
error.filename = "<bpython-input-123>"
60+
error.lineno = 3
61+
error.offset = 2
62+
error.end_lineno = 3
63+
error.end_offset = 5
64+
error.text = "bad source"
65+
error.add_note("additional information")
66+
try:
67+
raise error
68+
except SyntaxError:
69+
i.showsyntaxerror(error.filename)
70+
self.assertIs(sys.last_value, error)
71+
self.assertEqual(error.filename, "<input>")
72+
self.assertEqual((error.lineno, error.offset), (3, 2))
73+
self.assertEqual((error.end_lineno, error.end_offset), (3, 5))
74+
self.assertIn("additional information", str(plain("").join(i.a)))
75+
76+
def test_explicit_syntaxerror_filename(self):
77+
i = Interpreter()
78+
i.runsource("1.1.1.1", filename="example.py")
79+
self.assertIn('"example.py"', str(plain("").join(i.a)))
80+
self.assertEqual(sys.last_value.filename, "example.py")
81+
82+
def test_indentationerror_filename(self):
83+
i = Interpreter()
84+
i.runsource(" answer = 42")
85+
output = str(plain("").join(i.a))
86+
self.assertIn("IndentationError", output)
87+
self.assertIn('"<input>"', output)
88+
self.assertIsInstance(sys.last_value, IndentationError)
89+
3790
def test_traceback(self):
3891
i = Interpreter()
3992

0 commit comments

Comments
 (0)