Skip to content

Commit 7ed3dc6

Browse files
authored
gh-128231: Use runcode() return value for failing early (GH-129488)
1 parent 9f25c1f commit 7ed3dc6

File tree

5 files changed

+40
-3
lines changed

5 files changed

+40
-3
lines changed

Lib/_pyrepl/console.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ def repaint(self) -> None: ...
152152

153153

154154
class InteractiveColoredConsole(code.InteractiveConsole):
155+
STATEMENT_FAILED = object()
156+
155157
def __init__(
156158
self,
157159
locals: dict[str, object] | None = None,
@@ -173,6 +175,16 @@ def _excepthook(self, typ, value, tb):
173175
limit=traceback.BUILTIN_EXCEPTION_LIMIT)
174176
self.write(''.join(lines))
175177

178+
def runcode(self, code):
179+
try:
180+
exec(code, self.locals)
181+
except SystemExit:
182+
raise
183+
except BaseException:
184+
self.showtraceback()
185+
return self.STATEMENT_FAILED
186+
return None
187+
176188
def runsource(self, source, filename="<input>", symbol="single"):
177189
try:
178190
tree = self.compile.compiler(
@@ -209,5 +221,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
209221
if code is None:
210222
return True
211223

212-
self.runcode(code)
224+
result = self.runcode(code)
225+
if result is self.STATEMENT_FAILED:
226+
break
213227
return False

Lib/asyncio/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def callback():
7575
self.write("\nKeyboardInterrupt\n")
7676
else:
7777
self.showtraceback()
78-
78+
return self.STATEMENT_FAILED
7979

8080
class REPLThread(threading.Thread):
8181

Lib/test/test_pyrepl/test_interact.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ def test_multiple_statements_output(self):
5353
self.assertFalse(more)
5454
self.assertEqual(f.getvalue(), "1\n")
5555

56+
@force_not_colorized
57+
def test_multiple_statements_fail_early(self):
58+
console = InteractiveColoredConsole()
59+
code = dedent("""\
60+
raise Exception('foobar')
61+
print('spam&eggs')
62+
""")
63+
f = io.StringIO()
64+
with contextlib.redirect_stderr(f):
65+
console.runsource(code)
66+
self.assertIn('Exception: foobar', f.getvalue())
67+
self.assertNotIn('spam&eggs', f.getvalue())
68+
5669
def test_empty(self):
5770
namespace = {}
5871
code = ""

Lib/test/test_repl.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,15 @@ def f():
294294
self.assertEqual(traceback_lines, expected_lines)
295295

296296

297-
class TestAsyncioREPLContextVars(unittest.TestCase):
297+
class TestAsyncioREPL(unittest.TestCase):
298+
def test_multiple_statements_fail_early(self):
299+
user_input = "1 / 0; print('afterwards')"
300+
p = spawn_repl("-m", "asyncio")
301+
p.stdin.write(user_input)
302+
output = kill_python(p)
303+
self.assertIn("ZeroDivisionError", output)
304+
self.assertNotIn("afterwards", output)
305+
298306
def test_toplevel_contextvars_sync(self):
299307
user_input = dedent("""\
300308
from contextvars import ContextVar
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Execution of multiple statements in the new REPL now stops immediately upon
2+
the first exception encountered. Patch by Bartosz Sławecki.

0 commit comments

Comments
 (0)