8000 [3.13] gh-118894: Make asyncio REPL use pyrepl (GH-119433) (#119884) · python/cpython@a5272e6 · GitHub
[go: up one dir, main page]

Skip to content

Commit a5272e6

Browse files
miss-islingtonambv
andauthored
[3.13] gh-118894: Make asyncio REPL use pyrepl (GH-119433) (#119884)
(cherry picked from commit 2237946) Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 67ac191 commit a5272e6

File tree

7 files changed

+143
-65
lines changed

7 files changed

+143
-65
lines changed

Lib/_pyrepl/commands.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@ def do(self) -> None:
219219
os.kill(os.getpid(), signal.SIGINT)
220220

221221

222+
class ctrl_c(Command):
223+
def do(self) -> None:
224+
raise KeyboardInterrupt
225+
226+
222227
class suspend(Command):
223228
def do(self) -> None:
224229
import signal

Lib/_pyrepl/console.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919

2020
from __future__ import annotations
2121

22-
import sys
22+
import _colorize # type: ignore[import-not-found]
2323

2424
from abc import ABC, abstractmethod
25+
import ast
26+
import code
2527
from dataclasses import dataclass, field
28+
import os.path
29+
import sys
2630

2731

2832
TYPE_CHECKING = False
@@ -136,3 +140,54 @@ def wait(self) -> None:
136140

137141
@abstractmethod
138142
def repaint(self) -> None: ...
143+
144+
145+
class InteractiveColoredConsole(code.InteractiveConsole):
146+
def __init__(
147+
self,
148+
locals: dict[str, object] | None = None,
149+
filename: str = "<console>",
150+
*,
151+
local_exit: bool = False,
152+
) -> None:
153+
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
154+
self.can_colorize = _colorize.can_colorize()
155+
156+
def showsyntaxerror(self, filename=None):
157+
super().showsyntaxerror(colorize=self.can_colorize)
158+
159+
def showtraceback(self):
160+
super().showtraceback(colorize=self.can_colorize)
161+
162+
def runsource(self, source, filename="<input>", symbol="single"):
163+
try:
164+
tree = ast.parse(source)
165+
except (SyntaxError, OverflowError, ValueError):
166+
self.showsyntaxerror(filename)
167+
return False
168+
if tree.body:
169+
*_, last_stmt = tree.body
170+
for stmt in tree.body:
171+
wrapper = ast.Interactive if stmt is last_stmt else ast.Module
172+
the_symbol = symbol if stmt is last_stmt else "exec"
173+
item = wrapper([stmt])
174+
try:
175+
code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True)
176+
except SyntaxError as e:
177+
if e.args[0] == "'await' outside function":
178+
python = os.path.basename(sys.executable)
179+
e.add_note(
180+
f"Try the asyncio REPL ({python} -m asyncio) to use"
181+
f" top-level 'await' and run background asyncio tasks."
182+
)
183+
self.showsyntaxerror(filename)
184+
return False
185+
except (OverflowError, ValueError):
186+
self.showsyntaxerror(filename)
187+
return False
188+
189+
if code is None:
190+
return True
191+
192+
self.runcode(code)
193+
return False

Lib/_pyrepl/reader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def make_default_commands() -> dict[CommandName, type[Command]]:
131131
("\\\\", "self-insert"),
132132
(r"\x1b[200~", "enable_bracketed_paste"),
133133
(r"\x1b[201~", "disable_bracketed_paste"),
134+
(r"\x03", "ctrl-c"),
134135
]
135136
+ [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"]
136137
+ [(c, "self-insert") for c in map(chr, range(128, 256)) if c.isalpha()]

Lib/_pyrepl/simple_interact.py

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,13 @@
2525

2626
from __future__ import annotations
2727

28-
import _colorize # type: ignore[import-not-found]
2928
import _sitebuiltins
3029
import linecache
3130
import sys
3231
import code
33-
import ast
3432
from types import ModuleType
3533

34+
from .console import InteractiveColoredConsole
3635
from .readline import _get_reader, multiline_input
3736

3837
_error: tuple[type[Exception], ...] | type[Exception]
@@ -74,57 +73,21 @@ def _clear_screen():
7473
"clear": _clear_screen,
7574
}
7675

77-
class InteractiveColoredConsole(code.InteractiveConsole):
78-
def __init__(
79-
self,
80-
locals: dict[str, object] | None = None,
81-
filename: str = "<console>",
82-
*,
83-
local_exit: bool = False,
84-
) -> None:
85-
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
86-
self.can_colorize = _colorize.can_colorize()
87-
88-
def showsyntaxerror(self, filename=None):
89-
super().showsyntaxerror(colorize=self.can_colorize)
90-
91-
def showtraceback(self):
92-
super().showtraceback(colorize=self.can_colorize)
93-
94-
def runsource(self, source, filename="<input>", symbol="single"):
95-
try:
96-
tree = ast.parse(source)
97-
except (OverflowError, SyntaxError, ValueError):
98-
self.showsyntaxerror(filename)
99-
return False
100-
if tree.body:
101-
*_, last_stmt = tree.body
102-
for stmt in tree.body:
103-
wrapper = ast.Interactive if stmt is last_stmt else ast.Module
104-
the_symbol = symbol if stmt is last_stmt else "exec"
105-
item = wrapper([stmt])
106-
try:
107-
code = compile(item, filename, the_symbol, dont_inherit=True)
108-
except (OverflowError, ValueError, SyntaxError):
109-
self.showsyntaxerror(filename)
110-
return False
111-
112-
if code is None:
113-
return True
114-
115-
self.runcode(code)
116-
return False
117-
11876

11977
def run_multiline_interactive_console(
120-
mainmodule: ModuleType | None= None, future_flags: int = 0
78+
mainmodule: ModuleType | None = None,
79+
future_flags: int = 0,
80+
console: code.InteractiveConsole | None = None,
12181
) -> None:
12282
import __main__
12383
from .readline import _setup
12484
_setup()
12585

12686
mainmodule = mainmodule or __main__
127-
console = InteractiveColoredConsole(mainmodule.__dict__, filename="<stdin>")
87+
if console is None:
88+
console = InteractiveColoredConsole(
89+
mainmodule.__dict__, filename="<stdin>"
90+
)
12891
if future_flags:
12992
console.compile.compiler.flags |= future_flags
13093

Lib/asyncio/__main__.py

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,49 @@
11
import ast
22
import asyncio
3-
import code
43
import concurrent.futures
54
import inspect
5+
import os
66
import site
77
import sys
88
import threading
99
import types
1010
import warnings
1111

12+
from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found]
13+
from _pyrepl.console import InteractiveColoredConsole
14+
1215
from . import futures
1316

1417

15-
class AsyncIOInteractiveConsole(code.InteractiveConsole):
18+
class AsyncIOInteractiveConsole(InteractiveColoredConsole):
1619

1720
def __init__(self, locals, loop):
18-
super().__init__(locals)
21+
super().__init__(locals, filename="<stdin>")
1922
self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
2023

2124
self.loop = loop
2225

2326
def runcode(self, code):
27+
global return_code
2428
future = concurrent.futures.Future()
2529

2630
def callback():
31+
global return_code
2732
global repl_future
28-
global repl_future_interrupted
33+
global keyboard_interrupted
2934

3035
repl_future = None
31-
repl_future_interrupted = False
36+
keyboard_interrupted = False
3237

3338
func = types.FunctionType(code, self.locals)
3439
try:
3540
coro = func()
36-
except SystemExit:
37-
raise
41+
except SystemExit as se:
42+
return_code = se.code
43+
self.loop.stop()
44+
return
3845
except KeyboardInterrupt as ex:
39-
repl_future_interrupted = True
46+
keyboard_interrupted = True
4047
future.set_exception(ex)
4148
return
4249
except BaseException as ex:
@@ -57,10 +64,12 @@ def callback():
5764

5865
try:
5966
return future.result()
60-
except SystemExit:
61-
raise
67+
except SystemExit as se:
68+
return_code = se.code
69+
self.loop.stop()
70+
return
6271
except BaseException:
63-
if repl_future_interrupted:
72+
if keyboard_interrupted:
6473
self.write("\nKeyboardInterrupt\n")
6574
else:
6675
self.showtraceback()
@@ -69,18 +78,56 @@ def callback():
6978
class REPLThread(threading.Thread):
7079

7180
def run(self):
81+
global return_code
82+
7283
try:
7384
banner = (
7485
f'asyncio REPL {sys.version} on {sys.platform}\n'
7586
f'Use "await" directly instead of "asyncio.run()".\n'
7687
f'Type "help", "copyright", "credits" or "license" '
7788
f'for more information.\n'
78-
f'{getattr(sys, "ps1", ">>> ")}import asyncio'
7989
)
8090

81-
console.interact(
82-
banner=banner,
83-
exitmsg='exiting asyncio REPL...')
91+
console.write(banner)
92+
93+
if startup_path := os.getenv("PYTHONSTARTUP"):
94+
import tokenize
95+
with tokenize.open(startup_path) as f:
96+
startup_code = compile(f.read(), startup_path, "exec")
97+
exec(startup_code, console.locals)
98+
99+
ps1 = getattr(sys, "ps1", ">>> ")
100+
if can_colorize():
101+
ps1 = f"{ANSIColors.BOLD_MAGENTA}{ps1}{ANSIColors.RESET}"
102+
console.write(f"{ps1}import asyncio\n")
103+
104+
try:
105+
import errno
106+
if os.getenv("PYTHON_BASIC_REPL"):
107+
raise RuntimeError("user environment requested basic REPL")
108+
if not os.isatty(sys.stdin.fileno()):
109+
raise OSError(errno.ENOTTY, "tty required", "stdin")
110+
111+
# This import will fail on operating systems with no termios.
112+
from _pyrepl.simple_interact import (
113+
check,
114+
run_multiline_interactive_console,
115+
)
116+
if err := check():
117+
raise RuntimeError(err)
118+
except Exception as e:
119+
console.interact(banner="", exitmsg=exit_message)
120+
else:
121+
try:
122+
run_multiline_interactive_console(console=console)
123+
except SystemExit:
124+
# expected via the `exit` and `quit` commands
125+
pass
126+
except BaseException:
127+
# unexpected issue
128+
console.showtraceback()
129+
console.write("Internal error, ")
130+
return_code = 1
84131
finally:
85132
warnings.filterwarnings(
86133
'ignore',
@@ -91,6 +138,9 @@ def run(self):
91138

92139

93140
if __name__ == '__main__':
141+
CAN_USE_PYREPL = True
142+
143+
return_code = 0
94144
loop = asyncio.new_event_loop()
95145
asyncio.set_event_loop(loop)
96146

@@ -103,7 +153,7 @@ def run(self):
103153
console = AsyncIOInteractiveConsole(repl_locals, loop)
104154

105155
repl_future = None
106-
repl_future_interrupted = False
156+
keyboard_interrupted = False
107157

108158
try:
109159
import readline # NoQA
@@ -126,17 +176,20 @@ def run(self):
126176
completer = rlcompleter.Completer(console.locals)
127177
readline.set_completer(completer.complete)
128178

129-
repl_thread = REPLThread()
179+
repl_thread = REPLThread(name="Interactive thread")
130180
repl_thread.daemon = True
131181
repl_thread.start()
132182

133183
while True:
134184
try:
135185
loop.run_forever()
136186
except KeyboardInterrupt:
187+
keyboard_interrupted = True
137188
if repl_future and not repl_future.done():
138189
repl_future.cancel()
139-
repl_future_interrupted = True
140190
continue
141191
else:
142192
break
193+
194+
console.write('exiting asyncio REPL...\n')
195+
sys.exit(return_code)

Lib/test/test_pyrepl/test_interact.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from test.support import force_not_colorized
88

9-
from _pyrepl.simple_interact import InteractiveColoredConsole
9+
from _pyrepl.console import InteractiveColoredConsole
1010

1111

1212
class TestSimpleInteract(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:mod:`asyncio` REPL now has the same capabilities as PyREPL.

0 commit comments

Comments
 (0)
0