Skip to content

Commit 40df02e

Browse files
committed
gh-118893: Evaluate all statements in the new REPL separately
1 parent c4f9823 commit 40df02e

File tree

4 files changed

+121
-9
lines changed

4 files changed

+121
-9
lines changed

Lib/_pyrepl/simple_interact.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import linecache
3131
import sys
3232
import code
33+
import ast
3334
from types import ModuleType
3435

3536
from .readline import _get_reader, multiline_input
@@ -74,9 +75,36 @@ def __init__(
7475
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
7576
self.can_colorize = _colorize.can_colorize()
7677

78+
def showsyntaxerror(self, filename=None):
79+
super().showsyntaxerror(colorize=self.can_colorize)
80+
7781
def showtraceback(self):
7882
super().showtraceback(colorize=self.can_colorize)
7983

84+
def runsource(self, source, filename="<input>", symbol="single"):
85+
try:
86+
tree = ast.parse(source)
87+
except (OverflowError, SyntaxError, ValueError):
88+
self.showsyntaxerror(filename)
89+
return False
90+
if tree.body:
91+
*_, last_stmt = tree.body
92+
for stmt in tree.body:
93+
wrapper = ast.Interactive if stmt is last_stmt else ast.Module
94+
the_symbol = symbol if stmt is last_stmt else "exec"
95+
item = wrapper([stmt])
96+
try:
97+
code = compile(item, filename, the_symbol)
98+
except (OverflowError, ValueError):
99+
self.showsyntaxerror(filename)
100+
return False
101+
102+
if code is None:
103+
return True
104+
105+
self.runcode(code)
106+
return False
107+
80108

81109
def run_multiline_interactive_console(
82110
mainmodule: ModuleType | None= None, future_flags: int = 0
@@ -144,10 +172,7 @@ def more_lines(unicodetext: str) -> bool:
144172

145173
input_name = f"<python-input-{input_n}>"
146174
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
147-
symbol = "single" if not contains_pasted_code else "exec"
148-
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol=symbol) # type: ignore[call-arg]
149-
if contains_pasted_code and more:
150-
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
175+
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
151176
assert not more
152177
input_n += 1
153178
except KeyboardInterrupt:

Lib/code.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def runcode(self, code):
9494
except:
9595
self.showtraceback()
9696

97-
def showsyntaxerror(self, filename=None):
97+
def showsyntaxerror(self, filename=None, **kwargs):
9898
"""Display the syntax error that just occurred.
9999
100100
This doesn't display a stack trace because there isn't one.
@@ -106,6 +106,7 @@ def showsyntaxerror(self, filename=None):
106106
The output is written by self.write(), below.
107107
108108
"""
109+
colorize = kwargs.pop('colorize', False)
109110
type, value, tb = sys.exc_info()
110111
sys.last_exc = value
111112
sys.last_type = type
@@ -123,7 +124,7 @@ def showsyntaxerror(self, filename=None):
123124
value = SyntaxError(msg, (filename, lineno, offset, line))
124125
sys.last_exc = sys.last_value = value
125126
if sys.excepthook is sys.__excepthook__:
126-
lines = traceback.format_exception_only(type, value)
127+
lines = traceback.format_exception_only(type, value, colorize=colorize)
127128
self.write(''.join(lines))
128129
else:
129130
# If someone has set sys.excepthook, we let that take precedence

Lib/test/test_pyrepl.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import itertools
22
import os
33
import rlcompleter
4-
import sys
54
import tempfile
65
import unittest
76
from code import InteractiveConsole
87
from functools import partial
98
from unittest import TestCase
109
from unittest.mock import MagicMock, patch
10+
from textwrap import dedent
11+
import contextlib
12+
import io
1113

1214
from test.support import requires
1315
from test.support.import_helper import import_module
@@ -1002,5 +1004,88 @@ def test_up_arrow_after_ctrl_r(self):
10021004
self.assert_screen_equals(reader, "")
10031005

10041006

1007+
class TestSimpleInteract(unittest.TestCase):
1008+
def test_multiple_statements(self):
1009+
namespace = {}
1010+
code = dedent("""\
1011+
class A:
1012+
def foo(self):
1013+
1014+
1015+
pass
1016+
1017+
class B:
1018+
def bar(self):
1019+
pass
1020+
1021+
a = 1
1022+
a
1023+
""")
1024+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
1025+
with (
1026+
patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror,
1027+
patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource,
1028+
):
1029+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
1030+
self.assertFalse(more)
1031+
showsyntaxerror.assert_not_called()
1032+
1033+
1034+
def test_multiple_statements_output(self):
1035+
namespace = {}
1036+
code = dedent("""\
1037+
b = 1
1038+
b
1039+
a = 1
1040+
a
1041+
""")
1042+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
1043+
f = io.StringIO()
1044+
with contextlib.redirect_stdout(f):
1045+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
1046+
self.assertFalse(more)
1047+
self.assertEqual(f.getvalue(), "1\n")
1048+
1049+
def test_empty(self):
1050+
namespace = {}
1051+
code = ""
1052+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
1053+
f = io.StringIO()
1054+
with contextlib.redirect_stdout(f):
1055+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
1056+
self.assertFalse(more)
1057+
self.assertEqual(f.getvalue(), "")
1058+
1059+
def test_runsource_compiles_and_runs_code(self):
1060+
console = InteractiveColoredConsole()
1061+
source = "print('Hello, world!')"
1062+
with patch.object(console, "runcode") as mock_runcode:
1063+
console.runsource(source)
1064+
mock_runcode.assert_called_once()
1065+
1066+
def test_runsource_returns_false_for_successful_compilation(self):
1067+
console = InteractiveColoredConsole()
1068+
source = "print('Hello, world!')"
1069+
result = console.runsource(source)
1070+
self.assertFalse(result)
1071+
1072+
def test_runsource_returns_false_for_failed_compilation(self):
1073+
console = InteractiveColoredConsole()
1074+
source = "print('Hello, world!'"
1075+
result = console.runsource(source)
1076+
self.assertFalse(result)
1077+
1078+
def test_runsource_shows_syntax_error_for_failed_compilation(self):
1079+
console = InteractiveColoredConsole()
1080+
source = "print('Hello, world!'"
1081+
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
1082+
console.runsource(source)
1083+
mock_showsyntaxerror.assert_called_once()
1084+
1085+
1086+
if __name__ == '__main__':
1087+
unittest.main()
1088+
1089+
10051090
if __name__ == '__main__':
10061091
unittest.main()

Lib/traceback.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
155155
return list(te.format(chain=chain, colorize=colorize))
156156

157157

158-
def format_exception_only(exc, /, value=_sentinel, *, show_group=False):
158+
def format_exception_only(exc, /, value=_sentinel, *, show_group=False, **kwargs):
159159
"""Format the exception part of a traceback.
160160
161161
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):
170170
:exc:`BaseExceptionGroup`, the nested exceptions are included as
171171
well, recursively, with indentation relative to their nesting depth.
172172
"""
173+
colorize = kwargs.get("colorize", False)
173174
if value is _sentinel:
174175
value = exc
175176
te = TracebackException(type(value), value, None, compact=True)
176-
return list(te.format_exception_only(show_group=show_group))
177+
return list(te.format_exception_only(show_group=show_group, colorize=colorize))
177178

178179

179180
# -- not official API but folk probably use these two functions.

0 commit comments

Comments
 (0)