3535import inspect
3636import os
3737import pydoc
38- import queue
3938import re
4039import sys
4140import tempfile
4241import threading
4342import time
4443from code import InteractiveConsole
45- from collections import namedtuple
44+ from collections import (
45+ deque ,
46+ namedtuple ,
47+ )
4648from collections .abc import (
4749 Callable ,
4850 Iterable ,
@@ -286,10 +288,10 @@ class AsyncAlert:
286288 """Contents of an asynchonous alert which display while user is at prompt.
287289
288290 :param msg: an optional message to be printed above the prompt.
289- :param prompt: an optional string to dynamically replace the active prompt.
291+ :param prompt: an optional string to dynamically replace the current prompt.
290292
291293 :ivar timestamp: monotonic creation time of the alert. If an alert was created
292- before the active prompt started , the prompt update is ignored
294+ before the current prompt was rendered , the prompt update is ignored
293295 to avoid a stale display but the msg will still be displayed.
294296 """
295297
@@ -613,10 +615,10 @@ def __init__(
613615 self ._command_parsers : _CommandParsers = _CommandParsers (self )
614616
615617 # Members related to printing asychronous alerts
616- self ._alert_queue : queue . Queue [AsyncAlert ] = queue . Queue ()
618+ self ._alert_queue : deque [AsyncAlert ] = deque ()
617619 self ._alert_condition = threading .Condition ()
618620 self ._alert_allowed = False
619- self ._alert_shutdown_event = threading . Event ()
621+ self ._alert_shutdown = False
620622 self ._alert_thread : threading .Thread | None = None
621623 self ._alert_prompt_timestamp : float = 0.0 # Uses time.monotonic()
622624
@@ -3358,35 +3360,57 @@ def read_input(
33583360
33593361 def _process_alerts (self ) -> None :
33603362 """Background worker that processes queued alerts and dynamic prompt updates."""
3361- while not self . _alert_shutdown_event . is_set () :
3363+ while True :
33623364 with self ._alert_condition :
3363- # Wait until alerts are allowed and available , or shutdown is signaled.
3365+ # Wait until we have alerts and are allowed to display them , or shutdown is signaled.
33643366 self ._alert_condition .wait_for (
3365- lambda : (not self ._alert_queue . empty () and self ._alert_allowed ) or self ._alert_shutdown_event . is_set ()
3367+ lambda : (len ( self ._alert_queue ) > 0 and self ._alert_allowed ) or self ._alert_shutdown
33663368 )
33673369
3368- if self ._alert_shutdown_event .is_set ():
3370+ # Shutdown immediately even if we have alerts.
3371+ if self ._alert_shutdown :
33693372 break
33703373
3371- # Get the next alert while still holding the condition lock.
3372- alert = self ._alert_queue .get ()
3374+ # Hold the condition lock while printing to block command execution. This
3375+ # prevents async alerts from printing once a command starts.
3376+
3377+ # Print all alerts at once to reduce flicker.
3378+ alert_text = "\n " .join (alert .msg for alert in self ._alert_queue if alert .msg )
3379+
3380+ # Find the latest prompt update among all pending alerts.
3381+ latest_prompt = None
3382+ for alert in reversed (self ._alert_queue ):
3383+ if (
3384+ alert .prompt is not None
3385+ and alert .prompt != self .prompt
3386+ and alert .timestamp > self ._alert_prompt_timestamp
3387+ ):
3388+ latest_prompt = alert .prompt
3389+ self ._alert_prompt_timestamp = alert .timestamp
3390+ break
3391+
3392+ # Clear the alerts
3393+ self ._alert_queue .clear ()
3394+
3395+ if alert_text :
3396+ if not self ._at_continuation_prompt and latest_prompt is not None :
3397+ # Update prompt now so patch_stdout can redraw it immediately.
3398+ self .prompt = latest_prompt
33733399
3374- # Only apply prompt changes generated after the active prompt started.
3375- prompt_updated = False
3376- if (alert .prompt is not None and
3377- alert .prompt != self .prompt and
3378- alert .timestamp > self ._alert_prompt_timestamp ): # fmt: skip
3379- self .prompt = alert .prompt
3380- prompt_updated = True
3400+ # Print the alert messages above the prompt.
3401+ with patch_stdout ():
3402+ print_formatted_text (pt_filter_style (alert_text ))
33813403
3382- if alert .msg :
3383- # Print the message above the active prompt.
3384- with patch_stdout ():
3385- print_formatted_text (pt_filter_style (alert .msg ))
3404+ if self ._at_continuation_prompt and latest_prompt is not None :
3405+ # Update state only. The onscreen prompt won't change until the next prompt starts.
3406+ self .prompt = latest_prompt
33863407
3387- # Refresh UI immediately unless at a continuation prompt.
3388- if prompt_updated and not self ._at_continuation_prompt :
3389- get_app ().invalidate ()
3408+ elif latest_prompt is not None :
3409+ self .prompt = latest_prompt
3410+
3411+ # Refresh UI immediately unless at a continuation prompt.
3412+ if not self ._at_continuation_prompt :
3413+ get_app ().invalidate ()
33903414
33913415 def _read_command_line (self , prompt : str ) -> str :
33923416 """Read the next command line from the input stream.
@@ -3409,12 +3433,13 @@ def _pre_prompt() -> None:
34093433 """Run standard pre-prompt processing and activate the background alerter."""
34103434 self .pre_prompt ()
34113435
3412- # Record when this prompt was started .
3436+ # Record when this prompt was rendered .
34133437 self ._alert_prompt_timestamp = time .monotonic ()
34143438
34153439 # Start alerter thread if it's not already running.
34163440 if self ._alert_thread is None or not self ._alert_thread .is_alive ():
3417- self ._alert_shutdown_event .clear ()
3441+ self ._alert_allowed = False
3442+ self ._alert_shutdown = False
34183443 self ._alert_thread = threading .Thread (target = self ._process_alerts , daemon = True )
34193444 self ._alert_thread .start ()
34203445
@@ -3465,7 +3490,7 @@ def _cmdloop(self) -> None:
34653490 # Shut down the alert thread.
34663491 if self ._alert_thread is not None :
34673492 with self ._alert_condition :
3468- self ._alert_shutdown_event . set ()
3493+ self ._alert_shutdown = True
34693494 self ._alert_condition .notify_all ()
34703495
34713496 # The thread is event-driven and stays suspended until notified.
@@ -5317,15 +5342,15 @@ def add_alert(self, *, msg: str | None = None, prompt: str | None = None) -> Non
53175342 add_alert(msg="Done", prompt="> ") # Update both
53185343
53195344 :param msg: an optional message to be printed above the prompt.
5320- :param prompt: an optional string to dynamically replace the active prompt.
5345+ :param prompt: an optional string to dynamically replace the current prompt.
53215346
53225347 """
53235348 if msg is None and prompt is None :
53245349 return
53255350
53265351 with self ._alert_condition :
53275352 alert = AsyncAlert (msg = msg , prompt = prompt )
5328- self ._alert_queue .put (alert )
5353+ self ._alert_queue .append (alert )
53295354 self ._alert_condition .notify_all ()
53305355
53315356 @staticmethod
0 commit comments