Skip to content

Commit a463cd8

Browse files
miss-islingtonpablogsalambv
authored
[3.13] gh-118893: Evaluate all statements in the new REPL separately (GH-119318) (#119408)
(cherry picked from commit a3e4fec) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent eafd633 commit a463cd8

File tree

5 files changed

+128
-9
lines changed

5 files changed

+128
-9
lines changed

Lib/_pyrepl/simple_interact.py

+29-4
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

+3-2
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/test_interact.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import contextlib
2+
import io
3+
import unittest
4+
from unittest.mock import patch
5+
from textwrap import dedent
6+
7+
from test.support import force_not_colorized
8+
9+
from _pyrepl.simple_interact import InteractiveColoredConsole
10+
11+
12+
class TestSimpleInteract(unittest.TestCase):
13+
def test_multiple_statements(self):
14+
namespace = {}
15+
code = dedent("""\
16+
class A:
17+
def foo(self):
18+
19+
20+
pass
21+
22+
class B:
23+
def bar(self):
24+
pass
25+
26+
a = 1
27+
a
28+
""")
29+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
30+
with (
31+
patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror,
32+
patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource,
33+
):
34+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
35+
self.assertFalse(more)
36+
showsyntaxerror.assert_not_called()
37+
38+
39+
def test_multiple_statements_output(self):
40+
namespace = {}
41+
code = dedent("""\
42+
b = 1
43+
b
44+
a = 1
45+
a
46+
""")
47+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
48+
f = io.StringIO()
49+
with contextlib.redirect_stdout(f):
50+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
51+
self.assertFalse(more)
52+
self.assertEqual(f.getvalue(), "1\n")
53+
54+
def test_empty(self):
55+
namespace = {}
56+
code = ""
57+
console = InteractiveColoredConsole(namespace, filename="<stdin>")
58+
f = io.StringIO()
59+
with contextlib.redirect_stdout(f):
60+
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
61+
self.assertFalse(more)
62+
self.assertEqual(f.getvalue(), "")
63+
64+
def test_runsource_compiles_and_runs_code(self):
65+
console = InteractiveColoredConsole()
66+
source = "print('Hello, world!')"
67+
with patch.object(console, "runcode") as mock_runcode:
68+
console.runsource(source)
69+
mock_runcode.assert_called_once()
70+
71+
def test_runsource_returns_false_for_successful_compilation(self):
72+
console = InteractiveColoredConsole()
73+
source = "print('Hello, world!')"
74+
result = console.runsource(source)
75+
self.assertFalse(result)
76+
77+
@force_not_colorized
78+
def test_runsource_returns_false_for_failed_compilation(self):
79+
console = InteractiveColoredConsole()
80+
source = "print('Hello, world!'"
81+
f = io.StringIO()
82+
with contextlib.redirect_stderr(f):
83+
result = console.runsource(source)
84+
self.assertFalse(result)
85+
self.assertIn('SyntaxError', f.getvalue())
86+
87+
def test_runsource_shows_syntax_error_for_failed_compilation(self):
88+
console = InteractiveColoredConsole()
89+
source = "print('Hello, world!'"
90+
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
91+
console.runsource(source)
92+
mock_showsyntaxerror.assert_called_once()

Lib/test/test_traceback.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ def test_signatures(self):
543543

544544
self.assertEqual(
545545
str(inspect.signature(traceback.format_exception_only)),
546-
'(exc, /, value=<implicit>, *, show_group=False)')
546+
'(exc, /, value=<implicit>, *, show_group=False, **kwargs)')
547547

548548

549549
class PurePythonExceptionFormattingMixin:

Lib/traceback.py

+3-2
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)