Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-118894: Make asyncio REPL use pyrepl #119433

Merged
merged 9 commits into from
May 31, 2024
5 changes: 5 additions & 0 deletions Lib/_pyrepl/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ def do(self) -> None:
os.kill(os.getpid(), signal.SIGINT)


class ctrl_c(Command):
def do(self) -> None:
raise KeyboardInterrupt


class suspend(Command):
def do(self) -> None:
import signal
Expand Down
57 changes: 56 additions & 1 deletion Lib/_pyrepl/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@

from __future__ import annotations

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

from abc import ABC, abstractmethod
import ast
import code
from dataclasses import dataclass, field
import os.path
import sys


TYPE_CHECKING = False
Expand Down Expand Up @@ -136,3 +140,54 @@ def wait(self) -> None:

@abstractmethod
def repaint(self) -> None: ...


class InteractiveColoredConsole(code.InteractiveConsole):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this here so it can be imported even when _pyrepl.readline doesn't import (which is imported by _pyrepl.simple_interact).

def __init__(
self,
locals: dict[str, object] | None = None,
filename: str = "<console>",
*,
local_exit: bool = False,
) -> None:
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 (SyntaxError, OverflowError, 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 = self.compile.compiler(item, filename, the_symbol, dont_inherit=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually want to use our console's compiler because then we can set custom flags, like top-level await.

except SyntaxError as e:
if e.args[0] == "'await' outside function":
python = os.path.basename(sys.executable)
e.add_note(
f"Try the asyncio REPL ({python} -m asyncio) to use"
f" top-level 'await' and run background asyncio tasks."
)
Comment on lines +179 to +182
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python REPL, now with ads.

self.showsyntaxerror(filename)
return False
except (OverflowError, ValueError):
self.showsyntaxerror(filename)
return False

if code is None:
return True

self.runcode(code)
return False
1 change: 1 addition & 0 deletions Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def make_default_commands() -> dict[CommandName, type[Command]]:
("\\\\", "self-insert"),
(r"\x1b[200~", "enable_bracketed_paste"),
(r"\x1b[201~", "disable_bracketed_paste"),
(r"\x03", "ctrl-c"),
]
+ [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"]
+ [(c, "self-insert") for c in map(chr, range(128, 256)) if c.isalpha()]
Expand Down
53 changes: 8 additions & 45 deletions Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@

from __future__ import annotations

import _colorize # type: ignore[import-not-found]
import _sitebuiltins
import linecache
import sys
import code
import ast
from types import ModuleType

from .console import InteractiveColoredConsole
from .readline import _get_reader, multiline_input

_error: tuple[type[Exception], ...] | type[Exception]
Expand Down Expand Up @@ -74,57 +73,21 @@ def _clear_screen():
"clear": _clear_screen,
}

class InteractiveColoredConsole(code.InteractiveConsole):
def __init__(
self,
locals: dict[str, object] | None = None,
filename: str = "<console>",
*,
local_exit: bool = False,
) -> None:
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, dont_inherit=True)
except (OverflowError, ValueError, SyntaxError):
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
mainmodule: ModuleType | None = None,
future_flags: int = 0,
console: code.InteractiveConsole | None = None,
) -> None:
import __main__
from .readline import _setup
_setup()

mainmodule = mainmodule or __main__
console = InteractiveColoredConsole(mainmodule.__dict__, filename="<stdin>")
if console is None:
console = InteractiveColoredConsole(
mainmodule.__dict__, filename="<stdin>"
)
if future_flags:
console.compile.compiler.flags |= future_flags

Expand Down
89 changes: 71 additions & 18 deletions Lib/asyncio/__main__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,49 @@
import ast
import asyncio
import code
import concurrent.futures
import inspect
import os
import site
import sys
import threading
import types
import warnings

from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found]
from _pyrepl.console import InteractiveColoredConsole

from . import futures


class AsyncIOInteractiveConsole(code.InteractiveConsole):
class AsyncIOInteractiveConsole(InteractiveColoredConsole):

def __init__(self, locals, loop):
super().__init__(locals)
super().__init__(locals, filename="<stdin>")
self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT

self.loop = loop

def runcode(self, code):
global return_code
future = concurrent.futures.Future()

def callback():
global return_code
global repl_future
global repl_future_interrupted
global keyboard_interrupted

repl_future = None
repl_future_interrupted = False
keyboard_interrupted = False

func = types.FunctionType(code, self.locals)
try:
coro = func()
except SystemExit:
raise
except SystemExit as se:
return_code = se.code
self.loop.stop()
return
except KeyboardInterrupt as ex:
repl_future_interrupted = True
keyboard_interrupted = True
future.set_exception(ex)
return
except BaseException as ex:
Expand All @@ -57,10 +64,12 @@ def callback():

try:
return future.result()
except SystemExit:
raise
except SystemExit as se:
return_code = se.code
self.loop.stop()
return
except BaseException:
if repl_future_interrupted:
if keyboard_interrupted:
self.write("\nKeyboardInterrupt\n")
else:
self.showtraceback()
Expand All @@ -69,18 +78,56 @@ def callback():
class REPLThread(threading.Thread):

def run(self):
global return_code

try:
banner = (
f'asyncio REPL {sys.version} on {sys.platform}\n'
f'Use "await" directly instead of "asyncio.run()".\n'
f'Type "help", "copyright", "credits" or "license" '
f'for more information.\n'
f'{getattr(sys, "ps1", ">>> ")}import asyncio'
ambv marked this conversation as resolved.
Show resolved Hide resolved
)

console.interact(
banner=banner,
exitmsg='exiting asyncio REPL...')
console.write(banner)

if startup_path := os.getenv("PYTHONSTARTUP"):
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code, console.locals)

ps1 = getattr(sys, "ps1", ">>> ")
if can_colorize():
ps1 = f"{ANSIColors.BOLD_MAGENTA}{ps1}{ANSIColors.RESET}"
console.write(f"{ps1}import asyncio\n")

try:
import errno
if os.getenv("PYTHON_BASIC_REPL"):
raise RuntimeError("user environment requested basic REPL")
if not os.isatty(sys.stdin.fileno()):
raise OSError(errno.ENOTTY, "tty required", "stdin")

# This import will fail on operating systems with no termios.
from _pyrepl.simple_interact import (
check,
run_multiline_interactive_console,
)
if err := check():
raise RuntimeError(err)
except Exception as e:
console.interact(banner="", exitmsg=exit_message)
else:
try:
run_multiline_interactive_console(console=console)
except SystemExit:
# expected via the `exit` and `quit` commands
pass
except BaseException:
# unexpected issue
console.showtraceback()
ambv marked this conversation as resolved.
Show resolved Hide resolved
console.write("Internal error, ")
return_code = 1
finally:
warnings.filterwarnings(
'ignore',
Expand All @@ -91,6 +138,9 @@ def run(self):


if __name__ == '__main__':
CAN_USE_PYREPL = True

return_code = 0
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

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

repl_future = None
repl_future_interrupted = False
keyboard_interrupted = False

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

repl_thread = REPLThread()
repl_thread = REPLThread(name="Interactive thread")
repl_thread.daemon = True
repl_thread.start()

while True:
try:
loop.run_forever()
except KeyboardInterrupt:
keyboard_interrupted = True
if repl_future and not repl_future.done():
repl_future.cancel()
repl_future_interrupted = True
continue
else:
break

console.write('exiting asyncio REPL...\n')
sys.exit(return_code)
2 changes: 1 addition & 1 deletion Lib/test/test_pyrepl/test_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from test.support import force_not_colorized

from _pyrepl.simple_interact import InteractiveColoredConsole
from _pyrepl.console import InteractiveColoredConsole


class TestSimpleInteract(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:mod:`asyncio` REPL now has the same capabilities as PyREPL.
Loading