diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py
index 0d78890b4f45d5..db911b3e1f0b91 100644
--- a/Lib/_pyrepl/console.py
+++ b/Lib/_pyrepl/console.py
@@ -152,6 +152,8 @@ def repaint(self) -> None: ...
class InteractiveColoredConsole(code.InteractiveConsole):
+ STATEMENT_FAILED = object()
+
def __init__(
self,
locals: dict[str, object] | None = None,
@@ -173,6 +175,16 @@ def _excepthook(self, typ, value, tb):
limit=traceback.BUILTIN_EXCEPTION_LIMIT)
self.write(''.join(lines))
+ def runcode(self, code):
+ try:
+ exec(code, self.locals)
+ except SystemExit:
+ raise
+ except BaseException:
+ self.showtraceback()
+ return self.STATEMENT_FAILED
+ return None
+
def runsource(self, source, filename="", symbol="single"):
try:
tree = self.compile.compiler(
@@ -209,5 +221,7 @@ def runsource(self, source, filename="", symbol="single"):
if code is None:
return True
- self.runcode(code)
+ result = self.runcode(code)
+ if result is self.STATEMENT_FAILED:
+ break
return False
diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py
index 662ba649aa08be..e624f7632bedce 100644
--- a/Lib/asyncio/__main__.py
+++ b/Lib/asyncio/__main__.py
@@ -75,7 +75,7 @@ def callback():
self.write("\nKeyboardInterrupt\n")
else:
self.showtraceback()
-
+ return self.STATEMENT_FAILED
class REPLThread(threading.Thread):
diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py
index e0ee310e2c4dbc..c3204832a6a93a 100644
--- a/Lib/test/test_pyrepl/test_interact.py
+++ b/Lib/test/test_pyrepl/test_interact.py
@@ -53,6 +53,19 @@ def test_multiple_statements_output(self):
self.assertFalse(more)
self.assertEqual(f.getvalue(), "1\n")
+ @force_not_colorized
+ def test_multiple_statements_fail_early(self):
+ console = InteractiveColoredConsole()
+ code = dedent("""\
+ raise Exception('foobar')
+ print('spam&eggs')
+ """)
+ f = io.StringIO()
+ with contextlib.redirect_stderr(f):
+ console.runsource(code)
+ self.assertIn('Exception: foobar', f.getvalue())
+ self.assertNotIn('spam&eggs', f.getvalue())
+
def test_empty(self):
namespace = {}
code = ""
diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py
index 356ff5b198d637..cb7b1938871657 100644
--- a/Lib/test/test_repl.py
+++ b/Lib/test/test_repl.py
@@ -294,7 +294,15 @@ def f():
self.assertEqual(traceback_lines, expected_lines)
-class TestAsyncioREPLContextVars(unittest.TestCase):
+class TestAsyncioREPL(unittest.TestCase):
+ def test_multiple_statements_fail_early(self):
+ user_input = "1 / 0; print('afterwards')"
+ p = spawn_repl("-m", "asyncio")
+ p.stdin.write(user_input)
+ output = kill_python(p)
+ self.assertIn("ZeroDivisionError", output)
+ self.assertNotIn("afterwards", output)
+
def test_toplevel_contextvars_sync(self):
user_input = dedent("""\
from contextvars import ContextVar
diff --git a/Misc/NEWS.d/next/Library/2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst b/Misc/NEWS.d/next/Library/2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst
new file mode 100644
index 00000000000000..a70b6a1fc14d63
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst
@@ -0,0 +1,2 @@
+Execution of multiple statements in the new REPL now stops immediately upon
+the first exception encountered. Patch by Bartosz Sławecki.