Skip to content

Commit 06ccec4

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

File tree

4 files changed

+122
-9
lines changed

4 files changed

+122
-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: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
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
16+
from test.support import force_not_colorized
1417

1518
# Optionally test pyrepl. This currently requires that the
1619
# 'curses' resource be given on the regrtest command line using the -u
@@ -1002,5 +1005,88 @@ def test_up_arrow_after_ctrl_r(self):
10021005
self.assert_screen_equals(reader, "")
10031006

10041007

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