From bd34fee86d66a610d1e0bb392186dc73e883e2db Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 02:05:08 -0400 Subject: Wrote function and test command to print alerts --- cmd2/cmd2.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 888d9531..874fdf6c 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -50,7 +50,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .parsing import StatementParser, Statement # Set up readline -from .rl_utils import rl_type, RlType +from .rl_utils import rl_type, RlType, rl_get_point if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ @@ -3191,6 +3191,74 @@ Script should contain one command per line, just like command would be typed in runner = unittest.TextTestRunner() runner.run(testcase) + def print_alert(self, alert_msg: str) -> None: + """ + Used to display an important message to the user while they are at the prompt. + The current prompt and input lines are erased, then the alert is printed in their place, and + finally the prompt and input lines restored below the alert. To the user it appears as if + an alert message is printed above the prompt. + + :param alert_msg: the message to display to the user + """ + if rl_type == RlType.NONE: + return + + import shutil + import colorama.ansi as ansi + from colorama import Cursor + + visible_prompt = self.visible_prompt + + # Get the size of the terminal + terminal_size = shutil.get_terminal_size() + + # Figure out how many lines the prompt and user input take up + total_str_size = len(visible_prompt) + len(readline.get_line_buffer()) + num_input_lines = int(total_str_size / terminal_size.columns) + 1 + + # Get the cursor's offset from the beginning of the first input line + cursor_input_offset = len(visible_prompt) + rl_get_point() + + # Calculate what input line the cursor is on + cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1 + + # Create a string that will clear all input lines and print the alert + terminal_str = '' + + # Move the cursor down to the last input line + if cursor_input_line != num_input_lines: + terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line) + + # Clear each input line from the bottom up so that the cursor ends up on the original first input line + terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1) + terminal_str += ansi.clear_line() + + # Print the alert + terminal_str += alert_msg + '\n' + + # Write the string to the terminal + if rl_type == RlType.GNU: + sys.stdout.write(terminal_str) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode.console.write(terminal_str) + + # Redraw the prompt and input line + rl_force_redisplay() + + def worker(self): + import time + while True: + alert_msg = "\n***********************************************\n" \ + " Message failed to send\n" \ + "***********************************************" + time.sleep(5) + self.print_alert(alert_msg) + + def do_alert(self, args): + import threading + printer = threading.Thread(target=self.worker, daemon=True) + printer.start() + def cmdloop(self, intro: Optional[str]=None) -> None: """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. -- cgit v1.2.1 From d9ed3612b9550ec3b82427a0ef822e8c8418ac89 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 02:19:48 -0400 Subject: Moving cursor to beginning of line before printing alert --- cmd2/cmd2.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 874fdf6c..d817e461 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3233,6 +3233,9 @@ Script should contain one command per line, just like command would be typed in terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1) terminal_str += ansi.clear_line() + # Move the cursor to the beginning of the first input line + terminal_str += '\r' + # Print the alert terminal_str += alert_msg + '\n' @@ -3245,7 +3248,7 @@ Script should contain one command per line, just like command would be typed in # Redraw the prompt and input line rl_force_redisplay() - def worker(self): + def alerter(self): import time while True: alert_msg = "\n***********************************************\n" \ @@ -3256,7 +3259,7 @@ Script should contain one command per line, just like command would be typed in def do_alert(self, args): import threading - printer = threading.Thread(target=self.worker, daemon=True) + printer = threading.Thread(target=self.alerter, daemon=True) printer.start() def cmdloop(self, intro: Optional[str]=None) -> None: -- cgit v1.2.1 From f406d5b81625a830c54b35acec6caf937c94c6da Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 02:44:53 -0400 Subject: Wrote a prompt updater example --- cmd2/cmd2.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 3 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d817e461..baf511f3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -50,7 +50,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .parsing import StatementParser, Statement # Set up readline -from .rl_utils import rl_type, RlType, rl_get_point +from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ @@ -3248,6 +3248,64 @@ Script should contain one command per line, just like command would be typed in # Redraw the prompt and input line rl_force_redisplay() + def update_prompt(self, new_prompt: str) -> None: + """ + Used to display an important message to the user while they are at the prompt. + The current prompt and input lines are erased, then the alert is printed in their place, and + finally the prompt and input lines restored below the alert. To the user it appears as if + an alert message is printed above the prompt. + + :param alert_msg: the message to display to the user + """ + if rl_type == RlType.NONE: + return + + import shutil + import colorama.ansi as ansi + from colorama import Cursor + + visible_prompt = self.visible_prompt + + # Get the size of the terminal + terminal_size = shutil.get_terminal_size() + + # Figure out how many lines the prompt and user input take up + total_str_size = len(visible_prompt) + len(readline.get_line_buffer()) + num_input_lines = int(total_str_size / terminal_size.columns) + 1 + + # Get the cursor's offset from the beginning of the first input line + cursor_input_offset = len(visible_prompt) + rl_get_point() + + # Calculate what input line the cursor is on + cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1 + + # Create a string that will clear all input lines + terminal_str = '' + + # Move the cursor down to the last input line + if cursor_input_line != num_input_lines: + terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line) + + # Clear each input line from the bottom up so that the cursor ends up on the original first input line + terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1) + terminal_str += ansi.clear_line() + + # Move the cursor to the beginning of the line + terminal_str += '\r' + + # Set the new prompt + rl_set_prompt(new_prompt) + self.prompt = new_prompt + + # Write the string to the terminal + if rl_type == RlType.GNU: + sys.stdout.write(terminal_str) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode.console.write(terminal_str) + + # Redraw the prompt and input line + rl_force_redisplay() + def alerter(self): import time while True: @@ -3259,8 +3317,22 @@ Script should contain one command per line, just like command would be typed in def do_alert(self, args): import threading - printer = threading.Thread(target=self.alerter, daemon=True) - printer.start() + alert_thread = threading.Thread(target=self.alerter, daemon=True) + alert_thread.start() + + def prompt_changer(self): + import time + counter = 1 + while True: + time.sleep(1) + self.update_prompt("({:>3}) ".format(counter)) + counter += 1 + + def do_update_prompt(self, args): + import threading + self.prompt = "( 0) " + change_thread = threading.Thread(target=self.prompt_changer, daemon=True) + change_thread.start() def cmdloop(self, intro: Optional[str]=None) -> None: """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. -- cgit v1.2.1 From 1cf3b9566f6c7e87a8074357c5b3ca2b4924120d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 13 Sep 2018 11:35:08 -0400 Subject: This just fixes a couple minor lint warnings related to raw strings and escapes --- cmd2/cmd2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index baf511f3..b6eb3fe4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3031,7 +3031,7 @@ a..b, a:b, a:, ..b items by indices (inclusive) # get the output out of the buffer output = membuf.read() # and add the regex-escaped output to the transcript - transcript += output.replace('/', '\/') + transcript += output.replace('/', r'\/') # Restore stdout to its original state self.stdout = saved_self_stdout -- cgit v1.2.1 From 556858e64d75fb316800ad34bda465b345ce3eb6 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 18:06:20 -0400 Subject: Made common function to clear input lines in the terminal --- cmd2/cmd2.py | 74 ++++++++++++++++++++---------------------------------------- 1 file changed, 24 insertions(+), 50 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b6eb3fe4..08e97685 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3191,14 +3191,11 @@ Script should contain one command per line, just like command would be typed in runner = unittest.TextTestRunner() runner.run(testcase) - def print_alert(self, alert_msg: str) -> None: + def _clear_input_lines_str(self) -> str: """ - Used to display an important message to the user while they are at the prompt. - The current prompt and input lines are erased, then the alert is printed in their place, and - finally the prompt and input lines restored below the alert. To the user it appears as if - an alert message is printed above the prompt. - - :param alert_msg: the message to display to the user + Returns a string that if printed will clear the prompt and input lines in the terminal, + leaving the cursor at the beginning of the first input line + :return: the string to print """ if rl_type == RlType.NONE: return @@ -3236,74 +3233,51 @@ Script should contain one command per line, just like command would be typed in # Move the cursor to the beginning of the first input line terminal_str += '\r' - # Print the alert + return terminal_str + + def print_alert(self, alert_msg: str) -> None: + """ + Used to display an important message to the user while they are at the prompt. + To the user it appears as if an alert message is printed above the prompt. + + :param alert_msg: the message to display to the user + """ + if rl_type == RlType.NONE: + return + + # Clear the prompt and input lines and replace with the alert + terminal_str = self._clear_input_lines_str() terminal_str += alert_msg + '\n' - # Write the string to the terminal if rl_type == RlType.GNU: sys.stdout.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) - # Redraw the prompt and input line + # Redraw the prompt and input lines rl_force_redisplay() def update_prompt(self, new_prompt: str) -> None: """ - Used to display an important message to the user while they are at the prompt. - The current prompt and input lines are erased, then the alert is printed in their place, and - finally the prompt and input lines restored below the alert. To the user it appears as if - an alert message is printed above the prompt. + Dynamically alters the prompt in the terminal - :param alert_msg: the message to display to the user + :param new_prompt: what to change the prompt to """ if rl_type == RlType.NONE: return - import shutil - import colorama.ansi as ansi - from colorama import Cursor - - visible_prompt = self.visible_prompt - - # Get the size of the terminal - terminal_size = shutil.get_terminal_size() - - # Figure out how many lines the prompt and user input take up - total_str_size = len(visible_prompt) + len(readline.get_line_buffer()) - num_input_lines = int(total_str_size / terminal_size.columns) + 1 - - # Get the cursor's offset from the beginning of the first input line - cursor_input_offset = len(visible_prompt) + rl_get_point() - - # Calculate what input line the cursor is on - cursor_input_line = int(cursor_input_offset / terminal_size.columns) + 1 - - # Create a string that will clear all input lines - terminal_str = '' - - # Move the cursor down to the last input line - if cursor_input_line != num_input_lines: - terminal_str += Cursor.DOWN(num_input_lines - cursor_input_line) - - # Clear each input line from the bottom up so that the cursor ends up on the original first input line - terminal_str += (ansi.clear_line() + Cursor.UP(1)) * (num_input_lines - 1) - terminal_str += ansi.clear_line() - - # Move the cursor to the beginning of the line - terminal_str += '\r' - # Set the new prompt rl_set_prompt(new_prompt) self.prompt = new_prompt - # Write the string to the terminal + # Clear the prompt and input lines + terminal_str = self._clear_input_lines_str() if rl_type == RlType.GNU: sys.stdout.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) - # Redraw the prompt and input line + # Redraw the prompt and input lines rl_force_redisplay() def alerter(self): -- cgit v1.2.1 From 9a25ec6cdf29b1efa670b16b409976eb9df7987d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 22:13:11 -0400 Subject: More code to support asynchronous changes to the terminal --- cmd2/cmd2.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 14 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e22ff9d6..d3e674de 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -40,6 +40,7 @@ import platform import re import shlex import sys +import threading from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union from . import constants @@ -50,7 +51,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .parsing import StatementParser, Statement # Set up readline -from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt +from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ @@ -526,6 +527,11 @@ class Cmd(cmd.Cmd): # This determines if a non-zero exit code should be used when exiting the application self.exit_code = None + # This lock should be acquired before doing any asynchronous changes to the terminal to + # ensure the updates to the terminal don't interfere with the input being typed. It can be + # acquired any time there is a readline prompt on screen. + self._terminal_lock = threading.RLock() + # ----- Methods related to presenting output to the user ----- @property @@ -1643,6 +1649,9 @@ class Cmd(cmd.Cmd): # Register a default SIGINT signal handler for Ctrl+C signal.signal(signal.SIGINT, self.sigint_handler) + # Grab the terminal lock before the prompt has been drawn by readline + self._terminal_lock.acquire() + def precmd(self, statement: Statement) -> Statement: """Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history. @@ -2114,6 +2123,8 @@ class Cmd(cmd.Cmd): if self.use_rawinput: try: if sys.stdin.isatty(): + # A prompt is about to be drawn. Allow asynchronous changes to the terminal. + self._terminal_lock.release() line = input(safe_prompt) else: line = input() @@ -2121,6 +2132,10 @@ class Cmd(cmd.Cmd): sys.stdout.write('{}{}\n'.format(safe_prompt, line)) except EOFError: line = 'eof' + finally: + if sys.stdin.isatty(): + # The prompt is gone. Do not allow asynchronous changes to the terminal. + self._terminal_lock.acquire() else: if self.stdin.isatty(): # on a tty, print the prompt first, then read the line @@ -3197,8 +3212,8 @@ Script should contain one command per line, just like command would be typed in leaving the cursor at the beginning of the first input line :return: the string to print """ - if rl_type == RlType.NONE: - return + if not (vt100_support and self.use_rawinput): + return '' import shutil import colorama.ansi as ansi @@ -3235,35 +3250,52 @@ Script should contain one command per line, just like command would be typed in return terminal_str - def print_alert(self, alert_msg: str) -> None: + def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: """ - Used to display an important message to the user while they are at the prompt. - To the user it appears as if an alert message is printed above the prompt. + Used to display an important message to the user while they are at the prompt in between commands. + To the user it appears as if an alert message is printed above the prompt and their current input + text and cursor location is left alone. + + IMPORTANT: Do not call this unless you have acquired self._terminal_lock + first, which ensures a prompt is onscreen :param alert_msg: the message to display to the user + :param new_prompt: if you also want to change the prompt that is displayed, then include it here + see async_update_prompt() docstring for guidance on updating a prompt """ - if rl_type == RlType.NONE: + if not (vt100_support and self.use_rawinput): return + if new_prompt is not None: + rl_set_prompt(new_prompt) + self.prompt = new_prompt + # Clear the prompt and input lines and replace with the alert terminal_str = self._clear_input_lines_str() terminal_str += alert_msg + '\n' if rl_type == RlType.GNU: - sys.stdout.write(terminal_str) + sys.stderr.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) # Redraw the prompt and input lines rl_force_redisplay() - def update_prompt(self, new_prompt: str) -> None: + def async_update_prompt(self, new_prompt: str) -> None: """ - Dynamically alters the prompt in the terminal + Updates the prompt while the user is still typing at it. This is good for alerting the user to system + changes dynamically in between commands. For instance you could alter the color of the prompt to indicate + a system status or increase a counter to report an event. If you do alter the actual text of the prompt, + it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will + be shifted and the update will not be seamless. + + IMPORTANT: Do not call this unless you have acquired self._terminal_lock + first, which ensures a prompt is onscreen :param new_prompt: what to change the prompt to """ - if rl_type == RlType.NONE: + if not (vt100_support and self.use_rawinput): return # Set the new prompt @@ -3273,13 +3305,29 @@ Script should contain one command per line, just like command would be typed in # Clear the prompt and input lines terminal_str = self._clear_input_lines_str() if rl_type == RlType.GNU: - sys.stdout.write(terminal_str) + sys.stderr.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) # Redraw the prompt and input lines rl_force_redisplay() + @staticmethod + def set_window_title(title: str) -> None: + """ + Sets the terminal window title + :param title: the new window title + """ + if not vt100_support: + return + + import colorama.ansi as ansi + try: + sys.stderr.write(ansi.set_title(title)) + except AttributeError: + # Debugging in Pycharm has issues with setting terminal title + pass + def alerter(self): import time while True: @@ -3287,7 +3335,7 @@ Script should contain one command per line, just like command would be typed in " Message failed to send\n" \ "***********************************************" time.sleep(5) - self.print_alert(alert_msg) + self.async_alert(alert_msg) def do_alert(self, args): import threading @@ -3299,7 +3347,7 @@ Script should contain one command per line, just like command would be typed in counter = 1 while True: time.sleep(1) - self.update_prompt("({:>3}) ".format(counter)) + self.async_update_prompt("({:>3}) ".format(counter)) counter += 1 def do_update_prompt(self, args): -- cgit v1.2.1 From 58bf4ca1f53e690ef782413a6979a0b5d11cf1ba Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 19 Sep 2018 22:20:36 -0400 Subject: Handling case of releasing _terminal_lock before its been locked --- cmd2/cmd2.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d3e674de..c9247d66 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2123,8 +2123,12 @@ class Cmd(cmd.Cmd): if self.use_rawinput: try: if sys.stdin.isatty(): - # A prompt is about to be drawn. Allow asynchronous changes to the terminal. - self._terminal_lock.release() + try: + # A prompt is about to be drawn. Allow asynchronous changes to the terminal. + self._terminal_lock.release() + except RuntimeError: + pass + line = input(safe_prompt) else: line = input() -- cgit v1.2.1 From 5009b7c6d633223d171cbdedd0d6dd864a89cbe9 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 20 Sep 2018 15:19:57 -0400 Subject: Removed example commands --- cmd2/cmd2.py | 28 ---------------------------- 1 file changed, 28 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index c9247d66..30fe67e5 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3332,34 +3332,6 @@ Script should contain one command per line, just like command would be typed in # Debugging in Pycharm has issues with setting terminal title pass - def alerter(self): - import time - while True: - alert_msg = "\n***********************************************\n" \ - " Message failed to send\n" \ - "***********************************************" - time.sleep(5) - self.async_alert(alert_msg) - - def do_alert(self, args): - import threading - alert_thread = threading.Thread(target=self.alerter, daemon=True) - alert_thread.start() - - def prompt_changer(self): - import time - counter = 1 - while True: - time.sleep(1) - self.async_update_prompt("({:>3}) ".format(counter)) - counter += 1 - - def do_update_prompt(self, args): - import threading - self.prompt = "( 0) " - change_thread = threading.Thread(target=self.prompt_changer, daemon=True) - change_thread.start() - def cmdloop(self, intro: Optional[str]=None) -> None: """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. -- cgit v1.2.1 From 864116af66cdb1e94607bfbd83386a9bd7b37b32 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 20 Sep 2018 21:26:09 -0400 Subject: Fixed when prompt is set in async functions Added more type hinting Removed post loop and moved its contents to cmdloop --- cmd2/cmd2.py | 59 ++++++++++++++++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 29 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 30fe67e5..756c043f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1643,15 +1643,6 @@ class Cmd(cmd.Cmd): # Re-raise a KeyboardInterrupt so other parts of the code can catch it raise KeyboardInterrupt("Got a keyboard interrupt") - def preloop(self) -> None: - """Hook method executed once when the cmdloop() method is called.""" - import signal - # Register a default SIGINT signal handler for Ctrl+C - signal.signal(signal.SIGINT, self.sigint_handler) - - # Grab the terminal lock before the prompt has been drawn by readline - self._terminal_lock.acquire() - def precmd(self, statement: Statement) -> Statement: """Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history. @@ -3270,10 +3261,6 @@ Script should contain one command per line, just like command would be typed in if not (vt100_support and self.use_rawinput): return - if new_prompt is not None: - rl_set_prompt(new_prompt) - self.prompt = new_prompt - # Clear the prompt and input lines and replace with the alert terminal_str = self._clear_input_lines_str() terminal_str += alert_msg + '\n' @@ -3283,6 +3270,11 @@ Script should contain one command per line, just like command would be typed in elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) + # Set the new prompt + if new_prompt is not None: + self.prompt = new_prompt + rl_set_prompt(self.prompt) + # Redraw the prompt and input lines rl_force_redisplay() @@ -3302,10 +3294,6 @@ Script should contain one command per line, just like command would be typed in if not (vt100_support and self.use_rawinput): return - # Set the new prompt - rl_set_prompt(new_prompt) - self.prompt = new_prompt - # Clear the prompt and input lines terminal_str = self._clear_input_lines_str() if rl_type == RlType.GNU: @@ -3313,6 +3301,10 @@ Script should contain one command per line, just like command would be typed in elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) + # Set the new prompt + self.prompt = new_prompt + rl_set_prompt(self.prompt) + # Redraw the prompt and input lines rl_force_redisplay() @@ -3357,6 +3349,13 @@ Script should contain one command per line, just like command would be typed in if callargs: self.cmdqueue.extend(callargs) + # Register a default SIGINT signal handler for Ctrl+C + import signal + signal.signal(signal.SIGINT, self.sigint_handler) + + # Grab the terminal lock before the prompt has been drawn by readline + self._terminal_lock.acquire() + # Always run the preloop first for func in self._preloop_hooks: func() @@ -3390,7 +3389,7 @@ Script should contain one command per line, just like command would be typed in # plugin related functions # ### - def _initialize_plugin_system(self): + def _initialize_plugin_system(self) -> None: """Initialize the plugin system""" self._preloop_hooks = [] self._postloop_hooks = [] @@ -3400,7 +3399,7 @@ Script should contain one command per line, just like command would be typed in self._cmdfinalization_hooks = [] @classmethod - def _validate_callable_param_count(cls, func: Callable, count: int): + def _validate_callable_param_count(cls, func: Callable, count: int) -> None: """Ensure a function has the given number of parameters.""" signature = inspect.signature(func) # validate that the callable has the right number of parameters @@ -3413,7 +3412,7 @@ Script should contain one command per line, just like command would be typed in )) @classmethod - def _validate_prepostloop_callable(cls, func: Callable): + def _validate_prepostloop_callable(cls, func: Callable[[None], None]) -> None: """Check parameter and return types for preloop and postloop hooks.""" cls._validate_callable_param_count(func, 0) # make sure there is no return notation @@ -3423,18 +3422,18 @@ Script should contain one command per line, just like command would be typed in func.__name__, )) - def register_preloop_hook(self, func: Callable): + def register_preloop_hook(self, func: Callable[[None], None]) -> None: """Register a function to be called at the beginning of the command loop.""" self._validate_prepostloop_callable(func) self._preloop_hooks.append(func) - def register_postloop_hook(self, func: Callable): + def register_postloop_hook(self, func: Callable[[None], None]) -> None: """Register a function to be called at the end of the command loop.""" self._validate_prepostloop_callable(func) self._postloop_hooks.append(func) @classmethod - def _validate_postparsing_callable(cls, func: Callable): + def _validate_postparsing_callable(cls, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None: """Check parameter and return types for postparsing hooks""" cls._validate_callable_param_count(func, 1) signature = inspect.signature(func) @@ -3448,13 +3447,13 @@ Script should contain one command per line, just like command would be typed in func.__name__ )) - def register_postparsing_hook(self, func: Callable): + def register_postparsing_hook(self, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None: """Register a function to be called after parsing user input but before running the command""" self._validate_postparsing_callable(func) self._postparsing_hooks.append(func) @classmethod - def _validate_prepostcmd_hook(cls, func: Callable, data_type: Type): + def _validate_prepostcmd_hook(cls, func: Callable, data_type: Type) -> None: """Check parameter and return types for pre and post command hooks.""" signature = inspect.signature(func) # validate that the callable has the right number of parameters @@ -3481,18 +3480,19 @@ Script should contain one command per line, just like command would be typed in data_type, )) - def register_precmd_hook(self, func: Callable): + def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None: """Register a hook to be called before the command function.""" self._validate_prepostcmd_hook(func, plugin.PrecommandData) self._precmd_hooks.append(func) - def register_postcmd_hook(self, func: Callable): + def register_postcmd_hook(self, func: Callable[[plugin.PostcommandData], plugin.PostcommandData]) -> None: """Register a hook to be called after the command function.""" self._validate_prepostcmd_hook(func, plugin.PostcommandData) self._postcmd_hooks.append(func) @classmethod - def _validate_cmdfinalization_callable(cls, func: Callable): + def _validate_cmdfinalization_callable(cls, func: Callable[[plugin.CommandFinalizationData], + plugin.CommandFinalizationData]) -> None: """Check parameter and return types for command finalization hooks.""" cls._validate_callable_param_count(func, 1) signature = inspect.signature(func) @@ -3504,7 +3504,8 @@ Script should contain one command per line, just like command would be typed in raise TypeError("{} must declare return a return type of " "'cmd2.plugin.CommandFinalizationData'".format(func.__name__)) - def register_cmdfinalization_hook(self, func: Callable): + def register_cmdfinalization_hook(self, func: Callable[[plugin.CommandFinalizationData], + plugin.CommandFinalizationData]) -> None: """Register a hook to be called after a command is completed, whether it completes successfully or not.""" self._validate_cmdfinalization_callable(func) self._cmdfinalization_hooks.append(func) -- cgit v1.2.1 From 92dd10ec702e0cde0ee1fd157957aa816f2c137f Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 20 Sep 2018 21:45:50 -0400 Subject: Restoring original signint handler Recreating _terminal_lock when cmdloop begins to avoid any deadlock from previous runs of cmdloop. --- cmd2/cmd2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 756c043f..5add08b8 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3349,11 +3349,13 @@ Script should contain one command per line, just like command would be typed in if callargs: self.cmdqueue.extend(callargs) - # Register a default SIGINT signal handler for Ctrl+C + # Register a SIGINT signal handler for Ctrl+C import signal + original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - # Grab the terminal lock before the prompt has been drawn by readline + # Recreate terminal lock for this session and grab it before the prompt has been drawn by readline + self._terminal_lock = threading.RLock() self._terminal_lock.acquire() # Always run the preloop first @@ -3381,6 +3383,9 @@ Script should contain one command per line, just like command would be typed in func() self.postloop() + # Restore the original signal handler + signal.signal(signal.SIGINT, original_sigint_handler) + if self.exit_code is not None: sys.exit(self.exit_code) -- cgit v1.2.1 From c706f6a95c41392fcca0b3a93b689a19ba06a0f4 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 21 Sep 2018 17:27:58 -0400 Subject: Made sure all prompts sent to GNU readline are made safe --- cmd2/cmd2.py | 41 ++++++----------------------------------- 1 file changed, 6 insertions(+), 35 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 62077427..dae934b8 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -51,7 +51,7 @@ from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer from .parsing import StatementParser, Statement # Set up readline -from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support +from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt if rl_type == RlType.NONE: # pragma: no cover rl_warning = "Readline features including tab completion have been disabled since no \n" \ "supported version of readline was found. To resolve this, install \n" \ @@ -2070,34 +2070,6 @@ class Cmd(cmd.Cmd): # Print out a message stating this is an unknown command self.poutput('*** Unknown syntax: {}\n'.format(arg)) - @staticmethod - def _surround_ansi_escapes(prompt: str, start: str="\x01", end: str="\x02") -> str: - """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes. - - :param prompt: original prompt - :param start: start code to tell GNU Readline about beginning of invisible characters - :param end: end code to tell GNU Readline about end of invisible characters - :return: prompt safe to pass to GNU Readline - """ - # Windows terminals don't use ANSI escape codes and Windows readline isn't based on GNU Readline - if sys.platform == "win32": - return prompt - - escaped = False - result = "" - - for c in prompt: - if c == "\x1b" and not escaped: - result += start + c - escaped = True - elif c.isalpha() and escaped: - result += c + end - escaped = False - else: - result += c - - return result - def pseudo_raw_input(self, prompt: str) -> str: """Began life as a copy of cmd's cmdloop; like raw_input but @@ -2106,9 +2078,6 @@ class Cmd(cmd.Cmd): to decide whether to print the prompt and the input """ - # Deal with the vagaries of readline and ANSI escape codes - safe_prompt = self._surround_ansi_escapes(prompt) - if self.use_rawinput: try: if sys.stdin.isatty(): @@ -2118,11 +2087,13 @@ class Cmd(cmd.Cmd): except RuntimeError: pass + # Deal with the vagaries of readline and ANSI escape codes + safe_prompt = rl_make_safe_prompt(prompt) line = input(safe_prompt) else: line = input() if self.echo: - sys.stdout.write('{}{}\n'.format(safe_prompt, line)) + sys.stdout.write('{}{}\n'.format(self.prompt, line)) except EOFError: line = 'eof' finally: @@ -2132,7 +2103,7 @@ class Cmd(cmd.Cmd): else: if self.stdin.isatty(): # on a tty, print the prompt first, then read the line - self.poutput(safe_prompt, end='') + self.poutput(self.prompt, end='') self.stdout.flush() line = self.stdin.readline() if len(line) == 0: @@ -2145,7 +2116,7 @@ class Cmd(cmd.Cmd): if len(line): # we read something, output the prompt and the something if self.echo: - self.poutput('{}{}'.format(safe_prompt, line)) + self.poutput('{}{}'.format(self.prompt, line)) else: line = 'eof' return line.strip() -- cgit v1.2.1 From 3f57b97df8cd5576547bddea98cc72238f0c6072 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 21 Sep 2018 21:21:21 -0400 Subject: Updating prompt in example app --- cmd2/cmd2.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index dae934b8..3063068d 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3221,7 +3221,7 @@ Script should contain one command per line, just like command would be typed in return terminal_str - def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: + def _async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: """ Used to display an important message to the user while they are at the prompt in between commands. To the user it appears as if an alert message is printed above the prompt and their current input @@ -3237,24 +3237,25 @@ Script should contain one command per line, just like command would be typed in if not (vt100_support and self.use_rawinput): return - # Clear the prompt and input lines and replace with the alert + # Generate a string to clear the prompt and input lines and replace with the alert terminal_str = self._clear_input_lines_str() terminal_str += alert_msg + '\n' + # Set the new prompt now that _clear_input_lines_str is done using the old prompt + if new_prompt is not None: + self.prompt = new_prompt + rl_set_prompt(self.prompt) + + # Print terminal_str to erase the lines if rl_type == RlType.GNU: sys.stderr.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) - # Set the new prompt - if new_prompt is not None: - self.prompt = new_prompt - rl_set_prompt(self.prompt) - # Redraw the prompt and input lines rl_force_redisplay() - def async_update_prompt(self, new_prompt: str) -> None: + def _async_update_prompt(self, new_prompt: str) -> None: """ Updates the prompt while the user is still typing at it. This is good for alerting the user to system changes dynamically in between commands. For instance you could alter the color of the prompt to indicate @@ -3270,17 +3271,19 @@ Script should contain one command per line, just like command would be typed in if not (vt100_support and self.use_rawinput): return - # Clear the prompt and input lines + # Generate a string to clear the prompt and input lines terminal_str = self._clear_input_lines_str() + + # Set the new prompt now that _clear_input_lines_str is done using the old prompt + self.prompt = new_prompt + rl_set_prompt(self.prompt) + + # Print terminal_str to erase the lines if rl_type == RlType.GNU: sys.stderr.write(terminal_str) elif rl_type == RlType.PYREADLINE: readline.rl.mode.console.write(terminal_str) - # Set the new prompt - self.prompt = new_prompt - rl_set_prompt(self.prompt) - # Redraw the prompt and input lines rl_force_redisplay() -- cgit v1.2.1 From 097c065b740e1f03ad960cb8ec79666d045e1286 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 22 Sep 2018 13:47:40 -0400 Subject: Making sure self.prompt reflects what is on screen --- cmd2/cmd2.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 3063068d..071dd468 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2078,6 +2078,10 @@ class Cmd(cmd.Cmd): to decide whether to print the prompt and the input """ + # Temporarily save over self.prompt to reflect what will be on screen + orig_prompt = self.prompt + self.prompt = prompt + if self.use_rawinput: try: if sys.stdin.isatty(): @@ -2119,6 +2123,10 @@ class Cmd(cmd.Cmd): self.poutput('{}{}'.format(self.prompt, line)) else: line = 'eof' + + # Restore prompt + self.prompt = orig_prompt + return line.strip() def _cmdloop(self) -> bool: @@ -2496,7 +2504,8 @@ Usage: Usage: unalias [-a] name [name ...] for (idx, (_, text)) in enumerate(fulloptions): self.poutput(' %2d. %s\n' % (idx + 1, text)) while True: - response = input(prompt) + safe_prompt = rl_make_safe_prompt(prompt) + response = input(safe_prompt) if rl_type != RlType.NONE: hlen = readline.get_current_history_length() @@ -3333,8 +3342,7 @@ Script should contain one command per line, just like command would be typed in original_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - # Recreate terminal lock for this session and grab it before the prompt has been drawn by readline - self._terminal_lock = threading.RLock() + # Grab terminal lock before the prompt has been drawn by readline self._terminal_lock.acquire() # Always run the preloop first @@ -3362,6 +3370,10 @@ Script should contain one command per line, just like command would be typed in func() self.postloop() + # Release terminal lock now that postloop code should have stopped any terminal updater threads + # This will also zero the lock count in case cmdloop() is called again + self._terminal_lock.release() + # Restore the original signal handler signal.signal(signal.SIGINT, original_sigint_handler) -- cgit v1.2.1 From 1c5c9aa0fb38bb5b62f2c62bef62eaee9fa91d95 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sun, 23 Sep 2018 14:48:05 -0400 Subject: Async printing functions raise RuntimeError if called before acquiring terminal lock --- cmd2/cmd2.py | 71 ++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 26 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 071dd468..3a9d460e 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2085,6 +2085,7 @@ class Cmd(cmd.Cmd): if self.use_rawinput: try: if sys.stdin.isatty(): + # Wrap in try since _terminal_lock may not be locked when this function is called from unit tests try: # A prompt is about to be drawn. Allow asynchronous changes to the terminal. self._terminal_lock.release() @@ -3242,27 +3243,36 @@ Script should contain one command per line, just like command would be typed in :param alert_msg: the message to display to the user :param new_prompt: if you also want to change the prompt that is displayed, then include it here see async_update_prompt() docstring for guidance on updating a prompt + :raises RuntimeError if called while another thread holds _terminal_lock """ if not (vt100_support and self.use_rawinput): return - # Generate a string to clear the prompt and input lines and replace with the alert - terminal_str = self._clear_input_lines_str() - terminal_str += alert_msg + '\n' + # Sanity check that can't fail if self._terminal_lock was acquired before calling this function + if self._terminal_lock.acquire(blocking=False): - # Set the new prompt now that _clear_input_lines_str is done using the old prompt - if new_prompt is not None: - self.prompt = new_prompt - rl_set_prompt(self.prompt) + # Generate a string to clear the prompt and input lines and replace with the alert + terminal_str = self._clear_input_lines_str() + terminal_str += alert_msg + '\n' - # Print terminal_str to erase the lines - if rl_type == RlType.GNU: - sys.stderr.write(terminal_str) - elif rl_type == RlType.PYREADLINE: - readline.rl.mode.console.write(terminal_str) + # Set the new prompt now that _clear_input_lines_str is done using the old prompt + if new_prompt is not None: + self.prompt = new_prompt + rl_set_prompt(self.prompt) + + # Print terminal_str to erase the lines + if rl_type == RlType.GNU: + sys.stderr.write(terminal_str) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode.console.write(terminal_str) + + # Redraw the prompt and input lines + rl_force_redisplay() - # Redraw the prompt and input lines - rl_force_redisplay() + self._terminal_lock.release() + + else: + raise RuntimeError("another thread holds _terminal_lock") def _async_update_prompt(self, new_prompt: str) -> None: """ @@ -3276,25 +3286,34 @@ Script should contain one command per line, just like command would be typed in first, which ensures a prompt is onscreen :param new_prompt: what to change the prompt to + :raises RuntimeError if called while another thread holds _terminal_lock """ if not (vt100_support and self.use_rawinput): return - # Generate a string to clear the prompt and input lines - terminal_str = self._clear_input_lines_str() + # Sanity check that can't fail if self._terminal_lock was acquired before calling this function + if self._terminal_lock.acquire(blocking=False): - # Set the new prompt now that _clear_input_lines_str is done using the old prompt - self.prompt = new_prompt - rl_set_prompt(self.prompt) + # Generate a string to clear the prompt and input lines + terminal_str = self._clear_input_lines_str() - # Print terminal_str to erase the lines - if rl_type == RlType.GNU: - sys.stderr.write(terminal_str) - elif rl_type == RlType.PYREADLINE: - readline.rl.mode.console.write(terminal_str) + # Set the new prompt now that _clear_input_lines_str is done using the old prompt + self.prompt = new_prompt + rl_set_prompt(self.prompt) + + # Print terminal_str to erase the lines + if rl_type == RlType.GNU: + sys.stderr.write(terminal_str) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode.console.write(terminal_str) + + # Redraw the prompt and input lines + rl_force_redisplay() - # Redraw the prompt and input lines - rl_force_redisplay() + self._terminal_lock.release() + + else: + raise RuntimeError("another thread holds _terminal_lock") @staticmethod def set_window_title(title: str) -> None: -- cgit v1.2.1 From 10dd79e9dee5f0286cd413d513645083e7c44e8a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 25 Sep 2018 00:19:20 -0400 Subject: Made async stuff public --- cmd2/cmd2.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index bf1e01df..ba8e47ed 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -530,7 +530,7 @@ class Cmd(cmd.Cmd): # This lock should be acquired before doing any asynchronous changes to the terminal to # ensure the updates to the terminal don't interfere with the input being typed. It can be # acquired any time there is a readline prompt on screen. - self._terminal_lock = threading.RLock() + self.terminal_lock = threading.RLock() # ----- Methods related to presenting output to the user ----- @@ -2085,10 +2085,10 @@ class Cmd(cmd.Cmd): if self.use_rawinput: try: if sys.stdin.isatty(): - # Wrap in try since _terminal_lock may not be locked when this function is called from unit tests + # Wrap in try since terminal_lock may not be locked when this function is called from unit tests try: # A prompt is about to be drawn. Allow asynchronous changes to the terminal. - self._terminal_lock.release() + self.terminal_lock.release() except RuntimeError: pass @@ -2104,7 +2104,7 @@ class Cmd(cmd.Cmd): finally: if sys.stdin.isatty(): # The prompt is gone. Do not allow asynchronous changes to the terminal. - self._terminal_lock.acquire() + self.terminal_lock.acquire() else: if self.stdin.isatty(): # on a tty, print the prompt first, then read the line @@ -3237,25 +3237,25 @@ Script should contain one command per line, just like command would be typed in return terminal_str - def _async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: + def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: """ Used to display an important message to the user while they are at the prompt in between commands. To the user it appears as if an alert message is printed above the prompt and their current input text and cursor location is left alone. - IMPORTANT: Do not call this unless you have acquired self._terminal_lock + IMPORTANT: Do not call this unless you have acquired self.terminal_lock first, which ensures a prompt is onscreen :param alert_msg: the message to display to the user :param new_prompt: if you also want to change the prompt that is displayed, then include it here see async_update_prompt() docstring for guidance on updating a prompt - :raises RuntimeError if called while another thread holds _terminal_lock + :raises RuntimeError if called while another thread holds terminal_lock """ if not (vt100_support and self.use_rawinput): return - # Sanity check that can't fail if self._terminal_lock was acquired before calling this function - if self._terminal_lock.acquire(blocking=False): + # Sanity check that can't fail if self.terminal_lock was acquired before calling this function + if self.terminal_lock.acquire(blocking=False): # Generate a string to clear the prompt and input lines and replace with the alert terminal_str = self._clear_input_lines_str() @@ -3275,12 +3275,12 @@ Script should contain one command per line, just like command would be typed in # Redraw the prompt and input lines rl_force_redisplay() - self._terminal_lock.release() + self.terminal_lock.release() else: - raise RuntimeError("another thread holds _terminal_lock") + raise RuntimeError("another thread holds terminal_lock") - def _async_update_prompt(self, new_prompt: str) -> None: + def async_update_prompt(self, new_prompt: str) -> None: """ Updates the prompt while the user is still typing at it. This is good for alerting the user to system changes dynamically in between commands. For instance you could alter the color of the prompt to indicate @@ -3288,17 +3288,17 @@ Script should contain one command per line, just like command would be typed in it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will be shifted and the update will not be seamless. - IMPORTANT: Do not call this unless you have acquired self._terminal_lock + IMPORTANT: Do not call this unless you have acquired self.terminal_lock first, which ensures a prompt is onscreen :param new_prompt: what to change the prompt to - :raises RuntimeError if called while another thread holds _terminal_lock + :raises RuntimeError if called while another thread holds terminal_lock """ if not (vt100_support and self.use_rawinput): return - # Sanity check that can't fail if self._terminal_lock was acquired before calling this function - if self._terminal_lock.acquire(blocking=False): + # Sanity check that can't fail if self.terminal_lock was acquired before calling this function + if self.terminal_lock.acquire(blocking=False): # Generate a string to clear the prompt and input lines terminal_str = self._clear_input_lines_str() @@ -3316,10 +3316,10 @@ Script should contain one command per line, just like command would be typed in # Redraw the prompt and input lines rl_force_redisplay() - self._terminal_lock.release() + self.terminal_lock.release() else: - raise RuntimeError("another thread holds _terminal_lock") + raise RuntimeError("another thread holds terminal_lock") @staticmethod def set_window_title(title: str) -> None: @@ -3368,7 +3368,7 @@ Script should contain one command per line, just like command would be typed in signal.signal(signal.SIGINT, self.sigint_handler) # Grab terminal lock before the prompt has been drawn by readline - self._terminal_lock.acquire() + self.terminal_lock.acquire() # Always run the preloop first for func in self._preloop_hooks: @@ -3397,7 +3397,7 @@ Script should contain one command per line, just like command would be typed in # Release terminal lock now that postloop code should have stopped any terminal updater threads # This will also zero the lock count in case cmdloop() is called again - self._terminal_lock.release() + self.terminal_lock.release() # Restore the original signal handler signal.signal(signal.SIGINT, original_sigint_handler) -- cgit v1.2.1 From 87551d5e9675e77cfbb7be7c499f46d9c6cbe559 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 25 Sep 2018 18:12:34 -0400 Subject: Removed terminal functions from code coverage --- cmd2/cmd2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 704773cc..2adfe743 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3131,7 +3131,7 @@ Script should contain one command per line, just like command would be typed in runner = unittest.TextTestRunner() runner.run(testcase) - def _clear_input_lines_str(self) -> str: + def _clear_input_lines_str(self) -> str: # pragma: no cover """ Returns a string that if printed will clear the prompt and input lines in the terminal, leaving the cursor at the beginning of the first input line @@ -3175,7 +3175,7 @@ Script should contain one command per line, just like command would be typed in return terminal_str - def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: + def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover """ Used to display an important message to the user while they are at the prompt in between commands. To the user it appears as if an alert message is printed above the prompt and their current input @@ -3218,7 +3218,7 @@ Script should contain one command per line, just like command would be typed in else: raise RuntimeError("another thread holds terminal_lock") - def async_update_prompt(self, new_prompt: str) -> None: + def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover """ Updates the prompt while the user is still typing at it. This is good for alerting the user to system changes dynamically in between commands. For instance you could alter the color of the prompt to indicate @@ -3260,7 +3260,7 @@ Script should contain one command per line, just like command would be typed in raise RuntimeError("another thread holds terminal_lock") @staticmethod - def set_window_title(title: str) -> None: + def set_window_title(title: str) -> None: # pragma: no cover """ Sets the terminal window title :param title: the new window title -- cgit v1.2.1 From 5f46487bf7909045f217c89c7ade2d9fa89ebd7b Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 25 Sep 2018 18:58:17 -0400 Subject: Simplified async_update_prompt --- cmd2/cmd2.py | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) (limited to 'cmd2/cmd2.py') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 2adfe743..00e113a9 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3197,7 +3197,8 @@ Script should contain one command per line, just like command would be typed in # Generate a string to clear the prompt and input lines and replace with the alert terminal_str = self._clear_input_lines_str() - terminal_str += alert_msg + '\n' + if alert_msg: + terminal_str += alert_msg + '\n' # Set the new prompt now that _clear_input_lines_str is done using the old prompt if new_prompt is not None: @@ -3230,34 +3231,8 @@ Script should contain one command per line, just like command would be typed in first, which ensures a prompt is onscreen :param new_prompt: what to change the prompt to - :raises RuntimeError if called while another thread holds terminal_lock """ - if not (vt100_support and self.use_rawinput): - return - - # Sanity check that can't fail if self.terminal_lock was acquired before calling this function - if self.terminal_lock.acquire(blocking=False): - - # Generate a string to clear the prompt and input lines - terminal_str = self._clear_input_lines_str() - - # Set the new prompt now that _clear_input_lines_str is done using the old prompt - self.prompt = new_prompt - rl_set_prompt(self.prompt) - - # Print terminal_str to erase the lines - if rl_type == RlType.GNU: - sys.stderr.write(terminal_str) - elif rl_type == RlType.PYREADLINE: - readline.rl.mode.console.write(terminal_str) - - # Redraw the prompt and input lines - rl_force_redisplay() - - self.terminal_lock.release() - - else: - raise RuntimeError("another thread holds terminal_lock") + self.async_alert('', new_prompt) @staticmethod def set_window_title(title: str) -> None: # pragma: no cover -- cgit v1.2.1