Skip to content

Commit d3b3db3

Browse files
committed
Convert async_printing example to asyncio
Updated 'examples/async_printing.py' to use 'asyncio' tasks and 'create_background_task' instead of 'threading'. Added 'pre_prompt' hook to 'cmd2.Cmd' and integrated it into 'read_input' to support starting background tasks when the prompt loop starts.
1 parent 478035f commit d3b3db3

2 files changed

Lines changed: 63 additions & 42 deletions

File tree

cmd2/cmd2.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3285,6 +3285,9 @@ def completedefault(self, *_ignored: list[str]) -> list[str]:
32853285
def _suggest_similar_command(self, command: str) -> str | None:
32863286
return suggest_similar(command, self.get_visible_commands())
32873287

3288+
def pre_prompt(self) -> None:
3289+
"""Call this before the prompt is displayed (and after the event loop has started)."""
3290+
32883291
def read_input(
32893292
self,
32903293
prompt: str = '',
@@ -3378,13 +3381,15 @@ def get_prompt() -> ANSI | str:
33783381
return temp_session1.prompt(
33793382
prompt_to_use,
33803383
completer=completer_to_use,
3384+
pre_run=self.pre_prompt,
33813385
bottom_toolbar=self._bottom_toolbar if self.include_bottom_toolbar else None,
33823386
)
33833387

33843388
# history is None
33853389
return self.session.prompt(
33863390
prompt_to_use,
33873391
completer=completer_to_use,
3392+
pre_run=self.pre_prompt,
33883393
bottom_toolbar=self._bottom_toolbar if self.include_bottom_toolbar else None,
33893394
)
33903395

@@ -3399,6 +3404,7 @@ def get_prompt() -> ANSI | str:
33993404
)
34003405
line = temp_session2.prompt(
34013406
prompt,
3407+
pre_run=self.pre_prompt,
34023408
bottom_toolbar=self._bottom_toolbar if self.include_bottom_toolbar else None,
34033409
)
34043410
if len(line) == 0:
@@ -3413,6 +3419,7 @@ def get_prompt() -> ANSI | str:
34133419
complete_while_typing=self.session.complete_while_typing,
34143420
)
34153421
line = temp_session3.prompt(
3422+
pre_run=self.pre_prompt,
34163423
bottom_toolbar=self._bottom_toolbar if self.include_bottom_toolbar else None,
34173424
)
34183425
if len(line) == 0:
@@ -5467,10 +5474,7 @@ def async_alert(self, alert_msg: str, new_prompt: str | None = None) -> None:
54675474
:raises RuntimeError: if called from the main thread.
54685475
:raises RuntimeError: if main thread is not currently at the prompt.
54695476
"""
5470-
if threading.current_thread() is threading.main_thread():
5471-
raise RuntimeError("async_alert should not be called from the main thread")
5472-
5473-
# Check if the main thread is currently waiting at the prompt
5477+
# Check if prompt is currently displayed and waiting for user input
54745478
if not self._in_prompt:
54755479
raise RuntimeError("Main thread is not at the prompt")
54765480

examples/async_printing.py

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
and changes the window title.
44
"""
55

6+
import asyncio
7+
import contextlib
68
import random
7-
import threading
89
import time
910

1011
import cmd2
@@ -36,45 +37,57 @@ def __init__(self, *args, **kwargs) -> None:
3637

3738
self.prompt = "(APR)> "
3839

39-
# The thread that will asynchronously alert the user of events
40-
self._stop_event = threading.Event()
41-
self._alerter_thread = threading.Thread()
40+
# The task that will asynchronously alert the user of events
41+
self._alerter_task: asyncio.Task | None = None
42+
self._alerts_enabled = True
4243
self._alert_count = 0
4344
self._next_alert_time = 0
4445

45-
# Create some hooks to handle the starting and stopping of our thread
46-
self.register_preloop_hook(self._preloop_hook)
46+
# Register hook to stop alerts when the command loop finishes
4747
self.register_postloop_hook(self._postloop_hook)
4848

49-
def _preloop_hook(self) -> None:
50-
"""Start the alerter thread."""
51-
self._stop_event.clear()
52-
53-
self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func)
54-
self._alerter_thread.start()
49+
def pre_prompt(self) -> None:
50+
"""Start the alerter task if enabled.
51+
This is called after the prompt event loop has started, so create_background_task works.
52+
"""
53+
if self._alerts_enabled:
54+
self._start_alerter_task()
5555

5656
def _postloop_hook(self) -> None:
57-
"""Stops the alerter thread."""
58-
self._stop_event.set()
59-
if self._alerter_thread.is_alive():
60-
self._alerter_thread.join()
57+
"""Stops the alerter task."""
58+
self._cancel_alerter_task()
6159

6260
def do_start_alerts(self, _) -> None:
63-
"""Starts the alerter thread."""
64-
if self._alerter_thread.is_alive():
65-
print("The alert thread is already started")
61+
"""Starts the alerter task."""
62+
if self._alerts_enabled:
63+
print("The alert task is already started")
6664
else:
67-
self._stop_event.clear()
68-
self._alerter_thread = threading.Thread(name='alerter', target=self._alerter_thread_func)
69-
self._alerter_thread.start()
65+
self._alerts_enabled = True
66+
# Task will be started in pre_prompt at next prompt
7067

7168
def do_stop_alerts(self, _) -> None:
72-
"""Stops the alerter thread."""
73-
self._stop_event.set()
74-
if self._alerter_thread.is_alive():
75-
self._alerter_thread.join()
69+
"""Stops the alerter task."""
70+
if not self._alerts_enabled:
71+
print("The alert task is already stopped")
7672
else:
77-
print("The alert thread is already stopped")
73+
self._alerts_enabled = False
74+
self._cancel_alerter_task()
75+
76+
def _start_alerter_task(self) -> None:
77+
"""Start the alerter task if it's not running."""
78+
if self._alerter_task is not None and not self._alerter_task.done():
79+
return
80+
81+
# self.session.app is the prompt_toolkit Application.
82+
# create_background_task creates a task that runs on the same loop as the app.
83+
with contextlib.suppress(RuntimeError):
84+
self._alerter_task = self.session.app.create_background_task(self._alerter())
85+
86+
def _cancel_alerter_task(self) -> None:
87+
"""Cancel the alerter task."""
88+
if self._alerter_task is not None:
89+
self._alerter_task.cancel()
90+
self._alerter_task = None
7891

7992
def _get_alerts(self) -> list[str]:
8093
"""Reports alerts
@@ -147,13 +160,13 @@ def _generate_colored_prompt(self) -> str:
147160

148161
return stylize(self.visible_prompt, style=status_color)
149162

150-
def _alerter_thread_func(self) -> None:
163+
async def _alerter(self) -> None:
151164
"""Prints alerts and updates the prompt any time the prompt is showing."""
152165
self._alert_count = 0
153166
self._next_alert_time = 0
154167

155-
while not self._stop_event.is_set():
156-
try:
168+
try:
169+
while True:
157170
# Get any alerts that need to be printed
158171
alert_str = self._generate_alert_str()
159172

@@ -162,22 +175,26 @@ def _alerter_thread_func(self) -> None:
162175

163176
# Check if we have alerts to print
164177
if alert_str:
165-
# new_prompt is an optional parameter to async_alert()
166-
self.async_alert(alert_str, new_prompt)
178+
# We are running on the main loop, so we can print directly.
179+
# patch_stdout (active during read_input) handles the output.
180+
print(alert_str)
181+
182+
self.prompt = new_prompt
167183
new_title = f"Alerts Printed: {self._alert_count}"
168184
self.set_window_title(new_title)
185+
self.session.app.invalidate()
169186

170187
# Otherwise check if the prompt needs to be updated or refreshed
171188
elif self.prompt != new_prompt:
172-
self.async_update_prompt(new_prompt)
189+
self.prompt = new_prompt
190+
self.session.app.invalidate()
173191

174192
elif self.need_prompt_refresh():
175-
self.async_refresh_prompt()
176-
177-
except RuntimeError:
178-
pass
193+
self.session.app.invalidate()
179194

180-
self._stop_event.wait(0.5)
195+
await asyncio.sleep(0.5)
196+
except asyncio.CancelledError:
197+
pass
181198

182199

183200
if __name__ == '__main__':

0 commit comments

Comments
 (0)