From: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:11:45 +0000 (+0100) Subject: [3.13] gh-128231: Use `runcode()` return value for failing early (GH-129488) (#130513) X-Git-Tag: v3.13.3~72 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8f6a9aa6aeb4fbbac186b7cb284f6301af5cbe36;p=thirdparty%2FPython%2Fcpython.git [3.13] gh-128231: Use `runcode()` return value for failing early (GH-129488) (#130513) gh-128231: Use `runcode()` return value for failing early (GH-129488) (cherry picked from commit 7ed3dc6392613832f66c63507385b1da109cbf21) Co-authored-by: Bartosz Sławecki --- diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 3c1ca6f68265..8956fb1242e5 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -153,6 +153,8 @@ class Console(ABC): class InteractiveColoredConsole(code.InteractiveConsole): + STATEMENT_FAILED = object() + def __init__( self, locals: dict[str, object] | None = None, @@ -174,6 +176,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( @@ -211,5 +223,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 95c636f9e028..69f5a30cfe50 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 8b941b93670e..2651cf7d32d7 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 fdaff5b99bb8..1e4b48622b93 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 000000000000..a70b6a1fc14d --- /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.