@@ -548,8 +548,8 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
548548 self .exit_code = None
549549
550550 # This lock should be acquired before doing any asynchronous changes to the terminal to
551- # ensure the updates to the terminal don't interfere with the input being typed. It can be
552- # acquired any time there is a readline prompt on screen .
551+ # ensure the updates to the terminal don't interfere with the input being typed or output
552+ # being printed by a command .
553553 self .terminal_lock = threading .RLock ()
554554
555555 # ----- Methods related to presenting output to the user -----
@@ -2204,7 +2204,7 @@ def _cmdloop(self) -> bool:
22042204
22052205 return stop
22062206
2207- # ----- Alias subcommand functions -----
2207+ # ----- Alias sub-command functions -----
22082208
22092209 def alias_create (self , args : argparse .Namespace ):
22102210 """Create or overwrite an alias"""
@@ -2267,8 +2267,8 @@ def alias_list(self, args: argparse.Namespace):
22672267 " macro" )
22682268 alias_parser = ACArgumentParser (description = alias_description , epilog = alias_epilog , prog = 'alias' )
22692269
2270- # Add subcommands to alias
2271- alias_subparsers = alias_parser .add_subparsers (title = 'sub-commands' )
2270+ # Add sub-commands to alias
2271+ alias_subparsers = alias_parser .add_subparsers ()
22722272
22732273 # alias -> create
22742274 alias_create_help = "create or overwrite an alias"
@@ -2326,13 +2326,13 @@ def do_alias(self, args: argparse.Namespace):
23262326 """Manage aliases"""
23272327 func = getattr (args , 'func' , None )
23282328 if func is not None :
2329- # Call whatever subcommand function was selected
2329+ # Call whatever sub-command function was selected
23302330 func (self , args )
23312331 else :
2332- # No subcommand was provided, so call help
2332+ # No sub-command was provided, so call help
23332333 self .do_help ('alias' )
23342334
2335- # ----- Macro subcommand functions -----
2335+ # ----- Macro sub-command functions -----
23362336
23372337 def macro_create (self , args : argparse .Namespace ):
23382338 """Create or overwrite a macro"""
@@ -2445,8 +2445,8 @@ def macro_list(self, args: argparse.Namespace):
24452445 " alias" )
24462446 macro_parser = ACArgumentParser (description = macro_description , epilog = macro_epilog , prog = 'macro' )
24472447
2448- # Add subcommands to macro
2449- macro_subparsers = macro_parser .add_subparsers (title = 'sub-commands' )
2448+ # Add sub-commands to macro
2449+ macro_subparsers = macro_parser .add_subparsers ()
24502450
24512451 # macro -> create
24522452 macro_create_help = "create or overwrite a macro"
@@ -2527,10 +2527,10 @@ def do_macro(self, args: argparse.Namespace):
25272527 """Manage macros"""
25282528 func = getattr (args , 'func' , None )
25292529 if func is not None :
2530- # Call whatever subcommand function was selected
2530+ # Call whatever sub-command function was selected
25312531 func (self , args )
25322532 else :
2533- # No subcommand was provided, so call help
2533+ # No sub-command was provided, so call help
25342534 self .do_help ('macro' )
25352535
25362536 def complete_help_command (self , text : str , line : str , begidx : int , endidx : int ) -> List [str ]:
@@ -2551,7 +2551,7 @@ def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: in
25512551 if not tokens :
25522552 return []
25532553
2554- # Must have at least 3 args for 'help command subcommand '
2554+ # Must have at least 3 args for 'help command sub-command '
25552555 if len (tokens ) < 3 :
25562556 return []
25572557
@@ -2580,7 +2580,7 @@ def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: in
25802580
25812581 setattr (help_parser .add_argument ('command' , help = "command to retrieve help for" , nargs = "?" ),
25822582 ACTION_ARG_CHOICES , ('complete_help_command' ,))
2583- setattr (help_parser .add_argument ('subcommand' , help = "subcommand to retrieve help for" ,
2583+ setattr (help_parser .add_argument ('subcommand' , help = "sub-command to retrieve help for" ,
25842584 nargs = argparse .REMAINDER ),
25852585 ACTION_ARG_CHOICES , ('complete_help_subcommand' ,))
25862586 help_parser .add_argument ('-v' , '--verbose' , action = 'store_true' ,
@@ -3481,12 +3481,13 @@ def _clear_input_lines_str(self) -> str: # pragma: no cover
34813481
34823482 def async_alert (self , alert_msg : str , new_prompt : Optional [str ] = None ) -> None : # pragma: no cover
34833483 """
3484- Used to display an important message to the user while they are at the prompt in between commands.
3484+ Display an important message to the user while they are at the prompt in between commands.
34853485 To the user it appears as if an alert message is printed above the prompt and their current input
34863486 text and cursor location is left alone.
34873487
3488- IMPORTANT: Do not call this unless you have acquired self.terminal_lock
3489- first, which ensures a prompt is onscreen
3488+ IMPORTANT: This function will not print an alert unless it can acquire self.terminal_lock to ensure
3489+ a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
3490+ to guarantee the alert prints.
34903491
34913492 :param alert_msg: the message to display to the user
34923493 :param new_prompt: if you also want to change the prompt that is displayed, then include it here
@@ -3525,34 +3526,48 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
35253526
35263527 def async_update_prompt (self , new_prompt : str ) -> None : # pragma: no cover
35273528 """
3528- Updates the prompt while the user is still typing at it. This is good for alerting the user to system
3529+ Update the prompt while the user is still typing at it. This is good for alerting the user to system
35293530 changes dynamically in between commands. For instance you could alter the color of the prompt to indicate
35303531 a system status or increase a counter to report an event. If you do alter the actual text of the prompt,
35313532 it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
35323533 be shifted and the update will not be seamless.
35333534
3534- IMPORTANT: Do not call this unless you have acquired self.terminal_lock
3535- first, which ensures a prompt is onscreen
3535+ IMPORTANT: This function will not update the prompt unless it can acquire self.terminal_lock to ensure
3536+ a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
3537+ to guarantee the prompt changes.
35363538
35373539 :param new_prompt: what to change the prompt to
3540+ :raises RuntimeError if called while another thread holds terminal_lock
35383541 """
35393542 self .async_alert ('' , new_prompt )
35403543
3541- @staticmethod
3542- def set_window_title (title : str ) -> None : # pragma: no cover
3544+ def set_window_title (self , title : str ) -> None : # pragma: no cover
35433545 """
3544- Sets the terminal window title
3546+ Set the terminal window title
3547+
3548+ IMPORTANT: This function will not set the title unless it can acquire self.terminal_lock to avoid
3549+ writing to stderr while a command is running. Therefore it is best to acquire the lock
3550+ before calling this function to guarantee the title changes.
3551+
35453552 :param title: the new window title
3553+ :raises RuntimeError if called while another thread holds terminal_lock
35463554 """
35473555 if not vt100_support :
35483556 return
35493557
3550- import colorama .ansi as ansi
3551- try :
3552- sys .stderr .write (ansi .set_title (title ))
3553- except AttributeError :
3554- # Debugging in Pycharm has issues with setting terminal title
3555- pass
3558+ # Sanity check that can't fail if self.terminal_lock was acquired before calling this function
3559+ if self .terminal_lock .acquire (blocking = False ):
3560+ try :
3561+ import colorama .ansi as ansi
3562+ sys .stderr .write (ansi .set_title (title ))
3563+ except AttributeError :
3564+ # Debugging in Pycharm has issues with setting terminal title
3565+ pass
3566+
3567+ self .terminal_lock .release ()
3568+
3569+ else :
3570+ raise RuntimeError ("another thread holds terminal_lock" )
35563571
35573572 def cmdloop (self , intro : Optional [str ]= None ) -> None :
35583573 """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
0 commit comments