Skip to content

Commit f019301

Browse files
Many asyncio REPL improvements.
- Added `--asyncio` flag to the `ptpython` entry point to activate the asyncio-REPL. This will ensure that an event loop is created at the start in which we can run top-level await statements. - Use `get_running_loop()` instead of `get_event_loop()`. - Better handling of `SystemExit` and control-c in the async REPL.
1 parent 6801f94 commit f019301

File tree

4 files changed

+70
-25
lines changed

4 files changed

+70
-25
lines changed

ptpython/contrib/asyncssh_repl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def data_received(self, data: AnyStr, datatype: int | None) -> None:
110110
"""
111111
When data is received, send to inputstream of the CLI and repaint.
112112
"""
113-
self._input_pipe.send(data)
113+
self._input_pipe.send(data) # type: ignore
114114

115115
def _print(
116116
self, *data: object, sep: str = " ", end: str = "\n", file: Any = None

ptpython/entry_points/run_ptpython.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
-h, --help show this help message and exit
1010
--vi Enable Vi key bindings
1111
-i, --interactive Start interactive shell after executing this file.
12+
--asyncio Run an asyncio event loop to support top-level "await".
1213
--light-bg Run on a light background (use dark colors for text).
1314
--dark-bg Run on a dark background (use light colors for text).
1415
--config-file CONFIG_FILE
@@ -24,6 +25,7 @@
2425
from __future__ import annotations
2526

2627
import argparse
28+
import asyncio
2729
import os
2830
import pathlib
2931
import sys
@@ -68,6 +70,11 @@ def create_parser() -> _Parser:
6870
action="store_true",
6971
help="Start interactive shell after executing this file.",
7072
)
73+
parser.add_argument(
74+
"--asyncio",
75+
action="store_true",
76+
help='Run an asyncio event loop to support top-level "await".',
77+
)
7178
parser.add_argument(
7279
"--light-bg",
7380
action="store_true",
@@ -206,16 +213,20 @@ def configure(repl: PythonRepl) -> None:
206213

207214
import __main__
208215

209-
embed(
216+
embed_result = embed( # type: ignore
210217
vi_mode=a.vi,
211218
history_filename=history_file,
212219
configure=configure,
213220
locals=__main__.__dict__,
214221
globals=__main__.__dict__,
215222
startup_paths=startup_paths,
216223
title="Python REPL (ptpython)",
224+
return_asyncio_coroutine=a.asyncio,
217225
)
218226

227+
if a.asyncio:
228+
asyncio.run(embed_result)
229+
219230

220231
if __name__ == "__main__":
221232
run()

ptpython/python_input.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55
from __future__ import annotations
66

7-
from asyncio import get_event_loop
7+
from asyncio import get_running_loop
88
from functools import partial
99
from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Mapping, TypeVar, Union
1010

@@ -1010,7 +1010,7 @@ def get_signatures_in_executor(document: Document) -> list[Signature]:
10101010
app = self.app
10111011

10121012
async def on_timeout_task() -> None:
1013-
loop = get_event_loop()
1013+
loop = get_running_loop()
10141014

10151015
# Never run multiple get-signature threads.
10161016
if self._get_signatures_thread_running:

ptpython/repl.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import asyncio
1313
import builtins
1414
import os
15+
import signal
1516
import sys
1617
import traceback
1718
import types
@@ -158,27 +159,58 @@ def run(self) -> None:
158159
clear_title()
159160
self._remove_from_namespace()
160161

161-
async def run_and_show_expression_async(self, text: str) -> object:
162-
loop = asyncio.get_event_loop()
162+
async def run_and_show_expression_async(self, text: str) -> Any:
163+
loop = asyncio.get_running_loop()
164+
system_exit: SystemExit | None = None
163165

164166
try:
165-
result = await self.eval_async(text)
166-
except KeyboardInterrupt: # KeyboardInterrupt doesn't inherit from Exception.
167-
raise
168-
except SystemExit:
169-
return
170-
except BaseException as e:
171-
self._handle_exception(e)
172-
else:
173-
# Print.
174-
if result is not None:
175-
await loop.run_in_executor(None, lambda: self._show_result(result))
167+
try:
168+
# Create `eval` task. Ensure that control-c will cancel this
169+
# task.
170+
async def eval() -> Any:
171+
nonlocal system_exit
172+
try:
173+
return await self.eval_async(text)
174+
except SystemExit as e:
175+
# Don't propagate SystemExit in `create_task()`. That
176+
# will kill the event loop. We want to handle it
177+
# gracefully.
178+
system_exit = e
179+
180+
task = asyncio.create_task(eval())
181+
loop.add_signal_handler(signal.SIGINT, lambda *_: task.cancel())
182+
result = await task
183+
184+
if system_exit is not None:
185+
raise system_exit
186+
except KeyboardInterrupt:
187+
# KeyboardInterrupt doesn't inherit from Exception.
188+
raise
189+
except SystemExit:
190+
raise
191+
except BaseException as e:
192+
self._handle_exception(e)
193+
else:
194+
# Print.
195+
if result is not None:
196+
await loop.run_in_executor(None, lambda: self._show_result(result))
176197

177-
# Loop.
178-
self.current_statement_index += 1
179-
self.signatures = []
180-
# Return the result for future consumers.
181-
return result
198+
# Loop.
199+
self.current_statement_index += 1
200+
self.signatures = []
201+
# Return the result for future consumers.
202+
return result
203+
finally:
204+
loop.remove_signal_handler(signal.SIGINT)
205+
206+
except KeyboardInterrupt as e:
207+
# Handle all possible `KeyboardInterrupt` errors. This can
208+
# happen during the `eval`, but also during the
209+
# `show_result` if something takes too long.
210+
# (Try/catch is around the whole block, because we want to
211+
# prevent that a Control-C keypress terminates the REPL in
212+
# any case.)
213+
self._handle_keyboard_interrupt(e)
182214

183215
async def run_async(self) -> None:
184216
"""
@@ -192,7 +224,7 @@ async def run_async(self) -> None:
192224
(Both for control-C to work, as well as for the code to see the right
193225
thread in which it was embedded).
194226
"""
195-
loop = asyncio.get_event_loop()
227+
loop = asyncio.get_running_loop()
196228

197229
if self.terminal_title:
198230
set_title(self.terminal_title)
@@ -222,6 +254,8 @@ async def run_async(self) -> None:
222254
# `KeyboardInterrupt` exceptions can end up in the event
223255
# loop selector.
224256
self._handle_keyboard_interrupt(e)
257+
except SystemExit:
258+
return
225259
finally:
226260
if self.terminal_title:
227261
clear_title()
@@ -250,7 +284,7 @@ def eval(self, line: str) -> object:
250284
result = eval(code, self.get_globals(), self.get_locals())
251285

252286
if _has_coroutine_flag(code):
253-
result = asyncio.get_event_loop().run_until_complete(result)
287+
result = asyncio.get_running_loop().run_until_complete(result)
254288

255289
self._store_eval_result(result)
256290
return result
@@ -263,7 +297,7 @@ def eval(self, line: str) -> object:
263297
result = eval(code, self.get_globals(), self.get_locals())
264298

265299
if _has_coroutine_flag(code):
266-
result = asyncio.get_event_loop().run_until_complete(result)
300+
result = asyncio.get_running_loop().run_until_complete(result)
267301

268302
return None
269303

0 commit comments

Comments
 (0)