]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-128231: Use `runcode()` return value for failing early (GH-129488)
authorBartosz Sławecki <bartoszpiotrslawecki@gmail.com>
Mon, 24 Feb 2025 14:50:13 +0000 (15:50 +0100)
committerGitHub <noreply@github.com>
Mon, 24 Feb 2025 14:50:13 +0000 (15:50 +0100)
Lib/_pyrepl/console.py
Lib/asyncio/__main__.py
Lib/test/test_pyrepl/test_interact.py
Lib/test/test_repl.py
Misc/NEWS.d/next/Library/2025-01-30-22-49-42.gh-issue-128231.SuEC18.rst [new file with mode: 0644]

index 0d78890b4f45d5bcb85216fd7e6a68fa878e0194..db911b3e1f0b919047e8b449b5347085cefc58e1 100644 (file)
@@ -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="<input>", 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
index 662ba649aa08bef774b25e0dbd4c13f7bfbf14f6..e624f7632bedce6addc55874c8318716922e5c8c 100644 (file)
@@ -75,7 +75,7 @@ class AsyncIOInteractiveConsole(InteractiveColoredConsole):
                 self.write("\nKeyboardInterrupt\n")
             else:
                 self.showtraceback()
-
+            return self.STATEMENT_FAILED
 
 class REPLThread(threading.Thread):
 
index e0ee310e2c4dbc66d75937443c2e980cb5700b57..c3204832a6a93a0b89db1dd16429057602f7bc76 100644 (file)
@@ -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 = ""
index 356ff5b198d637943d5001c4c24f7c02c88316fb..cb7b1938871657fea5bdf7ca85a174682db693b0 100644 (file)
@@ -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 (file)
index 0000000..a70b6a1
--- /dev/null
@@ -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.