From 7ed3dc6392613832f66c63507385b1da109cbf21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 24 Feb 2025 15:50:13 +0100 Subject: gh-128231: Use `runcode()` return value for failing early (GH-129488) --- Lib/_pyrepl/console.py | 16 +++++++++++++++- Lib/asyncio/__main__.py | 2 +- Lib/test/test_pyrepl/test_interact.py | 13 +++++++++++++ Lib/test/test_repl.py | 10 +++++++++- .../2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst | 2 ++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 0d78890..db911b3 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -152,6 +152,8 @@ class Console(ABC): class InteractiveColoredConsole(code.InteractiveConsole): + STATEMENT_FAILED = object() + def __init__( self, locals: dict[str, object] | None = None, @@ -173,6 +175,16 @@ class InteractiveColoredConsole(code.InteractiveConsole): 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 @@ class InteractiveColoredConsole(code.InteractiveConsole): 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 662ba64..e624f76 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -75,7 +75,7 @@ class AsyncIOInteractiveConsole(InteractiveColoredConsole): 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 e0ee310..c320483 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -53,6 +53,19 @@ class TestSimpleInteract(unittest.TestCase): 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 356ff5b..cb7b193 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -294,7 +294,15 @@ class TestInteractiveModeSyntaxErrors(unittest.TestCase): 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 0000000..a70b6a1 --- /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. -- cgit v0.12