]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-118893: Evaluate all statements in the new REPL separately (#119318)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Tue, 21 May 2024 23:16:56 +0000 (19:16 -0400)
committerGitHub <noreply@github.com>
Tue, 21 May 2024 23:16:56 +0000 (23:16 +0000)
Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/simple_interact.py
Lib/code.py
Lib/test/test_pyrepl/test_interact.py [new file with mode: 0644]
Lib/test/test_traceback.py
Lib/traceback.py

index 31b2097a78a2268951e4b00b39d65f6ceb5b0907..d65b6d0d62790acf8e10b1c0635752bf21c1c8b7 100644 (file)
@@ -30,6 +30,7 @@ import _sitebuiltins
 import linecache
 import sys
 import code
+import ast
 from types import ModuleType
 
 from .readline import _get_reader, multiline_input
@@ -74,9 +75,36 @@ class InteractiveColoredConsole(code.InteractiveConsole):
         super().__init__(locals=locals, filename=filename, local_exit=local_exit)  # type: ignore[call-arg]
         self.can_colorize = _colorize.can_colorize()
 
+    def showsyntaxerror(self, filename=None):
+        super().showsyntaxerror(colorize=self.can_colorize)
+
     def showtraceback(self):
         super().showtraceback(colorize=self.can_colorize)
 
+    def runsource(self, source, filename="<input>", symbol="single"):
+        try:
+            tree = ast.parse(source)
+        except (OverflowError, SyntaxError, ValueError):
+            self.showsyntaxerror(filename)
+            return False
+        if tree.body:
+            *_, last_stmt = tree.body
+        for stmt in tree.body:
+            wrapper = ast.Interactive if stmt is last_stmt else ast.Module
+            the_symbol = symbol if stmt is last_stmt else "exec"
+            item = wrapper([stmt])
+            try:
+                code = compile(item, filename, the_symbol)
+            except (OverflowError, ValueError):
+                    self.showsyntaxerror(filename)
+                    return False
+
+            if code is None:
+                return True
+
+            self.runcode(code)
+        return False
+
 
 def run_multiline_interactive_console(
     mainmodule: ModuleType | None= None, future_flags: int = 0
@@ -144,10 +172,7 @@ def run_multiline_interactive_console(
 
             input_name = f"<python-input-{input_n}>"
             linecache._register_code(input_name, statement, "<stdin>")  # type: ignore[attr-defined]
-            symbol = "single" if not contains_pasted_code else "exec"
-            more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol)  # type: ignore[call-arg]
-            if contains_pasted_code and more:
-                more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single")  # type: ignore[call-arg]
+            more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single")  # type: ignore[call-arg]
             assert not more
             input_n += 1
         except KeyboardInterrupt:
index 0c2fd2963b211860a1ee1f21f32da0e612c169ff..b93902ccf545b3443cc6cac6c24469c66aad3873 100644 (file)
@@ -94,7 +94,7 @@ class InteractiveInterpreter:
         except:
             self.showtraceback()
 
-    def showsyntaxerror(self, filename=None):
+    def showsyntaxerror(self, filename=None, **kwargs):
         """Display the syntax error that just occurred.
 
         This doesn't display a stack trace because there isn't one.
@@ -106,6 +106,7 @@ class InteractiveInterpreter:
         The output is written by self.write(), below.
 
         """
+        colorize = kwargs.pop('colorize', False)
         type, value, tb = sys.exc_info()
         sys.last_exc = value
         sys.last_type = type
@@ -123,7 +124,7 @@ class InteractiveInterpreter:
                 value = SyntaxError(msg, (filename, lineno, offset, line))
                 sys.last_exc = sys.last_value = value
         if sys.excepthook is sys.__excepthook__:
-            lines = traceback.format_exception_only(type, value)
+            lines = traceback.format_exception_only(type, value, colorize=colorize)
             self.write(''.join(lines))
         else:
             # If someone has set sys.excepthook, we let that take precedence
diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py
new file mode 100644 (file)
index 0000000..b3dc07c
--- /dev/null
@@ -0,0 +1,92 @@
+import contextlib
+import io
+import unittest
+from unittest.mock import patch
+from textwrap import dedent
+
+from test.support import force_not_colorized
+
+from _pyrepl.simple_interact import InteractiveColoredConsole
+
+
+class TestSimpleInteract(unittest.TestCase):
+    def test_multiple_statements(self):
+        namespace = {}
+        code = dedent("""\
+        class A:
+            def foo(self):
+
+
+                pass
+
+        class B:
+            def bar(self):
+                pass
+
+        a = 1
+        a
+        """)
+        console = InteractiveColoredConsole(namespace, filename="<stdin>")
+        with (
+            patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror,
+            patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource,
+        ):
+            more = console.push(code, filename="<stdin>", _symbol="single")  # type: ignore[call-arg]
+        self.assertFalse(more)
+        showsyntaxerror.assert_not_called()
+
+
+    def test_multiple_statements_output(self):
+        namespace = {}
+        code = dedent("""\
+        b = 1
+        b
+        a = 1
+        a
+        """)
+        console = InteractiveColoredConsole(namespace, filename="<stdin>")
+        f = io.StringIO()
+        with contextlib.redirect_stdout(f):
+            more = console.push(code, filename="<stdin>", _symbol="single")  # type: ignore[call-arg]
+        self.assertFalse(more)
+        self.assertEqual(f.getvalue(), "1\n")
+
+    def test_empty(self):
+        namespace = {}
+        code = ""
+        console = InteractiveColoredConsole(namespace, filename="<stdin>")
+        f = io.StringIO()
+        with contextlib.redirect_stdout(f):
+            more = console.push(code, filename="<stdin>", _symbol="single")  # type: ignore[call-arg]
+        self.assertFalse(more)
+        self.assertEqual(f.getvalue(), "")
+
+    def test_runsource_compiles_and_runs_code(self):
+        console = InteractiveColoredConsole()
+        source = "print('Hello, world!')"
+        with patch.object(console, "runcode") as mock_runcode:
+            console.runsource(source)
+            mock_runcode.assert_called_once()
+
+    def test_runsource_returns_false_for_successful_compilation(self):
+        console = InteractiveColoredConsole()
+        source = "print('Hello, world!')"
+        result = console.runsource(source)
+        self.assertFalse(result)
+
+    @force_not_colorized
+    def test_runsource_returns_false_for_failed_compilation(self):
+        console = InteractiveColoredConsole()
+        source = "print('Hello, world!'"
+        f = io.StringIO()
+        with contextlib.redirect_stderr(f):
+            result = console.runsource(source)
+        self.assertFalse(result)
+        self.assertIn('SyntaxError', f.getvalue())
+
+    def test_runsource_shows_syntax_error_for_failed_compilation(self):
+        console = InteractiveColoredConsole()
+        source = "print('Hello, world!'"
+        with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
+            console.runsource(source)
+            mock_showsyntaxerror.assert_called_once()
index 5987ec382e6c8535dffbdd2315359538a9e23c58..5035de114b5e9d3ee4650fe8c7e8f1d820b277ab 100644 (file)
@@ -543,7 +543,7 @@ class TracebackCases(unittest.TestCase):
 
         self.assertEqual(
             str(inspect.signature(traceback.format_exception_only)),
-            '(exc, /, value=<implicit>, *, show_group=False)')
+            '(exc, /, value=<implicit>, *, show_group=False, **kwargs)')
 
 
 class PurePythonExceptionFormattingMixin:
index 9401b461497cc1e54da49fae8b9c95f9acafb5e9..280d92d04cac9b4c61abbb5ecaeddb23071880a1 100644 (file)
@@ -155,7 +155,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
     return list(te.format(chain=chain, colorize=colorize))
 
 
-def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
+def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs):
     """Format the exception part of a traceback.
 
     The return value is a list of strings, each ending in a newline.
@@ -170,10 +170,11 @@ def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
     :exc:`BaseExceptionGroup`, the nested exceptions are included as
     well, recursively, with indentation relative to their nesting depth.
     """
+    colorize = kwargs.get("colorize", False)
     if value is _sentinel:
         value = exc
     te = TracebackException(type(value), value, None, compact=True)
-    return list(te.format_exception_only(show_group=show_group))
+    return list(te.format_exception_only(show_group=show_group, colorize=colorize))
 
 
 # -- not official API but folk probably use these two functions.