From 17e2c05849321ca1701a0d3a8fd86022a4464150 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Tue, 21 May 2019 16:47:08 -0400 Subject: [PATCH 1/6] Implement asyncio REPL (activated via 'python -m asyncio') This makes it easy to play with asyncio APIs with simply using async/await in the REPL. --- Lib/asyncio/__main__.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Lib/asyncio/__main__.py diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py new file mode 100644 index 000000000000000..97c5c04f6fbf7ff --- /dev/null +++ b/Lib/asyncio/__main__.py @@ -0,0 +1,47 @@ +import ast +import asyncio +import code +import inspect +import sys +import types + + +class AsyncIOInteractiveConsole(code.InteractiveConsole): + + def __init__(self, locals=None): + super().__init__(locals) + self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def runcode(self, code): + try: + func = types.FunctionType(code, self.locals) + coro = func() + if inspect.isawaitable(coro): + return self.loop.run_until_complete(coro) + except SystemExit: + raise + except BaseException: + self.showtraceback() + + +if __name__ == '__main__': + console = AsyncIOInteractiveConsole(locals()) + + try: + import readline # NoQA + except ImportError: + pass + + banner = ( + f'asyncio REPL\n\n' + f'Python {sys.version} on {sys.platform}\n' + f'Type "help", "copyright", "credits" or "license" ' + f'for more information.\n' + ) + + console.interact( + banner=banner, + exitmsg='exiting asyncio REPL...') From dc35401366f8a954553773fa25d78c05870faec3 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 23 May 2019 13:46:24 -0400 Subject: [PATCH 2/6] Run REPL in a thread; keep asyncio loop always busy in the main thread --- Lib/asyncio/__main__.py | 87 +++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 97c5c04f6fbf7ff..af47f92cadc8d1a 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -1,47 +1,100 @@ import ast import asyncio import code +import concurrent.futures import inspect import sys +import threading import types +from . import futures + class AsyncIOInteractiveConsole(code.InteractiveConsole): - def __init__(self, locals=None): + def __init__(self, locals, loop): super().__init__(locals) self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) + self.loop = loop def runcode(self, code): - try: + future = concurrent.futures.Future() + + def callback(): + global repl_future + func = types.FunctionType(code, self.locals) - coro = func() - if inspect.isawaitable(coro): - return self.loop.run_until_complete(coro) + try: + coro = func() + except BaseException as ex: + future.set_exception(ex) + return + + if not inspect.iscoroutine(coro): + future.set_result(coro) + return + + try: + repl_future = self.loop.create_task(coro) + futures._chain_future(repl_future, future) + except BaseException as exc: + future.set_exception(exc) + + loop.call_soon_threadsafe(callback) + + try: + return future.result() except SystemExit: raise except BaseException: self.showtraceback() +class REPLThread(threading.Thread): + + def run(self): + try: + banner = ( + f'asyncio REPL {sys.version} on {sys.platform}\n' + f'Type "help", "copyright", "credits" or "license" ' + f'for more information.\n\n' + f'{getattr(sys, "ps1", ">>> ")}import asyncio\n' + ) + + console.interact( + banner=banner, + exitmsg='exiting asyncio REPL...') + finally: + loop.call_soon_threadsafe(loop.stop) + + if __name__ == '__main__': - console = AsyncIOInteractiveConsole(locals()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + repl_locals = {'asyncio': asyncio} + for key in {'__name__', '__package__', + '__loader__', '__spec__', + '__builtins__', '__file__'}: + repl_locals[key] = locals()[key] + + console = AsyncIOInteractiveConsole(repl_locals, loop) + repl_future = None try: import readline # NoQA except ImportError: pass - banner = ( - f'asyncio REPL\n\n' - f'Python {sys.version} on {sys.platform}\n' - f'Type "help", "copyright", "credits" or "license" ' - f'for more information.\n' - ) + REPLThread().start() - console.interact( - banner=banner, - exitmsg='exiting asyncio REPL...') + while True: + try: + loop.run_forever() + except KeyboardInterrupt: + if repl_future and not repl_future.done(): + repl_future.cancel() + continue + else: + break From 7a973fb6ec9a361e9c698c11a8fc0539f75860ae Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 23 May 2019 15:52:30 -0400 Subject: [PATCH 3/6] Better handle ^C; filter pointless warnings on exit; edit banner --- Lib/asyncio/__main__.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index af47f92cadc8d1a..01fa0ecea411907 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -6,6 +6,7 @@ import sys import threading import types +import warnings from . import futures @@ -23,10 +24,20 @@ def runcode(self, code): def callback(): global repl_future + global repl_future_interrupted + + repl_future = None + repl_future_interrupted = False func = types.FunctionType(code, self.locals) try: coro = func() + except SystemExit: + raise + except KeyboardInterrupt as ex: + repl_future_interrupted = True + future.set_exception(ex) + return except BaseException as ex: future.set_exception(ex) return @@ -48,7 +59,10 @@ def callback(): except SystemExit: raise except BaseException: - self.showtraceback() + if repl_future_interrupted: + self.write("\nKeyboardInterrupt\n") + else: + self.showtraceback() class REPLThread(threading.Thread): @@ -56,7 +70,8 @@ class REPLThread(threading.Thread): def run(self): try: banner = ( - f'asyncio REPL {sys.version} on {sys.platform}\n' + f'asyncio REPL {sys.version} on {sys.platform}\n\n' + f'Use "await" directly instead of asyncio.run().\n' f'Type "help", "copyright", "credits" or "license" ' f'for more information.\n\n' f'{getattr(sys, "ps1", ">>> ")}import asyncio\n' @@ -66,6 +81,11 @@ def run(self): banner=banner, exitmsg='exiting asyncio REPL...') finally: + warnings.filterwarnings( + 'ignore', + message=r'^coroutine .* was never awaited$', + category=RuntimeWarning) + loop.call_soon_threadsafe(loop.stop) @@ -80,14 +100,17 @@ def run(self): repl_locals[key] = locals()[key] console = AsyncIOInteractiveConsole(repl_locals, loop) + repl_future = None + repl_future_interrupted = False try: import readline # NoQA except ImportError: pass - REPLThread().start() + repl_thread = REPLThread() + repl_thread.start() while True: try: @@ -95,6 +118,7 @@ def run(self): except KeyboardInterrupt: if repl_future and not repl_future.done(): repl_future.cancel() + repl_future_interrupted = True continue else: break From 3264679496d980ea283e2bf33659715487a5303e Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 23 May 2019 18:57:39 -0400 Subject: [PATCH 4/6] Add NEWS file --- .../NEWS.d/next/Library/2019-05-23-18-57-34.bpo-37028.Vse6Pj.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-05-23-18-57-34.bpo-37028.Vse6Pj.rst diff --git a/Misc/NEWS.d/next/Library/2019-05-23-18-57-34.bpo-37028.Vse6Pj.rst b/Misc/NEWS.d/next/Library/2019-05-23-18-57-34.bpo-37028.Vse6Pj.rst new file mode 100644 index 000000000000000..d9db21fb6f3c960 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-05-23-18-57-34.bpo-37028.Vse6Pj.rst @@ -0,0 +1 @@ +Implement asyncio REPL From 00eb00300d89ed93f4c08b9da805708ddd371a95 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 23 May 2019 18:58:45 -0400 Subject: [PATCH 5/6] nit-fixes --- Lib/asyncio/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 01fa0ecea411907..96fef5a02abae3a 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -71,7 +71,7 @@ def run(self): try: banner = ( f'asyncio REPL {sys.version} on {sys.platform}\n\n' - f'Use "await" directly instead of asyncio.run().\n' + f'Use "await" directly instead of "asyncio.run()".\n' f'Type "help", "copyright", "credits" or "license" ' f'for more information.\n\n' f'{getattr(sys, "ps1", ">>> ")}import asyncio\n' From 284a8b16ff3b03ab59d3bab693794fb4d6714f38 Mon Sep 17 00:00:00 2001 From: Yury Selivanov Date: Thu, 23 May 2019 19:23:46 -0400 Subject: [PATCH 6/6] Cleanup newlines; make input() thread daemon --- Lib/asyncio/__main__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 96fef5a02abae3a..18bb87a5bc4ffd8 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -70,11 +70,11 @@ class REPLThread(threading.Thread): def run(self): try: banner = ( - f'asyncio REPL {sys.version} on {sys.platform}\n\n' + 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\n' - f'{getattr(sys, "ps1", ">>> ")}import asyncio\n' + f'for more information.\n' + f'{getattr(sys, "ps1", ">>> ")}import asyncio' ) console.interact( @@ -110,6 +110,7 @@ def run(self): pass repl_thread = REPLThread() + repl_thread.daemon = True repl_thread.start() while True: