From 4d4ce3de7cc3f21b4bf001225b9dade23b7b6da4 Mon Sep 17 00:00:00 2001 From: bswck Date: Thu, 30 Jan 2025 23:55:27 +0100 Subject: [PATCH 1/2] Use `STATEMENT_FAILED` and in `InteractiveColoredConsole` only --- Lib/_pyrepl/console.py | 16 +++++++++++++++- Lib/asyncio/__main__.py | 2 +- Lib/test/test_pyrepl/test_interact.py | 13 +++++++++++++ Lib/test/test_repl.py | 10 +++++++++- ...025-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 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..af5d4d0e67632a 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', sep='&') + """) + 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. From 71c4a5fbd80a930a6c610a3d89d19be54ab084cf Mon Sep 17 00:00:00 2001 From: bswck Date: Fri, 31 Jan 2025 00:03:01 +0100 Subject: [PATCH 2/2] Simplify the `test_multiple_statements_fail_early` test --- Lib/test/test_pyrepl/test_interact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index af5d4d0e67632a..c3204832a6a93a 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -58,7 +58,7 @@ def test_multiple_statements_fail_early(self): console = InteractiveColoredConsole() code = dedent("""\ raise Exception('foobar') - print('spam', 'eggs', sep='&') + print('spam&eggs') """) f = io.StringIO() with contextlib.redirect_stderr(f):