From 801bab847341fb9a35d10f1d0b4a629a4fc8f14c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 19 Dec 2019 16:04:06 -0500 Subject: Changed allow_ansi to allow_style for accuracy in what types of ANSI escape sequences are handled --- cmd2/ansi.py | 50 +++++++++++++++++++++++++++----------------------- cmd2/cmd2.py | 48 ++++++++++++++++++++++++------------------------ cmd2/transcript.py | 8 ++++---- cmd2/utils.py | 10 +++++----- 4 files changed, 60 insertions(+), 56 deletions(-) (limited to 'cmd2') diff --git a/cmd2/ansi.py b/cmd2/ansi.py index c875a9d1..57b204ca 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -1,5 +1,8 @@ # coding=utf-8 -"""Support for ANSI escape sequences which are used for things like applying style to text""" +""" +Support for ANSI escape sequences which are used for things like applying style to text, +setting the window title, and asynchronous alerts. + """ import functools import re from typing import Any, IO @@ -11,16 +14,17 @@ from wcwidth import wcswidth # On Windows, filter ANSI escape codes out of text sent to stdout/stderr, and replace them with equivalent Win32 calls colorama.init(strip=False) -# Values for allow_ansi setting -ANSI_NEVER = 'Never' -ANSI_TERMINAL = 'Terminal' -ANSI_ALWAYS = 'Always' +# Values for allow_style setting +STYLE_NEVER = 'Never' +STYLE_TERMINAL = 'Terminal' +STYLE_ALWAYS = 'Always' -# Controls when ANSI escape sequences are allowed in output -allow_ansi = ANSI_TERMINAL +# Controls when ANSI style style sequences are allowed in output +allow_style = STYLE_TERMINAL -# Regular expression to match ANSI escape sequences -ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m') +# Regular expression to match ANSI style sequences +# This matches: colorama.ansi.CSI + digit(s) + m +ANSI_STYLE_RE = re.compile(r'\033\[[0-9]+m') # Foreground color presets FG_COLORS = { @@ -71,41 +75,41 @@ RESET_ALL = Style.RESET_ALL BRIGHT = Style.BRIGHT NORMAL = Style.NORMAL -# ANSI escape sequences not provided by colorama +# ANSI style sequences not provided by colorama UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4) UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24) -def strip_ansi(text: str) -> str: +def strip_style(text: str) -> str: """ - Strip ANSI escape sequences from a string. + Strip ANSI style sequences from a string. - :param text: string which may contain ANSI escape sequences - :return: the same string with any ANSI escape sequences removed + :param text: string which may contain ANSI style sequences + :return: the same string with any ANSI style sequences removed """ - return ANSI_ESCAPE_RE.sub('', text) + return ANSI_STYLE_RE.sub('', text) def ansi_safe_wcswidth(text: str) -> int: """ - Wrap wcswidth to make it compatible with strings that contains ANSI escape sequences + Wrap wcswidth to make it compatible with strings that contains ANSI style sequences :param text: the string being measured """ - # Strip ANSI escape sequences since they cause wcswidth to return -1 - return wcswidth(strip_ansi(text)) + # Strip ANSI style sequences since they cause wcswidth to return -1 + return wcswidth(strip_style(text)) def ansi_aware_write(fileobj: IO, msg: str) -> None: """ - Write a string to a fileobject and strip its ANSI escape sequences if required by allow_ansi setting + Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting :param fileobj: the file object being written to :param msg: the string being written """ - if allow_ansi.lower() == ANSI_NEVER.lower() or \ - (allow_ansi.lower() == ANSI_TERMINAL.lower() and not fileobj.isatty()): - msg = strip_ansi(msg) + if allow_style.lower() == STYLE_NEVER.lower() or \ + (allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()): + msg = strip_style(msg) fileobj.write(msg) @@ -176,7 +180,7 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin additions.append(UNDERLINE_ENABLE) removals.append(UNDERLINE_DISABLE) - # Combine the ANSI escape sequences with the text + # Combine the ANSI style sequences with the text return "".join(additions) + text + "".join(removals) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6c4fdcbd..4adf349b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -206,11 +206,11 @@ class Cmd(cmd.Cmd): # To make an attribute settable with the "do_set" command, add it to this ... self.settable = \ { - # allow_ansi is a special case in which it's an application-wide setting defined in ansi.py - 'allow_ansi': ('Allow ANSI escape sequences in output ' - '(valid values: {}, {}, {})'.format(ansi.ANSI_TERMINAL, - ansi.ANSI_ALWAYS, - ansi.ANSI_NEVER)), + # allow_style is a special case in which it's an application-wide setting defined in ansi.py + 'allow_style': ('Allow ANSI style sequences in output ' + '(valid values: {}, {}, {})'.format(ansi.STYLE_TERMINAL, + ansi.STYLE_ALWAYS, + ansi.STYLE_NEVER)), 'continuation_prompt': 'On 2nd+ line of input', 'debug': 'Show full error stack on error', 'echo': 'Echo command issued into output', @@ -366,7 +366,7 @@ class Cmd(cmd.Cmd): else: # Here is the meaning of the various flags we are using with the less command: # -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped - # -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed) + # -R causes ANSI "style" escape sequences to be output in raw form (i.e. colors are displayed) # -X disables sending the termcap initialization and deinitialization strings to the terminal # -F causes less to automatically exit if the entire file can be displayed on the first screen self.pager = 'less -RXF' @@ -395,23 +395,23 @@ class Cmd(cmd.Cmd): # ----- Methods related to presenting output to the user ----- @property - def allow_ansi(self) -> str: - """Read-only property needed to support do_set when it reads allow_ansi""" - return ansi.allow_ansi + def allow_style(self) -> str: + """Read-only property needed to support do_set when it reads allow_style""" + return ansi.allow_style - @allow_ansi.setter - def allow_ansi(self, new_val: str) -> None: - """Setter property needed to support do_set when it updates allow_ansi""" + @allow_style.setter + def allow_style(self, new_val: str) -> None: + """Setter property needed to support do_set when it updates allow_style""" new_val = new_val.lower() - if new_val == ansi.ANSI_TERMINAL.lower(): - ansi.allow_ansi = ansi.ANSI_TERMINAL - elif new_val == ansi.ANSI_ALWAYS.lower(): - ansi.allow_ansi = ansi.ANSI_ALWAYS - elif new_val == ansi.ANSI_NEVER.lower(): - ansi.allow_ansi = ansi.ANSI_NEVER + if new_val == ansi.STYLE_TERMINAL.lower(): + ansi.allow_style = ansi.STYLE_TERMINAL + elif new_val == ansi.STYLE_ALWAYS.lower(): + ansi.allow_style = ansi.STYLE_ALWAYS + elif new_val == ansi.STYLE_NEVER.lower(): + ansi.allow_style = ansi.STYLE_NEVER else: - self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL, - ansi.ANSI_ALWAYS, ansi.ANSI_NEVER)) + self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL, + ansi.STYLE_ALWAYS, ansi.STYLE_NEVER)) def _completion_supported(self) -> bool: """Return whether tab completion is supported""" @@ -419,14 +419,14 @@ class Cmd(cmd.Cmd): @property def visible_prompt(self) -> str: - """Read-only property to get the visible prompt with any ANSI escape codes stripped. + """Read-only property to get the visible prompt with any ANSI style escape codes stripped. Used by transcript testing to make it easier and more reliable when users are doing things like coloring the prompt using ANSI color codes. :return: prompt stripped of any ANSI escape codes """ - return ansi.strip_ansi(self.prompt) + return ansi.strip_style(self.prompt) def poutput(self, msg: Any = '', *, end: str = '\n') -> None: """Print message to self.stdout and appends a newline by default @@ -551,8 +551,8 @@ class Cmd(cmd.Cmd): # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python) # Also only attempt to use a pager if actually running in a real fully functional terminal if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script(): - if ansi.allow_ansi.lower() == ansi.ANSI_NEVER.lower(): - msg_str = ansi.strip_ansi(msg_str) + if ansi.allow_style.lower() == ansi.STYLE_NEVER.lower(): + msg_str = ansi.strip_style(msg_str) msg_str += end pager = self.pager diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 25a79310..940c97db 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -56,13 +56,13 @@ class Cmd2TestCase(unittest.TestCase): def _test_transcript(self, fname: str, transcript): line_num = 0 finished = False - line = ansi.strip_ansi(next(transcript)) + line = ansi.strip_style(next(transcript)) line_num += 1 while not finished: # Scroll forward to where actual commands begin while not line.startswith(self.cmdapp.visible_prompt): try: - line = ansi.strip_ansi(next(transcript)) + line = ansi.strip_style(next(transcript)) except StopIteration: finished = True break @@ -89,7 +89,7 @@ class Cmd2TestCase(unittest.TestCase): result = self.cmdapp.stdout.read() stop_msg = 'Command indicated application should quit, but more commands in transcript' # Read the expected result from transcript - if ansi.strip_ansi(line).startswith(self.cmdapp.visible_prompt): + if ansi.strip_style(line).startswith(self.cmdapp.visible_prompt): message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format( fname, line_num, command, result) self.assertTrue(not (result.strip()), message) @@ -97,7 +97,7 @@ class Cmd2TestCase(unittest.TestCase): self.assertFalse(stop, stop_msg) continue expected = [] - while not ansi.strip_ansi(line).startswith(self.cmdapp.visible_prompt): + while not ansi.strip_style(line).startswith(self.cmdapp.visible_prompt): expected.append(line) try: line = next(transcript) diff --git a/cmd2/utils.py b/cmd2/utils.py index 9dd7a30b..ddb9f3b5 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -641,7 +641,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ Align text for display within a given width. Supports characters with display widths greater than 1. - ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is + ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is supported. If text has line breaks, then each line is aligned independently. There are convenience wrappers around this function: align_left(), align_center(), and align_right() @@ -688,7 +688,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', text_buf.write('\n') # Use ansi_safe_wcswidth to support characters with display widths - # greater than 1 as well as ANSI escape sequences + # greater than 1 as well as ANSI style sequences line_width = ansi.ansi_safe_wcswidth(line) if line_width == -1: raise(ValueError("Text to align contains an unprintable character")) @@ -728,7 +728,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ Left align text for display within a given width. Supports characters with display widths greater than 1. - ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is + ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is supported. If text has line breaks, then each line is aligned independently. :param text: text to left align (can contain multiple lines) @@ -746,7 +746,7 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ Center text for display within a given width. Supports characters with display widths greater than 1. - ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is + ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is supported. If text has line breaks, then each line is aligned independently. :param text: text to center (can contain multiple lines) @@ -764,7 +764,7 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ Right align text for display within a given width. Supports characters with display widths greater than 1. - ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is + ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is supported. If text has line breaks, then each line is aligned independently. :param text: text to right align (can contain multiple lines) -- cgit v1.2.1