From af47fee4f3c0ea59106e5cb6137263a94177d773 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 5 Dec 2019 20:27:43 -0500 Subject: Enabled line buffering when redirecting output to a file --- CHANGELOG.md | 5 ++--- cmd2/cmd2.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c423836..d773f784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ ## 0.9.22 (TBD, 2019) * Bug Fixes * Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files -* Other - * Removed `bold=True` from `ansi.style_success` because it was difficult for red-greed colorblind users to - distinguish that color from the `ansi.style_warning` color in certain terminals. +* Enhancements + * Enabled line buffering when redirecting output to a file ## 0.9.21 (November 26, 2019) * Bug Fixes diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 753fb5ab..127a8bac 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1874,22 +1874,26 @@ class Cmd(cmd.Cmd): self.perror("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies") redir_error = True + # Redirecting to a file elif statement.output_to: - # going to a file - mode = 'w' - # statement.output can only contain - # REDIRECTION_APPEND or REDIRECTION_OUTPUT + # statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT if statement.output == constants.REDIRECTION_APPEND: mode = 'a' + else: + mode = 'w' + mode += 't' + try: - new_stdout = open(utils.strip_quotes(statement.output_to), mode) + # Use line buffering + new_stdout = open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1) saved_state.redirecting = True sys.stdout = self.stdout = new_stdout except OSError as ex: self.pexcept('Failed to redirect because - {}'.format(ex)) redir_error = True + + # Redirecting to a paste buffer else: - # going to a paste buffer new_stdout = tempfile.TemporaryFile(mode="w+") saved_state.redirecting = True sys.stdout = self.stdout = new_stdout -- cgit v1.2.1 From 8862e278cf30b9bb09e19c5e13024cf6f6d70825 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 6 Dec 2019 12:28:05 -0500 Subject: Removed redundant line of code --- cmd2/cmd2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 127a8bac..bd581919 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1881,7 +1881,6 @@ class Cmd(cmd.Cmd): mode = 'a' else: mode = 'w' - mode += 't' try: # Use line buffering -- cgit v1.2.1 From ab35dd08094fff561f6a54079e48c52b44fab4d9 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 6 Dec 2019 12:41:32 -0500 Subject: Added missing information to 0.9.12 change log entry --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d773f784..4c193e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -249,11 +249,13 @@ * Removed *** from beginning of error messages printed by `do_help()` and `default()` * Significantly refactored ``cmd.Cmd`` class so that all class attributes got converted to instance attributes, also: * Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments - to ``cmd.Cmd.__init__()` + to ``cmd2.Cmd.__init__()` * A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them * ``self.pipe_proc`` is now called ``self.cur_pipe_proc_reader`` and is a ``ProcReader`` class. * Shell commands and commands being piped to while in a *pyscript* will function as if their output is going to a pipe and not a tty. This was necessary to be able to capture their output. + * Removed `reserved_words` class attribute due to lack of use + * Removed `keywords` instance attribute due to lack of use ## 0.9.11 (March 13, 2019) * Bug Fixes -- cgit v1.2.1 From 0aac6cee56a92bb6358106329f4f0c20e85bb7bc Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 6 Dec 2019 12:44:56 -0500 Subject: Fixed typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c193e72..1ac01813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -249,7 +249,7 @@ * Removed *** from beginning of error messages printed by `do_help()` and `default()` * Significantly refactored ``cmd.Cmd`` class so that all class attributes got converted to instance attributes, also: * Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments - to ``cmd2.Cmd.__init__()` + to ``cmd2.Cmd.__init__()`` * A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them * ``self.pipe_proc`` is now called ``self.cur_pipe_proc_reader`` and is a ``ProcReader`` class. * Shell commands and commands being piped to while in a *pyscript* will function as if their output is going -- cgit v1.2.1 From cda57dc1a1859408fb25d31178ad0f6e77ede902 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 15:23:58 -0500 Subject: Updated center_text to support ansi escape sequences and characters with display widths greater than 1. Also added left and right justification functions. --- cmd2/cmd2.py | 6 +- cmd2/parsing.py | 9 +-- cmd2/utils.py | 164 +++++++++++++++++++++++++++++++++++++++++++++++----- tests/test_utils.py | 25 ++++---- 4 files changed, 167 insertions(+), 37 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index bd581919..9c48b222 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3715,7 +3715,7 @@ class Cmd(cmd.Cmd): verinfo = ".".join(map(str, sys.version_info[:3])) num_transcripts = len(transcripts_expanded) plural = '' if len(transcripts_expanded) == 1 else 's' - self.poutput(ansi.style(utils.center_text('cmd2 transcript test', pad='='), bold=True)) + self.poutput(ansi.style(utils.center_text(' cmd2 transcript test ', fill_char='='), bold=True)) self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__, rl_type)) self.poutput('cwd: {}'.format(os.getcwd())) @@ -3733,8 +3733,8 @@ class Cmd(cmd.Cmd): execution_time = time.time() - start_time if test_results.wasSuccessful(): ansi.ansi_aware_write(sys.stderr, stream.read()) - finish_msg = '{0} transcript{1} passed in {2:.3f} seconds'.format(num_transcripts, plural, execution_time) - finish_msg = ansi.style_success(utils.center_text(finish_msg, pad='=')) + finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time) + finish_msg = ansi.style_success(utils.center_text(finish_msg, fill_char='=')) self.poutput(finish_msg) else: # Strip off the initial traceback which isn't particularly useful for end users diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 4e690b0b..cef0b088 100755 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -13,9 +13,10 @@ from . import utils def shlex_split(str_to_split: str) -> List[str]: - """A wrapper around shlex.split() that uses cmd2's preferred arguments. + """ + A wrapper around shlex.split() that uses cmd2's preferred arguments. + This allows other classes to easily call split() the same way StatementParser does. - This allows other classes to easily call split() the same way StatementParser does :param str_to_split: the string being split :return: A list of tokens """ @@ -26,8 +27,8 @@ def shlex_split(str_to_split: str) -> List[str]: class MacroArg: """ Information used to replace or unescape arguments in a macro value when the macro is resolved - Normal argument syntax : {5} - Escaped argument syntax: {{5}} + Normal argument syntax: {5} + Escaped argument syntax: {{5}} """ # The starting index of this argument in the macro value start_index = attr.ib(validator=attr.validators.instance_of(int)) diff --git a/cmd2/utils.py b/cmd2/utils.py index a1a0d377..c8ba5816 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -5,11 +5,11 @@ import collections import glob import os import re -import shutil import subprocess import sys import threading import unicodedata +from enum import Enum from typing import Any, Iterable, List, Optional, TextIO, Union from . import constants @@ -363,21 +363,6 @@ def get_exes_in_path(starts_with: str) -> List[str]: return list(exes_set) -def center_text(msg: str, *, pad: str = ' ') -> str: - """Centers text horizontally for display within the current terminal, optionally padding both sides. - - :param msg: message to display in the center - :param pad: if provided, the first character will be used to pad both sides of the message - :return: centered message, optionally padded on both sides with pad_char - """ - term_width = shutil.get_terminal_size().columns - surrounded_msg = ' {} '.format(msg) - if not pad: - pad = ' ' - fill_char = pad[:1] - return surrounded_msg.center(term_width, fill_char) - - class StdSim(object): """ Class to simulate behavior of sys.stdout or sys.stderr. @@ -644,3 +629,150 @@ def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against :return: a list of possible tab completions """ return [cur_match for cur_match in match_against if cur_match.startswith(text)] + + +class TextAlignment(Enum): + LEFT = 1 + CENTER = 2 + RIGHT = 3 + + +def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, + alignment: TextAlignment) -> 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 + supported. Each line in text will be aligned independently. + + There are convenience wrappers around this function: ljustify_text(), center_text(), and rjustify_text() + + :param text: text to align (Can contain multiple lines) + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) + :param width: display width of the aligned text. Defaults to width of the terminal. + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will + be converted to a space. + :param alignment: how to align the text + :return: aligned text + :raises: ValueError if text or fill_char contains an unprintable character + TypeError if fill_char is more than one character + + """ + import io + import shutil + + from . import ansi + + # Handle tabs + text.replace('\t', ' ' * tab_width) + if fill_char == '\t': + fill_char = ' ' + + if len(fill_char) != 1: + raise ValueError("Fill character must be exactly one character long") + + fill_char_width = ansi.ansi_safe_wcswidth(fill_char) + if fill_char_width == -1: + raise (ValueError("Fill character is an unprintable character")) + + if text: + lines = text.splitlines() + else: + lines = [''] + + text_buf = io.StringIO() + + for index, line in enumerate(lines): + if index > 0: + text_buf.write('\n') + + # Use ansi_safe_wcswidth to support characters with display widths greater than 1 + # as well as ANSI escape sequences + line_width = ansi.ansi_safe_wcswidth(line) + if line_width == -1: + # This can happen if text contains characters like newlines or tabs + raise(ValueError("Text to align contains an unprintable character")) + + if width is None: + width = shutil.get_terminal_size().columns + + # Check if line is wider than the desired final width + if width <= line_width: + text_buf.write(line) + continue + + # Calculate how wide each side of filling needs to be + total_fill_width = width - line_width + + if alignment == TextAlignment.LEFT: + left_fill_width = 0 + right_fill_width = total_fill_width + elif alignment == TextAlignment.CENTER: + left_fill_width = total_fill_width // 2 + right_fill_width = total_fill_width - left_fill_width + else: + left_fill_width = total_fill_width + right_fill_width = 0 + + # Determine how many fill characters are needed to cover the width + left_fill = (left_fill_width // fill_char_width) * fill_char + right_fill = (right_fill_width // fill_char_width) * fill_char + + # In cases where the fill character display width didn't divide evenly into + # the gaps being filled, pad the remainder with spaces. + left_fill += ' ' * (left_fill_width - ansi.ansi_safe_wcswidth(left_fill)) + right_fill += ' ' * (right_fill_width - ansi.ansi_safe_wcswidth(right_fill)) + + text_buf.write(left_fill + line + right_fill) + + return text_buf.getvalue() + + +def ljustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: + """ + Left justify 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 + supported. Each line in text will be aligned independently. + + :param text: text to left justify (Can contain multiple lines) + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) + :param width: display width of the aligned text. Defaults to width of the terminal. + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will + be converted to a space. + :return: left-justified text + """ + return align_text(text, fill_char=fill_char, width=width, + tab_width=tab_width, alignment=TextAlignment.LEFT) + + +def center_text(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 + supported. Each line in text will be aligned independently. + + :param text: text to center (Can contain multiple lines) + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) + :param width: display width of the aligned text. Defaults to width of the terminal. + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will + be converted to a space. + :return: centered text + """ + return align_text(text, fill_char=fill_char, width=width, + tab_width=tab_width, alignment=TextAlignment.CENTER) + + +def rjustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: + """ + Right justify 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 + supported. Each line in text will be aligned independently. + + :param text: text to right justify (Can contain multiple lines) + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) + :param width: display width of the aligned text. Defaults to width of the terminal. + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will + be converted to a space. + :return: right-justified text + """ + return align_text(text, fill_char=fill_char, width=width, + tab_width=tab_width, alignment=TextAlignment.RIGHT) diff --git a/tests/test_utils.py b/tests/test_utils.py index e4b9169c..2c43371f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -293,24 +293,21 @@ def test_context_flag_exit_err(context_flag): context_flag.__exit__() -def test_center_text_pad_none(): +def test_center_text_pad_equals(): msg = 'foo' - centered = cu.center_text(msg, pad=None) - expected_center = ' ' + msg + ' ' - assert expected_center in centered + fill_char = '=' + centered = cu.center_text(msg, fill_char=fill_char) + assert msg in centered + assert centered.startswith(fill_char) + assert centered.endswith(fill_char) letters_in_centered = set(centered) letters_in_msg = set(msg) assert len(letters_in_centered) == len(letters_in_msg) + 1 -def test_center_text_pad_equals(): + +def test_center_text_pad_blank(): msg = 'foo' - pad = '=' - centered = cu.center_text(msg, pad=pad) - expected_center = ' ' + msg + ' ' - assert expected_center in centered - assert centered.startswith(pad) - assert centered.endswith(pad) - letters_in_centered = set(centered) - letters_in_msg = set(msg) - assert len(letters_in_centered) == len(letters_in_msg) + 2 + fill_char = '' + with pytest.raises(ValueError): + cu.center_text(msg, fill_char=fill_char) -- cgit v1.2.1 From 6f16e671971a9fac4edfda9c0117ceaeb5e6487e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 16:24:54 -0500 Subject: Adding unit tests for text alignment functions --- cmd2/utils.py | 28 ++++----- tests/test_utils.py | 168 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 165 insertions(+), 31 deletions(-) diff --git a/cmd2/utils.py b/cmd2/utils.py index c8ba5816..d7c9ff19 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -642,7 +642,7 @@ def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, """ 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 - supported. Each line in text will be aligned independently. + supported. If text has line breaks, then each line is aligned independently. There are convenience wrappers around this function: ljustify_text(), center_text(), and rjustify_text() @@ -653,9 +653,8 @@ def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, be converted to a space. :param alignment: how to align the text :return: aligned text - :raises: ValueError if text or fill_char contains an unprintable character - TypeError if fill_char is more than one character - + :raises: TypeError if fill_char is more than one character + ValueError if text or fill_char contains an unprintable character """ import io import shutil @@ -663,12 +662,12 @@ def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, from . import ansi # Handle tabs - text.replace('\t', ' ' * tab_width) + text = text.replace('\t', ' ' * tab_width) if fill_char == '\t': fill_char = ' ' if len(fill_char) != 1: - raise ValueError("Fill character must be exactly one character long") + raise TypeError("Fill character must be exactly one character long") fill_char_width = ansi.ansi_safe_wcswidth(fill_char) if fill_char_width == -1: @@ -679,22 +678,21 @@ def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, else: lines = [''] + if width is None: + width = shutil.get_terminal_size().columns + text_buf = io.StringIO() for index, line in enumerate(lines): if index > 0: text_buf.write('\n') - # Use ansi_safe_wcswidth to support characters with display widths greater than 1 - # as well as ANSI escape sequences + # Use ansi_safe_wcswidth to support characters with display widths + # greater than 1 as well as ANSI escape sequences line_width = ansi.ansi_safe_wcswidth(line) if line_width == -1: - # This can happen if text contains characters like newlines or tabs raise(ValueError("Text to align contains an unprintable character")) - if width is None: - width = shutil.get_terminal_size().columns - # Check if line is wider than the desired final width if width <= line_width: text_buf.write(line) @@ -731,7 +729,7 @@ def ljustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = Non """ Left justify 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 - supported. Each line in text will be aligned independently. + supported. If text has line breaks, then each line is aligned independently. :param text: text to left justify (Can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) @@ -748,7 +746,7 @@ def center_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, """ 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 - supported. Each line in text will be aligned independently. + supported. If text has line breaks, then each line is aligned independently. :param text: text to center (Can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) @@ -765,7 +763,7 @@ def rjustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = Non """ Right justify 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 - supported. Each line in text will be aligned independently. + supported. If text has line breaks, then each line is aligned independently. :param text: text to right justify (Can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) diff --git a/tests/test_utils.py b/tests/test_utils.py index 2c43371f..1327064f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -293,21 +293,157 @@ def test_context_flag_exit_err(context_flag): context_flag.__exit__() -def test_center_text_pad_equals(): - msg = 'foo' - fill_char = '=' - centered = cu.center_text(msg, fill_char=fill_char) - assert msg in centered - assert centered.startswith(fill_char) - assert centered.endswith(fill_char) - letters_in_centered = set(centered) - letters_in_msg = set(msg) - assert len(letters_in_centered) == len(letters_in_msg) + 1 - - -def test_center_text_pad_blank(): - msg = 'foo' - fill_char = '' +def test_align_text_fill_char_is_tab(): + text = 'foo' + fill_char = '\t' + width = 5 + aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) + assert aligned == text + ' ' + +def test_align_text_fill_char_is_too_long(): + text = 'foo' + fill_char = 'fill' + width = 5 + with pytest.raises(TypeError): + cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) +def test_align_text_fill_char_is_unprintable(): + text = 'foo' + fill_char = '\n' + width = 5 with pytest.raises(ValueError): - cu.center_text(msg, fill_char=fill_char) + cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) + +def test_align_text_has_tabs(): + text = '\t\tfoo' + fill_char = '-' + width = 10 + aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT, tab_width=2) + assert aligned == ' ' + 'foo' + '---' + +def test_align_text_blank(): + text = '' + fill_char = '-' + width = 5 + aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) + assert aligned == text + fill_char * width + +def test_align_text_wider_than_width(): + text = 'long' + fill_char = '-' + width = 3 + aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) + assert aligned == text + +def test_align_text_term_width(): + import shutil + from cmd2 import ansi + text = 'foo' + fill_char = ' ' + + term_width = shutil.get_terminal_size().columns + expected_fill = (term_width - ansi.ansi_safe_wcswidth(text)) * fill_char + + aligned = cu.align_text(text, fill_char=fill_char, alignment=cu.TextAlignment.LEFT) + assert aligned == text + expected_fill + +def test_left_text(): + text = 'foo' + fill_char = '-' + width = 5 + aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + assert aligned == text + fill_char + fill_char + +def test_left_text_multiline(): + text = "foo\nshoes" + fill_char = '-' + width = 7 + aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + assert aligned == ('foo----\n' + 'shoes--') + +def test_left_text_asian_fill(): + """Test fill_char with display width greater than 1""" + text = 'foo' + fill_char = '苹' + width = 5 + aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + assert aligned == text + fill_char + +def test_left_text_asian_fill_needs_padding(): + """Test when fill_char's display width does not divide evenly into gap""" + text = 'foo' + fill_char = '苹' + width = 6 + aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + assert aligned == text + fill_char + ' ' + +def test_center_text(): + text = 'foo' + fill_char = '-' + width = 5 + aligned = cu.center_text(text,fill_char=fill_char, width=width) + assert aligned == fill_char + text + fill_char + +def test_center_text_multiline(): + text = "foo\nshoes" + fill_char = '-' + width = 7 + aligned = cu.center_text(text, fill_char=fill_char, width=width) + assert aligned == ('--foo--\n' + '-shoes-') + +def test_center_text_asian_fill(): + """Test fill_char with display width greater than 1""" + text = 'foo' + fill_char = '苹' + width = 7 + aligned = cu.center_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + text + fill_char + +def test_center_text_asian_fill_needs_right_padding(): + """Test when fill_char's display width does not divide evenly into right gap""" + text = 'foo' + fill_char = '苹' + width = 8 + aligned = cu.center_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + text + fill_char + ' ' + +def test_center_text_asian_fill_needs_left_and_right_padding(): + """Test when fill_char's display width does not divide evenly into either gap""" + text = 'foo' + fill_char = '苹' + width = 9 + aligned = cu.center_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + ' ' + text + fill_char + ' ' + +def test_right_text(): + text = 'foo' + fill_char = '-' + width = 5 + aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + fill_char + text + +def test_right_text_multiline(): + text = "foo\nshoes" + fill_char = '-' + width = 7 + aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + assert aligned == ('----foo\n' + '--shoes') + +def test_right_text_asian_fill(): + """Test fill_char with display width greater than 1""" + text = 'foo' + fill_char = '苹' + width = 5 + aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + text + +def test_right_text_asian_fill_needs_padding(): + """Test when fill_char's display width does not divide evenly into gap""" + text = 'foo' + fill_char = '苹' + width = 6 + aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + ' ' + text -- cgit v1.2.1 From 5ffed1387ddc4e5725f9c6b5b632a611eca1e3ca Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 16:34:07 -0500 Subject: Added more text alignment unit tests --- cmd2/utils.py | 6 ++++++ tests/test_utils.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd2/utils.py b/cmd2/utils.py index d7c9ff19..4235db7d 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -737,6 +737,8 @@ def ljustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = Non :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. :return: left-justified text + :raises: TypeError if fill_char is more than one character + ValueError if text or fill_char contains an unprintable character """ return align_text(text, fill_char=fill_char, width=width, tab_width=tab_width, alignment=TextAlignment.LEFT) @@ -754,6 +756,8 @@ def center_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. :return: centered text + :raises: TypeError if fill_char is more than one character + ValueError if text or fill_char contains an unprintable character """ return align_text(text, fill_char=fill_char, width=width, tab_width=tab_width, alignment=TextAlignment.CENTER) @@ -771,6 +775,8 @@ def rjustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = Non :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. :return: right-justified text + :raises: TypeError if fill_char is more than one character + ValueError if text or fill_char contains an unprintable character """ return align_text(text, fill_char=fill_char, width=width, tab_width=tab_width, alignment=TextAlignment.RIGHT) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1327064f..745e5ff4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -326,7 +326,7 @@ def test_align_text_blank(): fill_char = '-' width = 5 aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) - assert aligned == text + fill_char * width + assert aligned == fill_char * width def test_align_text_wider_than_width(): text = 'long' @@ -335,6 +335,13 @@ def test_align_text_wider_than_width(): aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) assert aligned == text +def test_align_text_has_unprintable(): + text = 'foo\x02' + fill_char = '-' + width = 5 + with pytest.raises(ValueError): + cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT) + def test_align_text_term_width(): import shutil from cmd2 import ansi -- cgit v1.2.1 From 6c522569ae9404528e9a3dfe9a1d4e769043e5e7 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 16:38:51 -0500 Subject: Updated change log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac01813..b38be085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files * Enhancements * Enabled line buffering when redirecting output to a file + * Added `ljustify_text()`, `center_text()`, and `rjustify_text()` to utils.py. All 3 of these functions support + ANSI escape sequences and characters with display widths greater than 1. ## 0.9.21 (November 26, 2019) * Bug Fixes -- cgit v1.2.1 From 4c3cd737d780819f02664f4d49edbebab08ee8a8 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 17:35:44 -0500 Subject: Added more unit tests for text alignment --- tests/test_utils.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 745e5ff4..233b3b70 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -369,15 +369,21 @@ def test_left_text_multiline(): assert aligned == ('foo----\n' 'shoes--') -def test_left_text_asian_fill(): - """Test fill_char with display width greater than 1""" +def test_left_wide_display_text(): + text = '苹' + fill_char = '-' + width = 4 + aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + assert aligned == text + fill_char + fill_char + +def test_left_text_wide_display_fill(): text = 'foo' fill_char = '苹' width = 5 aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) assert aligned == text + fill_char -def test_left_text_asian_fill_needs_padding(): +def test_left_text_wide_display_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' @@ -400,15 +406,21 @@ def test_center_text_multiline(): assert aligned == ('--foo--\n' '-shoes-') -def test_center_text_asian_fill(): - """Test fill_char with display width greater than 1""" +def test_center_wide_display_text(): + text = '苹' + fill_char = '-' + width = 4 + aligned = cu.center_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + text + fill_char + +def test_center_text_wide_display_fill(): text = 'foo' fill_char = '苹' width = 7 aligned = cu.center_text(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char -def test_center_text_asian_fill_needs_right_padding(): +def test_center_text_wide_display_fill_needs_right_padding(): """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' fill_char = '苹' @@ -416,7 +428,7 @@ def test_center_text_asian_fill_needs_right_padding(): aligned = cu.center_text(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + ' ' -def test_center_text_asian_fill_needs_left_and_right_padding(): +def test_center_text_wide_display_fill_needs_left_and_right_padding(): """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' fill_char = '苹' @@ -439,15 +451,21 @@ def test_right_text_multiline(): assert aligned == ('----foo\n' '--shoes') -def test_right_text_asian_fill(): - """Test fill_char with display width greater than 1""" +def test_right_wide_display_text(): + text = '苹' + fill_char = '-' + width = 4 + aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + assert aligned == fill_char + fill_char + text + +def test_right_text_wide_display_fill(): text = 'foo' fill_char = '苹' width = 5 aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) assert aligned == fill_char + text -def test_right_text_asian_fill_needs_padding(): +def test_right_text_wide_display_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' -- cgit v1.2.1 From 88e308b078847e0719e3d322a9d5c67e2f80036c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 19:14:28 -0500 Subject: Updated change log for release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b38be085..c6ec13f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.9.22 (TBD, 2019) +## 0.9.22 (December 10, 2019) * Bug Fixes * Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files * Enhancements -- cgit v1.2.1 From 9e747677a803210ad5ef5c1d5fdf01ea9c1a5ee9 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 20:53:37 -0500 Subject: Renamed functions based on code review comments. Fixed Python warnings. --- CHANGELOG.md | 6 ++--- cmd2/cmd2.py | 9 ++++++-- cmd2/utils.py | 39 +++++++++++++++----------------- tests/test_utils.py | 64 ++++++++++++++++++++++++++--------------------------- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6ec13f1..41e5e7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ -## 0.9.22 (December 10, 2019) +## 0.9.22 (December 9, 2019) * Bug Fixes * Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files * Enhancements * Enabled line buffering when redirecting output to a file - * Added `ljustify_text()`, `center_text()`, and `rjustify_text()` to utils.py. All 3 of these functions support + * Added `align_left()`, `align_center()`, and `align_right()` to utils.py. All 3 of these functions support ANSI escape sequences and characters with display widths greater than 1. - + ## 0.9.21 (November 26, 2019) * Bug Fixes * Fixed bug where pipe processes were not being stopped by Ctrl-C diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 9c48b222..28a9dedb 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2718,6 +2718,7 @@ class Cmd(cmd.Cmd): self.stdout.write("\n") shortcuts_parser = DEFAULT_ARGUMENT_PARSER(description="List available shortcuts") + @with_argparser(shortcuts_parser) def do_shortcuts(self, _: argparse.Namespace) -> None: """List available shortcuts""" @@ -2727,6 +2728,7 @@ class Cmd(cmd.Cmd): self.poutput("Shortcuts for other commands:\n{}".format(result)) eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when -D is pressed", epilog=INTERNAL_COMMAND_EPILOG) + @with_argparser(eof_parser) def do_eof(self, _: argparse.Namespace) -> bool: """Called when -D is pressed""" @@ -2734,6 +2736,7 @@ class Cmd(cmd.Cmd): return True quit_parser = DEFAULT_ARGUMENT_PARSER(description="Exit this application") + @with_argparser(quit_parser) def do_quit(self, _: argparse.Namespace) -> bool: """Exit this application""" @@ -3215,6 +3218,7 @@ class Cmd(cmd.Cmd): # Only include the do_ipy() method if IPython is available on the system if ipython_available: # pragma: no cover ipython_parser = DEFAULT_ARGUMENT_PARSER(description="Enter an interactive IPython shell") + @with_argparser(ipython_parser) def do_ipy(self, _: argparse.Namespace) -> None: """Enter an interactive IPython shell""" @@ -3223,6 +3227,7 @@ class Cmd(cmd.Cmd): 'Run Python code from external files with: run filename.py\n') exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0]) + # noinspection PyUnusedLocal def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge): """ Embed an IPython shell in an environment that is restricted to only the variables in this function @@ -3715,7 +3720,7 @@ class Cmd(cmd.Cmd): verinfo = ".".join(map(str, sys.version_info[:3])) num_transcripts = len(transcripts_expanded) plural = '' if len(transcripts_expanded) == 1 else 's' - self.poutput(ansi.style(utils.center_text(' cmd2 transcript test ', fill_char='='), bold=True)) + self.poutput(ansi.style(utils.align_center(' cmd2 transcript test ', fill_char='='), bold=True)) self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__, rl_type)) self.poutput('cwd: {}'.format(os.getcwd())) @@ -3734,7 +3739,7 @@ class Cmd(cmd.Cmd): if test_results.wasSuccessful(): ansi.ansi_aware_write(sys.stderr, stream.read()) finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time) - finish_msg = ansi.style_success(utils.center_text(finish_msg, fill_char='=')) + finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='=')) self.poutput(finish_msg) else: # Strip off the initial traceback which isn't particularly useful for end users diff --git a/cmd2/utils.py b/cmd2/utils.py index 4235db7d..9dd7a30b 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -637,21 +637,21 @@ class TextAlignment(Enum): RIGHT = 3 -def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, - alignment: TextAlignment) -> str: +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 supported. If text has line breaks, then each line is aligned independently. - There are convenience wrappers around this function: ljustify_text(), center_text(), and rjustify_text() + There are convenience wrappers around this function: align_left(), align_center(), and align_right() - :param text: text to align (Can contain multiple lines) + :param text: text to align (can contain multiple lines) + :param alignment: how to align the text :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) :param width: display width of the aligned text. Defaults to width of the terminal. :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. - :param alignment: how to align the text :return: aligned text :raises: TypeError if fill_char is more than one character ValueError if text or fill_char contains an unprintable character @@ -725,32 +725,31 @@ def align_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, return text_buf.getvalue() -def ljustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: +def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ - Left justify text for display within a given width. Supports characters with display widths greater than 1. + 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 supported. If text has line breaks, then each line is aligned independently. - :param text: text to left justify (Can contain multiple lines) + :param text: text to left align (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) :param width: display width of the aligned text. Defaults to width of the terminal. :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. - :return: left-justified text + :return: left-aligned text :raises: TypeError if fill_char is more than one character ValueError if text or fill_char contains an unprintable character """ - return align_text(text, fill_char=fill_char, width=width, - tab_width=tab_width, alignment=TextAlignment.LEFT) + return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width) -def center_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: +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 supported. If text has line breaks, then each line is aligned independently. - :param text: text to center (Can contain multiple lines) + :param text: text to center (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) :param width: display width of the aligned text. Defaults to width of the terminal. :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will @@ -759,24 +758,22 @@ def center_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :raises: TypeError if fill_char is more than one character ValueError if text or fill_char contains an unprintable character """ - return align_text(text, fill_char=fill_char, width=width, - tab_width=tab_width, alignment=TextAlignment.CENTER) + return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width) -def rjustify_text(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: +def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: """ - Right justify text for display within a given width. Supports characters with display widths greater than 1. + 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 supported. If text has line breaks, then each line is aligned independently. - :param text: text to right justify (Can contain multiple lines) + :param text: text to right align (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) :param width: display width of the aligned text. Defaults to width of the terminal. :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will be converted to a space. - :return: right-justified text + :return: right-aligned text :raises: TypeError if fill_char is more than one character ValueError if text or fill_char contains an unprintable character """ - return align_text(text, fill_char=fill_char, width=width, - tab_width=tab_width, alignment=TextAlignment.RIGHT) + return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width) diff --git a/tests/test_utils.py b/tests/test_utils.py index 233b3b70..27b0e3bb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -354,121 +354,121 @@ def test_align_text_term_width(): aligned = cu.align_text(text, fill_char=fill_char, alignment=cu.TextAlignment.LEFT) assert aligned == text + expected_fill -def test_left_text(): +def test_align_left(): text = 'foo' fill_char = '-' width = 5 - aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char -def test_left_text_multiline(): +def test_align_left_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 - aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == ('foo----\n' 'shoes--') -def test_left_wide_display_text(): +def test_align_left_wide_text(): text = '苹' fill_char = '-' width = 4 - aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + fill_char -def test_left_text_wide_display_fill(): +def test_align_left_wide_fill(): text = 'foo' fill_char = '苹' width = 5 - aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char -def test_left_text_wide_display_fill_needs_padding(): +def test_align_left_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' width = 6 - aligned = cu.ljustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_left(text, fill_char=fill_char, width=width) assert aligned == text + fill_char + ' ' -def test_center_text(): +def test_align_center(): text = 'foo' fill_char = '-' width = 5 - aligned = cu.center_text(text,fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char -def test_center_text_multiline(): +def test_align_center_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 - aligned = cu.center_text(text, fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == ('--foo--\n' '-shoes-') -def test_center_wide_display_text(): +def test_align_center_wide_text(): text = '苹' fill_char = '-' width = 4 - aligned = cu.center_text(text, fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char -def test_center_text_wide_display_fill(): +def test_align_center_wide_fill(): text = 'foo' fill_char = '苹' width = 7 - aligned = cu.center_text(text, fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char -def test_center_text_wide_display_fill_needs_right_padding(): +def test_align_center_wide_fill_needs_right_padding(): """Test when fill_char's display width does not divide evenly into right gap""" text = 'foo' fill_char = '苹' width = 8 - aligned = cu.center_text(text, fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + text + fill_char + ' ' -def test_center_text_wide_display_fill_needs_left_and_right_padding(): +def test_align_center_wide_fill_needs_left_and_right_padding(): """Test when fill_char's display width does not divide evenly into either gap""" text = 'foo' fill_char = '苹' width = 9 - aligned = cu.center_text(text, fill_char=fill_char, width=width) + aligned = cu.align_center(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text + fill_char + ' ' -def test_right_text(): +def test_align_right(): text = 'foo' fill_char = '-' width = 5 - aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text -def test_right_text_multiline(): +def test_align_right_multiline(): text = "foo\nshoes" fill_char = '-' width = 7 - aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == ('----foo\n' '--shoes') -def test_right_wide_display_text(): +def test_align_right_wide_text(): text = '苹' fill_char = '-' width = 4 - aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + fill_char + text -def test_right_text_wide_display_fill(): +def test_align_right_wide_fill(): text = 'foo' fill_char = '苹' width = 5 - aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + text -def test_right_text_wide_display_fill_needs_padding(): +def test_align_right_wide_fill_needs_padding(): """Test when fill_char's display width does not divide evenly into gap""" text = 'foo' fill_char = '苹' width = 6 - aligned = cu.rjustify_text(text, fill_char=fill_char, width=width) + aligned = cu.align_right(text, fill_char=fill_char, width=width) assert aligned == fill_char + ' ' + text -- cgit v1.2.1 From a4427a3a905d9e926e1ab9c57716235229247912 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 9 Dec 2019 21:03:31 -0500 Subject: Fixed doc generation errors --- CHANGELOG.md | 3 ++- docs/api/utility_functions.rst | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e5e7bf..79b50d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * Enhancements * Enabled line buffering when redirecting output to a file * Added `align_left()`, `align_center()`, and `align_right()` to utils.py. All 3 of these functions support - ANSI escape sequences and characters with display widths greater than 1. + ANSI escape sequences and characters with display widths greater than 1. They wrap `align_text()` which + is also in utils.py. ## 0.9.21 (November 26, 2019) * Bug Fixes diff --git a/docs/api/utility_functions.rst b/docs/api/utility_functions.rst index 86fb656c..e083cafe 100644 --- a/docs/api/utility_functions.rst +++ b/docs/api/utility_functions.rst @@ -9,7 +9,13 @@ Utility Functions .. autofunction:: cmd2.decorators.categorize -.. autofunction:: cmd2.utils.center_text +.. autofunction:: cmd2.utils.align_text + +.. autofunction:: cmd2.utils.align_left + +.. autofunction:: cmd2.utils.align_center + +.. autofunction:: cmd2.utils.align_right .. autofunction:: cmd2.utils.strip_quotes -- cgit v1.2.1 From e74ba5ee32f69d6aa3cedd3131bc6ed25ea6b8ec Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 10 Dec 2019 13:13:38 -0500 Subject: Took out rstrip() calls that are no longer neeeded --- cmd2/history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/history.py b/cmd2/history.py index 576ac37d..3b18fbeb 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -45,14 +45,14 @@ class HistoryItem(): """ if verbose: raw = self.raw.rstrip() - expanded = self.expanded.rstrip() + expanded = self.expanded ret_str = self._listformat.format(self.idx, raw) if raw != expanded: ret_str += '\n' + self._ex_listformat.format(self.idx, expanded) else: if expanded: - ret_str = self.expanded.rstrip() + ret_str = self.expanded else: ret_str = self.raw.rstrip() -- cgit v1.2.1 From fb5f7a80ce50139ea99bbf6a0d8aa8df7c3db453 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 10 Dec 2019 14:31:46 -0500 Subject: Took out more rstrip() calls that are no longer neeeded --- cmd2/cmd2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 28a9dedb..6c4fdcbd 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -3358,7 +3358,7 @@ class Cmd(cmd.Cmd): with os.fdopen(fd, 'w') as fobj: for command in history: if command.statement.multiline_command: - fobj.write('{}\n'.format(command.expanded.rstrip())) + fobj.write('{}\n'.format(command.expanded)) else: fobj.write('{}\n'.format(command.raw)) try: @@ -3372,7 +3372,7 @@ class Cmd(cmd.Cmd): with open(os.path.expanduser(args.output_file), 'w') as fobj: for item in history: if item.statement.multiline_command: - fobj.write('{}\n'.format(item.expanded.rstrip())) + fobj.write('{}\n'.format(item.expanded)) else: fobj.write('{}\n'.format(item.raw)) plural = 's' if len(history) > 1 else '' -- cgit v1.2.1 From bff8f7a4ef8b6cb8e160242b673ef30980e84fdb Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 10 Dec 2019 16:05:43 -0500 Subject: Increased code coverage --- tests/test_history.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/test_history.py b/tests/test_history.py index 4b900030..11f189f6 100755 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -447,15 +447,17 @@ def test_history_with_span_index_error(base_app): with pytest.raises(ValueError): base_app.onecmd('history "hal :"') -def test_history_output_file(base_app): - run_cmd(base_app, 'help') - run_cmd(base_app, 'shortcuts') - run_cmd(base_app, 'help history') +def test_history_output_file(): + app = cmd2.Cmd(multiline_commands=['alias']) + run_cmd(app, 'help') + run_cmd(app, 'shortcuts') + run_cmd(app, 'help history') + run_cmd(app, 'alias create my_alias history;') fd, fname = tempfile.mkstemp(prefix='', suffix='.txt') os.close(fd) - run_cmd(base_app, 'history -o "{}"'.format(fname)) - expected = normalize('\n'.join(['help', 'shortcuts', 'help history'])) + run_cmd(app, 'history -o "{}"'.format(fname)) + expected = normalize('\n'.join(['help', 'shortcuts', 'help history', 'alias create my_alias history;'])) with open(fname) as f: content = normalize(f.read()) assert content == expected @@ -471,10 +473,12 @@ def test_history_bad_output_file(base_app): assert not out assert "Error saving" in err[0] -def test_history_edit(base_app, monkeypatch): +def test_history_edit(monkeypatch): + app = cmd2.Cmd(multiline_commands=['alias']) + # Set a fake editor just to make sure we have one. We aren't really # going to call it due to the mock - base_app.editor = 'fooedit' + app.editor = 'fooedit' # Mock out the _run_editor call so we don't actually open an editor edit_mock = mock.MagicMock(name='_run_editor') @@ -484,9 +488,11 @@ def test_history_edit(base_app, monkeypatch): run_script_mock = mock.MagicMock(name='do_run_script') monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock) - # Run help command just so we have a command in history - run_cmd(base_app, 'help') - run_cmd(base_app, 'history -e 1') + # Put commands in history + run_cmd(app, 'help') + run_cmd(app, 'alias create my_alias history;') + + run_cmd(app, 'history -e 1:2') # Make sure both functions were called edit_mock.assert_called_once() -- cgit v1.2.1 From 1cb8de929b8700263f84753c739c49065898c081 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 10 Dec 2019 16:21:49 -0500 Subject: Increased code coverage in argparse_completer.py back to 100% --- tests/test_argparse_completer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index b904a6ac..97c75ef3 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -535,11 +535,11 @@ def test_autocomp_blank_token(ac_app): def test_completion_items(ac_app, num_aliases, show_description): # Create aliases for i in range(0, num_aliases): - run_cmd(ac_app, 'alias create fake{} help'.format(i)) + run_cmd(ac_app, 'alias create fake_alias{} help'.format(i)) assert len(ac_app.aliases) == num_aliases - text = 'fake' + text = 'fake_alias' line = 'alias list {}'.format(text) endidx = len(line) begidx = endidx - len(text) -- cgit v1.2.1 From 60d236fdd331304b9d516f080c9a36c08c4baa9e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 17 Dec 2019 22:06:38 -0500 Subject: Fixed bug where startup script containing a single quote in its file name was incorrectly quoted --- CHANGELOG.md | 4 ++++ cmd2/cmd2.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b50d49..20427976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.23 (TBD, 2019) +* Bug Fixes + * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted + ## 0.9.22 (December 9, 2019) * Bug Fixes * Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 6c4fdcbd..9120c6af 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -300,7 +300,7 @@ class Cmd(cmd.Cmd): if startup_script: startup_script = os.path.abspath(os.path.expanduser(startup_script)) if os.path.exists(startup_script): - self._startup_commands.append("run_script '{}'".format(startup_script)) + self._startup_commands.append("run_script {}".format(utils.quote_string(startup_script))) # Transcript files to run instead of interactive command loop self._transcript_files = None -- cgit v1.2.1 From d91943f4ddbfcc47b35f26883e1068d5693c0f03 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 17 Dec 2019 22:51:55 -0500 Subject: Updating unit tests --- tests/conftest.py | 9 ++++++++ tests/test_cmd2.py | 56 +++++++++------------------------------------- tests/test_run_pyscript.py | 27 ++++++++-------------- 3 files changed, 29 insertions(+), 63 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e09e07b1..6b7c5aff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,6 +157,15 @@ def base_app(): return cmd2.Cmd() +# These are odd file names for testing quoting of them +odd_file_names = [ + 'nothingweird', + 'has spaces', + '"is_double_quoted"', + "'is_single_quoted'" +] + + def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]: """ This is a convenience function to test cmd2.complete() since diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 88447416..2739fd1a 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -21,8 +21,8 @@ except ImportError: import cmd2 from cmd2 import ansi, clipboard, constants, plugin, utils, COMMAND_NAME -from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY -from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester +from .conftest import (run_cmd, normalize, verify_help_text, HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, + SHOW_LONG, complete_tester, odd_file_names) def CreateOutsimApp(): c = cmd2.Cmd() @@ -431,31 +431,15 @@ def test_relative_run_script(base_app, request): assert script_out == manual_out assert script_err == manual_err -def test_relative_run_script_with_odd_file_names(base_app, monkeypatch): +@pytest.mark.parametrize('file_name', odd_file_names) +def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch): """Test file names with various patterns""" # Mock out the do_run_script call to see what args are passed to it run_script_mock = mock.MagicMock(name='do_run_script') monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock) - file_name = utils.quote_string('nothingweird.txt') - out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name)) - run_script_mock.assert_called_once_with('"nothingweird.txt"') - run_script_mock.reset_mock() - - file_name = utils.quote_string('has spaces.txt') - out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name)) - run_script_mock.assert_called_once_with('"has spaces.txt"') - run_script_mock.reset_mock() - - file_name = utils.quote_string('"is_double_quoted.txt"') - out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name)) - run_script_mock.assert_called_once_with('\'"is_double_quoted.txt"\'') - run_script_mock.reset_mock() - - file_name = utils.quote_string("'is_single_quoted.txt'") - out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name)) - run_script_mock.assert_called_once_with('"\'is_single_quoted.txt\'"') - run_script_mock.reset_mock() + run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name))) + run_script_mock.assert_called_once_with(utils.quote_string(file_name)) def test_relative_run_script_requires_an_argument(base_app): out, err = run_cmd(base_app, '_relative_run_script') @@ -715,7 +699,8 @@ def test_edit_file(base_app, request, monkeypatch): # We think we have an editor, so should expect a Popen call m.assert_called_once() -def test_edit_file_with_odd_file_names(base_app, monkeypatch): +@pytest.mark.parametrize('file_name', odd_file_names) +def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch): """Test editor and file names with various patterns""" # Mock out the do_shell call to see what args are passed to it shell_mock = mock.MagicMock(name='do_shell') @@ -723,27 +708,8 @@ def test_edit_file_with_odd_file_names(base_app, monkeypatch): base_app.editor = 'fooedit' file_name = utils.quote_string('nothingweird.py') - out, err = run_cmd(base_app, "edit {}".format(file_name)) - shell_mock.assert_called_once_with('"fooedit" "nothingweird.py"') - shell_mock.reset_mock() - - base_app.editor = 'foo edit' - file_name = utils.quote_string('has spaces.py') - out, err = run_cmd(base_app, "edit {}".format(file_name)) - shell_mock.assert_called_once_with('"foo edit" "has spaces.py"') - shell_mock.reset_mock() - - base_app.editor = '"fooedit"' - file_name = utils.quote_string('"is_double_quoted.py"') - out, err = run_cmd(base_app, "edit {}".format(file_name)) - shell_mock.assert_called_once_with('\'"fooedit"\' \'"is_double_quoted.py"\'') - shell_mock.reset_mock() - - base_app.editor = "'fooedit'" - file_name = utils.quote_string("'is_single_quoted.py'") - out, err = run_cmd(base_app, "edit {}".format(file_name)) - shell_mock.assert_called_once_with('"\'fooedit\'" "\'is_single_quoted.py\'"') - shell_mock.reset_mock() + run_cmd(base_app, "edit {}".format(utils.quote_string(file_name))) + shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name))) def test_edit_file_with_spaces(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock @@ -2386,7 +2352,7 @@ def test_startup_script(request): startup_script = os.path.join(test_dir, '.cmd2rc') app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script) assert len(app._startup_commands) == 1 - assert app._startup_commands[0] == "run_script '{}'".format(startup_script) + assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(startup_script)) app._startup_commands.append('quit') app.cmdloop() out, err = run_cmd(app, 'alias list') diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py index a4ff097f..d717758c 100644 --- a/tests/test_run_pyscript.py +++ b/tests/test_run_pyscript.py @@ -6,8 +6,10 @@ Unit/functional testing for run_pytest in cmd2 import builtins import os +import pytest + from cmd2 import plugin, utils -from .conftest import run_cmd +from .conftest import run_cmd, odd_file_names # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available try: @@ -52,30 +54,19 @@ def test_run_pyscript_with_non_python_file(base_app, request): out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename)) assert "does not have a .py extension" in err[0] -def test_run_pyscript_with_odd_file_names(base_app): +@pytest.mark.parametrize('python_script', odd_file_names) +def test_run_pyscript_with_odd_file_names(base_app, python_script): """ Pass in file names with various patterns. Since these files don't exist, we will rely on the error text to make sure the file names were processed correctly. """ - python_script = utils.quote_string('nothingweird.py') - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) - assert "Error reading script file 'nothingweird.py'" in err[0] - - python_script = utils.quote_string('has spaces.py') - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) - assert "Error reading script file 'has spaces.py'" in err[0] - - # For remaining tests, mock input to get us passed the warning about not ending in .py + # Mock input to get us passed the warning about not ending in .py input_mock = mock.MagicMock(name='input', return_value='1') builtins.input = input_mock - python_script = utils.quote_string('"is_double_quoted.py"') - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) - assert "Error reading script file '\"is_double_quoted.py\"'" in err[1] - - python_script = utils.quote_string("'is_single_quoted.py'") - out, err = run_cmd(base_app, "run_pyscript {}".format(python_script)) - assert "Error reading script file ''is_single_quoted.py''" in err[1] + out, err = run_cmd(base_app, "run_pyscript {}".format(utils.quote_string(python_script))) + err = ''.join(err) + assert "Error reading script file '{}'".format(python_script) in err def test_run_pyscript_with_exception(base_app, request): test_dir = os.path.dirname(request.module.__file__) -- cgit v1.2.1 From 189df5538bf86ef33766fa18852270a9a3e0b281 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 17 Dec 2019 23:15:56 -0500 Subject: Updating unit tests --- tests/test_cmd2.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 2739fd1a..9a0218f2 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -2360,6 +2360,21 @@ def test_startup_script(request): assert 'alias create ls' in out[0] +@pytest.mark.parametrize('startup_script', odd_file_names) +def test_startup_script_with_odd_file_names(startup_script): + """Test file names with various patterns""" + # Mock os.path.exists to trick cmd2 into adding this script to its startup commands + saved_exists = os.path.exists + os.path.exists = mock.MagicMock(name='exists', return_value=True) + + app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script) + assert len(app._startup_commands) == 1 + assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(os.path.abspath(startup_script))) + + # Restore os.path.exists + os.path.exists = saved_exists + + def test_transcripts_at_init(): transcript_files = ['foo', 'bar'] app = cmd2.Cmd(allow_cli_args=False, transcript_files=transcript_files) -- cgit v1.2.1 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 --- README.md | 2 +- cmd2/ansi.py | 50 ++++++++++++----------- cmd2/cmd2.py | 48 +++++++++++----------- cmd2/transcript.py | 8 ++-- cmd2/utils.py | 10 ++--- docs/features/generating_output.rst | 12 +++--- docs/features/settings.rst | 4 +- examples/colors.py | 14 +++---- examples/plumbum_colors.py | 14 +++---- examples/transcripts/exampleSession.txt | 2 +- examples/transcripts/transcript_regex.txt | 2 +- tests/conftest.py | 4 +- tests/scripts/postcmds.txt | 2 +- tests/scripts/precmds.txt | 2 +- tests/test_ansi.py | 4 +- tests/test_cmd2.py | 68 +++++++++++++++---------------- tests/transcripts/regex_set.txt | 2 +- 17 files changed, 126 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index 7b68965f..f90875de 100755 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ example/transcript_regex.txt: # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set -allow_ansi: Terminal +allow_style: Terminal continuation_prompt: >/ / debug: False echo: False 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) diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst index 2ee820f1..e83b5fdf 100644 --- a/docs/features/generating_output.rst +++ b/docs/features/generating_output.rst @@ -45,23 +45,23 @@ changing the value of the ``quiet`` setting. Colored Output -------------- -The output methods in the previous section all honor the ``allow_ansi`` +The output methods in the previous section all honor the ``allow_style`` setting, which has three possible values: Never - poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences + poutput(), pfeedback(), and ppaged() strip all ANSI style sequences which instruct the terminal to colorize output Terminal (the default value) poutput(), pfeedback(), and ppaged() do not strip any - ANSI escape sequences when the output is a terminal, but if the output is a - pipe or a file the escape sequences are stripped. If you want colorized - output you must add ANSI escape sequences using either cmd2's internal ansi + ANSI style sequences when the output is a terminal, but if the output is a + pipe or a file the style sequences are stripped. If you want colorized + output you must add ANSI style sequences using either cmd2's internal ansi module or another color library such as `plumbum.colors`, `colorama`, or `colored`. Always - poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences, + poutput(), pfeedback(), and ppaged() never strip ANSI style sequences, regardless of the output destination Colored and otherwise styled output can be generated using the `ansi.style()` diff --git a/docs/features/settings.rst b/docs/features/settings.rst index b0575468..7cc0d727 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -46,7 +46,7 @@ comments, is viewable from within a running application with:: (Cmd) set --long - allow_ansi: Terminal # Allow ANSI escape sequences in output (valid values: Terminal, Always, Never) + allow_style: Terminal # Allow ANSI style sequences in output (valid values: Terminal, Always, Never) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output @@ -61,7 +61,7 @@ with:: Any of these user-settable parameters can be set while running your app with the ``set`` command like so:: - set allow_ansi Never + set allow_style Never diff --git a/examples/colors.py b/examples/colors.py index 8c54dfa4..ea8be479 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -6,21 +6,21 @@ A sample application for cmd2. Demonstrating colorized output. Experiment with the command line options on the `speak` command to see how different output colors ca -The allow_ansi setting has three possible values: +The allow_style setting has three possible values: Never - poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences + poutput(), pfeedback(), and ppaged() strip all ANSI style sequences which instruct the terminal to colorize output Terminal (the default value) poutput(), pfeedback(), and ppaged() do not strip any - ANSI escape sequences when the output is a terminal, but if the output is - a pipe or a file the escape sequences are stripped. If you want colorized - output you must add ANSI escape sequences using either cmd2's internal ansi + ANSI style sequences when the output is a terminal, but if the output is + a pipe or a file the style sequences are stripped. If you want colorized + output you must add ANSI style sequences using either cmd2's internal ansi module or another color library such as `plumbum.colors` or `colorama`. Always - poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences, + poutput(), pfeedback(), and ppaged() never strip ANSI style sequences, regardless of the output destination """ import argparse @@ -40,7 +40,7 @@ class CmdLineApp(cmd2.Cmd): self.settable['maxrepeats'] = 'max repetitions for speak command' # Should ANSI color output be allowed - self.allow_ansi = ansi.ANSI_TERMINAL + self.allow_style = ansi.STYLE_TERMINAL speak_parser = argparse.ArgumentParser() speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py index a969c4de..8eaf3fe8 100755 --- a/examples/plumbum_colors.py +++ b/examples/plumbum_colors.py @@ -6,21 +6,21 @@ A sample application for cmd2. Demonstrating colorized output using the plumbum Experiment with the command line options on the `speak` command to see how different output colors ca -The allow_ansi setting has three possible values: +The allow_style setting has three possible values: Never - poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences + poutput(), pfeedback(), and ppaged() strip all ANSI style sequences which instruct the terminal to colorize output Terminal (the default value) poutput(), pfeedback(), and ppaged() do not strip any - ANSI escape sequences when the output is a terminal, but if the output is - a pipe or a file the escape sequences are stripped. If you want colorized - output you must add ANSI escape sequences using either cmd2's internal ansi + ANSI style sequences when the output is a terminal, but if the output is + a pipe or a file the style sequences are stripped. If you want colorized + output you must add ANSI style sequences using either cmd2's internal ansi module or another color library such as `plumbum.colors` or `colorama`. Always - poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences, + poutput(), pfeedback(), and ppaged() never strip ANSI style sequences, regardless of the output destination WARNING: This example requires the plumbum package, which isn't normally required by cmd2. @@ -78,7 +78,7 @@ class CmdLineApp(cmd2.Cmd): self.settable['maxrepeats'] = 'max repetitions for speak command' # Should ANSI color output be allowed - self.allow_ansi = ansi.ANSI_TERMINAL + self.allow_style = ansi.STYLE_TERMINAL speak_parser = argparse.ArgumentParser() speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt index 3e579c28..5a75235b 100644 --- a/examples/transcripts/exampleSession.txt +++ b/examples/transcripts/exampleSession.txt @@ -3,7 +3,7 @@ # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set -allow_ansi: /(Terminal|Always|Never)/ +allow_style: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt index d94c442b..276f7d22 100644 --- a/examples/transcripts/transcript_regex.txt +++ b/examples/transcripts/transcript_regex.txt @@ -3,7 +3,7 @@ # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set -allow_ansi: /(Terminal|Always|Never)/ +allow_style: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False diff --git a/tests/conftest.py b/tests/conftest.py index e09e07b1..631c6e25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,7 +88,7 @@ SHORTCUTS_TXT = """Shortcuts for other commands: """ # Output from the show command with default settings -SHOW_TXT = """allow_ansi: Terminal +SHOW_TXT = """allow_style: Terminal continuation_prompt: > debug: False echo: False @@ -102,7 +102,7 @@ timing: False """ SHOW_LONG = """ -allow_ansi: Terminal # Allow ANSI escape sequences in output (valid values: Terminal, Always, Never) +allow_style: Terminal # Allow ANSI style sequences in output (valid values: Terminal, Always, Never) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt index 74f1e226..30f47055 100644 --- a/tests/scripts/postcmds.txt +++ b/tests/scripts/postcmds.txt @@ -1 +1 @@ -set allow_ansi Never +set allow_style Never diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt index 0167aa22..7d036acf 100644 --- a/tests/scripts/precmds.txt +++ b/tests/scripts/precmds.txt @@ -1 +1 @@ -set allow_ansi Always +set allow_style Always diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 056bb2db..4f7ab652 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -10,11 +10,11 @@ import cmd2.ansi as ansi HELLO_WORLD = 'Hello, world!' -def test_strip_ansi(): +def test_strip_style(): base_str = HELLO_WORLD ansi_str = ansi.style(base_str, fg='green') assert base_str != ansi_str - assert base_str == ansi.strip_ansi(ansi_str) + assert base_str == ansi.strip_style(ansi_str) def test_ansi_safe_wcswidth(): diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 88447416..a3167cdf 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -187,26 +187,26 @@ now: True assert out == ['quiet: True'] @pytest.mark.parametrize('new_val, is_valid, expected', [ - (ansi.ANSI_NEVER, False, ansi.ANSI_NEVER), - ('neVeR', False, ansi.ANSI_NEVER), - (ansi.ANSI_TERMINAL, False, ansi.ANSI_TERMINAL), - ('TeRMInal', False, ansi.ANSI_TERMINAL), - (ansi.ANSI_ALWAYS, False, ansi.ANSI_ALWAYS), - ('AlWaYs', False, ansi.ANSI_ALWAYS), - ('invalid', True, ansi.ANSI_TERMINAL), + (ansi.STYLE_NEVER, False, ansi.STYLE_NEVER), + ('neVeR', False, ansi.STYLE_NEVER), + (ansi.STYLE_TERMINAL, False, ansi.STYLE_TERMINAL), + ('TeRMInal', False, ansi.STYLE_TERMINAL), + (ansi.STYLE_ALWAYS, False, ansi.STYLE_ALWAYS), + ('AlWaYs', False, ansi.STYLE_ALWAYS), + ('invalid', True, ansi.STYLE_TERMINAL), ]) -def test_set_allow_ansi(base_app, new_val, is_valid, expected): - # Initialize allow_ansi for this test - ansi.allow_ansi = ansi.ANSI_TERMINAL +def test_set_allow_style(base_app, new_val, is_valid, expected): + # Initialize allow_style for this test + ansi.allow_style = ansi.STYLE_TERMINAL # Use the set command to alter it - out, err = run_cmd(base_app, 'set allow_ansi {}'.format(new_val)) + out, err = run_cmd(base_app, 'set allow_style {}'.format(new_val)) # Verify the results assert bool(err) == is_valid - assert ansi.allow_ansi == expected + assert ansi.allow_style == expected - # Reload ansi module to reset allow_ansi to its default since it's an + # Reload ansi module to reset allow_style to its default since it's an # application-wide setting that can affect other unit tests. import importlib importlib.reload(ansi) @@ -376,11 +376,11 @@ def test_run_script_nested_run_scripts(base_app, request): expected = """ %s _relative_run_script precmds.txt -set allow_ansi Always +set allow_style Always help shortcuts _relative_run_script postcmds.txt -set allow_ansi Never""" % initial_run +set allow_style Never""" % initial_run out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) @@ -395,11 +395,11 @@ def test_runcmds_plus_hooks(base_app, request): 'run_script ' + postfilepath]) expected = """ run_script %s -set allow_ansi Always +set allow_style Always help shortcuts run_script %s -set allow_ansi Never""" % (prefilepath, postfilepath) +set allow_style Never""" % (prefilepath, postfilepath) out, err = run_cmd(base_app, 'history -s') assert out == normalize(expected) @@ -1564,7 +1564,7 @@ def test_poutput_none(outsim_app): def test_poutput_ansi_always(outsim_app): msg = 'Hello World' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS colored_msg = ansi.style(msg, fg='cyan') outsim_app.poutput(colored_msg) out = outsim_app.stdout.getvalue() @@ -1574,7 +1574,7 @@ def test_poutput_ansi_always(outsim_app): def test_poutput_ansi_never(outsim_app): msg = 'Hello World' - ansi.allow_ansi = ansi.ANSI_NEVER + ansi.allow_style = ansi.STYLE_NEVER colored_msg = ansi.style(msg, fg='cyan') outsim_app.poutput(colored_msg) out = outsim_app.stdout.getvalue() @@ -1885,7 +1885,7 @@ def test_nonexistent_macro(base_app): def test_perror_style(base_app, capsys): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS base_app.perror(msg) out, err = capsys.readouterr() assert err == ansi.style_error(msg) + end @@ -1893,7 +1893,7 @@ def test_perror_style(base_app, capsys): def test_perror_no_style(base_app, capsys): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS base_app.perror(msg, apply_style=False) out, err = capsys.readouterr() assert err == msg + end @@ -1901,7 +1901,7 @@ def test_perror_no_style(base_app, capsys): def test_pwarning_style(base_app, capsys): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS base_app.pwarning(msg) out, err = capsys.readouterr() assert err == ansi.style_warning(msg) + end @@ -1909,7 +1909,7 @@ def test_pwarning_style(base_app, capsys): def test_pwarning_no_style(base_app, capsys): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS base_app.pwarning(msg, apply_style=False) out, err = capsys.readouterr() assert err == msg + end @@ -1936,7 +1936,7 @@ def test_ppaged_none(outsim_app): def test_ppaged_strips_ansi_when_redirecting(outsim_app): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_TERMINAL + ansi.allow_style = ansi.STYLE_TERMINAL outsim_app._redirecting = True outsim_app.ppaged(ansi.style(msg, fg='red')) out = outsim_app.stdout.getvalue() @@ -1945,7 +1945,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app): def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app): msg = 'testing...' end = '\n' - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS outsim_app._redirecting = True colored_msg = ansi.style(msg, fg='red') outsim_app.ppaged(colored_msg) @@ -2112,13 +2112,13 @@ class AnsiApp(cmd2.Cmd): def test_ansi_pouterr_always_tty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) app.onecmd_plus_hooks('echo_error oopsie') out, err = capsys.readouterr() - # if colors are on, the output should have some escape sequences in it + # if colors are on, the output should have some ANSI style sequences in it assert len(out) > len('oopsie\n') assert 'oopsie' in out assert len(err) > len('oopsie\n') @@ -2134,13 +2134,13 @@ def test_ansi_pouterr_always_tty(mocker, capsys): def test_ansi_pouterr_always_notty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_ALWAYS + ansi.allow_style = ansi.STYLE_ALWAYS mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) app.onecmd_plus_hooks('echo_error oopsie') out, err = capsys.readouterr() - # if colors are on, the output should have some escape sequences in it + # if colors are on, the output should have some ANSI style sequences in it assert len(out) > len('oopsie\n') assert 'oopsie' in out assert len(err) > len('oopsie\n') @@ -2156,12 +2156,12 @@ def test_ansi_pouterr_always_notty(mocker, capsys): def test_ansi_terminal_tty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_TERMINAL + ansi.allow_style = ansi.STYLE_TERMINAL mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) app.onecmd_plus_hooks('echo_error oopsie') - # if colors are on, the output should have some escape sequences in it + # if colors are on, the output should have some ANSI style sequences in it out, err = capsys.readouterr() assert len(out) > len('oopsie\n') assert 'oopsie' in out @@ -2177,7 +2177,7 @@ def test_ansi_terminal_tty(mocker, capsys): def test_ansi_terminal_notty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_TERMINAL + ansi.allow_style = ansi.STYLE_TERMINAL mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) @@ -2191,7 +2191,7 @@ def test_ansi_terminal_notty(mocker, capsys): def test_ansi_never_tty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_NEVER + ansi.allow_style = ansi.STYLE_NEVER mocker.patch.object(app.stdout, 'isatty', return_value=True) mocker.patch.object(sys.stderr, 'isatty', return_value=True) @@ -2205,7 +2205,7 @@ def test_ansi_never_tty(mocker, capsys): def test_ansi_never_notty(mocker, capsys): app = AnsiApp() - ansi.allow_ansi = ansi.ANSI_NEVER + ansi.allow_style = ansi.STYLE_NEVER mocker.patch.object(app.stdout, 'isatty', return_value=False) mocker.patch.object(sys.stderr, 'isatty', return_value=False) diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index fdcca3a8..17f43ede 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -4,7 +4,7 @@ # Regexes on prompts just make the trailing space obvious (Cmd) set -allow_ansi: /(Terminal|Always|Never)/ +allow_style: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False -- cgit v1.2.1 From b0e5aabad9c902ee5d664bf58885245060114f61 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 19 Dec 2019 17:00:29 -0500 Subject: Renamed ansi_safe_wcswidth() to style_aware_wcswidth() Renamed ansi_aware_write() to style_aware_write() --- CHANGELOG.md | 5 +++++ cmd2/ansi.py | 24 +++++++++++------------- cmd2/argparse_completer.py | 6 +++--- cmd2/argparse_custom.py | 4 ++-- cmd2/cmd2.py | 10 +++++----- cmd2/rl_utils.py | 2 +- cmd2/utils.py | 10 +++++----- tests/test_ansi.py | 4 ++-- tests/test_utils.py | 2 +- 9 files changed, 35 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20427976..72dedb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ ## 0.9.23 (TBD, 2019) * Bug Fixes * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted +* Breaking changes + * Renamed the following `ansi` members for accuracy in what types of ANSI escape sequences are handled + * `ansi.allow_ansi` -> `ansi.allow_style` + * `ansi.ansi_safe_wcswidth()` -> `ansi.style_aware_wcswidth()` + * `ansi.ansi_aware_write()` -> `ansi.style_aware_write()` ## 0.9.22 (December 9, 2019) * Bug Fixes diff --git a/cmd2/ansi.py b/cmd2/ansi.py index 57b204ca..b5b78164 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -90,20 +90,18 @@ def strip_style(text: str) -> str: return ANSI_STYLE_RE.sub('', text) -def ansi_safe_wcswidth(text: str) -> int: +def style_aware_wcswidth(text: str) -> int: """ Wrap wcswidth to make it compatible with strings that contains ANSI style sequences - :param text: the string being measured """ # 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: +def style_aware_write(fileobj: IO, msg: str) -> None: """ 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 """ @@ -114,8 +112,8 @@ def ansi_aware_write(fileobj: IO, msg: str) -> None: def fg_lookup(fg_name: str) -> str: - """Look up ANSI escape codes based on foreground color name. - + """ + Look up ANSI escape codes based on foreground color name. :param fg_name: foreground color name to look up ANSI escape code(s) for :return: ANSI escape code(s) associated with this color :raises ValueError if the color cannot be found @@ -128,8 +126,8 @@ def fg_lookup(fg_name: str) -> str: def bg_lookup(bg_name: str) -> str: - """Look up ANSI escape codes based on background color name. - + """ + Look up ANSI escape codes based on background color name. :param bg_name: background color name to look up ANSI escape code(s) for :return: ANSI escape code(s) associated with this color :raises ValueError if the color cannot be found @@ -142,8 +140,8 @@ def bg_lookup(bg_name: str) -> str: def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underline: bool = False) -> str: - """Styles a string with ANSI colors and/or styles and returns the new string. - + """ + Apply ANSI colors and/or styles to a string and return it. The styling is self contained which means that at the end of the string reset code(s) are issued to undo whatever styling was done at the beginning. @@ -210,14 +208,14 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off # That will be included in the input lines calculations since that is where the cursor is. num_prompt_terminal_lines = 0 for line in prompt_lines[:-1]: - line_width = ansi_safe_wcswidth(line) + line_width = style_aware_wcswidth(line) num_prompt_terminal_lines += int(line_width / terminal_columns) + 1 # Now calculate how many terminal lines are take up by the input last_prompt_line = prompt_lines[-1] - last_prompt_line_width = ansi_safe_wcswidth(last_prompt_line) + last_prompt_line_width = style_aware_wcswidth(last_prompt_line) - input_width = last_prompt_line_width + ansi_safe_wcswidth(line) + input_width = last_prompt_line_width + style_aware_wcswidth(line) num_input_terminal_lines = int(input_width / terminal_columns) + 1 diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index a2690dd0..23fd930e 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -444,11 +444,11 @@ class AutoCompleter(object): completions.sort(key=self._cmd2_app.default_sort_key) self._cmd2_app.matches_sorted = True - token_width = ansi.ansi_safe_wcswidth(action.dest) + token_width = ansi.style_aware_wcswidth(action.dest) completions_with_desc = [] for item in completions: - item_width = ansi.ansi_safe_wcswidth(item) + item_width = ansi.style_aware_wcswidth(item) if item_width > token_width: token_width = item_width @@ -585,7 +585,7 @@ class AutoCompleter(object): def _print_message(msg: str) -> None: """Print a message instead of tab completions and redraw the prompt and input line""" import sys - ansi.ansi_aware_write(sys.stdout, msg + '\n') + ansi.style_aware_write(sys.stdout, msg + '\n') rl_force_redisplay() def _print_arg_hint(self, arg_action: argparse.Action) -> None: diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 51c3375e..f735498d 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -802,11 +802,11 @@ class Cmd2ArgumentParser(argparse.ArgumentParser): return formatter.format_help() + '\n' def _print_message(self, message, file=None): - # Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color + # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color if message: if file is None: file = sys.stderr - ansi.ansi_aware_write(file, message) + ansi.style_aware_write(file, message) # The default ArgumentParser class for a cmd2 app diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 8a97761d..f133381b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -439,7 +439,7 @@ class Cmd(cmd.Cmd): :param end: string appended after the end of the message, default a newline """ try: - ansi.ansi_aware_write(self.stdout, "{}{}".format(msg, end)) + ansi.style_aware_write(self.stdout, "{}{}".format(msg, end)) except BrokenPipeError: # This occurs if a command's output is being piped to another # process and that process closes before the command is @@ -462,7 +462,7 @@ class Cmd(cmd.Cmd): final_msg = ansi.style_error(msg) else: final_msg = "{}".format(msg) - ansi.ansi_aware_write(sys.stderr, final_msg + end) + ansi.style_aware_write(sys.stderr, final_msg + end) def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None: """Wraps perror, but applies ansi.style_warning by default @@ -1096,7 +1096,7 @@ class Cmd(cmd.Cmd): longest_match_length = 0 for cur_match in matches_to_display: - cur_length = ansi.ansi_safe_wcswidth(cur_match) + cur_length = ansi.style_aware_wcswidth(cur_match) if cur_length > longest_match_length: longest_match_length = cur_length else: @@ -2653,7 +2653,7 @@ class Cmd(cmd.Cmd): widest = 0 # measure the commands for command in cmds: - width = ansi.ansi_safe_wcswidth(command) + width = ansi.style_aware_wcswidth(command) if width > widest: widest = width # add a 4-space pad @@ -3737,7 +3737,7 @@ class Cmd(cmd.Cmd): test_results = runner.run(testcase) execution_time = time.time() - start_time if test_results.wasSuccessful(): - ansi.ansi_aware_write(sys.stderr, stream.read()) + ansi.style_aware_write(sys.stderr, stream.read()) finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time) finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='=')) self.poutput(finish_msg) diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 9a23cbcd..4df733db 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -193,7 +193,7 @@ def rl_set_prompt(prompt: str) -> None: # pragma: no cover def rl_make_safe_prompt(prompt: str) -> str: # pragma: no cover - """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes. + """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes :param prompt: original prompt :return: prompt safe to pass to GNU Readline diff --git a/cmd2/utils.py b/cmd2/utils.py index ddb9f3b5..ffbe5a64 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -669,7 +669,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', if len(fill_char) != 1: raise TypeError("Fill character must be exactly one character long") - fill_char_width = ansi.ansi_safe_wcswidth(fill_char) + fill_char_width = ansi.style_aware_wcswidth(fill_char) if fill_char_width == -1: raise (ValueError("Fill character is an unprintable character")) @@ -687,9 +687,9 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', if index > 0: text_buf.write('\n') - # Use ansi_safe_wcswidth to support characters with display widths + # Use style_aware_wcswidth to support characters with display widths # greater than 1 as well as ANSI style sequences - line_width = ansi.ansi_safe_wcswidth(line) + line_width = ansi.style_aware_wcswidth(line) if line_width == -1: raise(ValueError("Text to align contains an unprintable character")) @@ -717,8 +717,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', # In cases where the fill character display width didn't divide evenly into # the gaps being filled, pad the remainder with spaces. - left_fill += ' ' * (left_fill_width - ansi.ansi_safe_wcswidth(left_fill)) - right_fill += ' ' * (right_fill_width - ansi.ansi_safe_wcswidth(right_fill)) + left_fill += ' ' * (left_fill_width - ansi.style_aware_wcswidth(left_fill)) + right_fill += ' ' * (right_fill_width - ansi.style_aware_wcswidth(right_fill)) text_buf.write(left_fill + line + right_fill) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 4f7ab652..2382c333 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -17,10 +17,10 @@ def test_strip_style(): assert base_str == ansi.strip_style(ansi_str) -def test_ansi_safe_wcswidth(): +def test_style_aware_wcswidth(): base_str = HELLO_WORLD ansi_str = ansi.style(base_str, fg='green') - assert ansi.ansi_safe_wcswidth(ansi_str) != len(ansi_str) + assert ansi.style_aware_wcswidth(ansi_str) != len(ansi_str) def test_style_none(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 27b0e3bb..b5231172 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -349,7 +349,7 @@ def test_align_text_term_width(): fill_char = ' ' term_width = shutil.get_terminal_size().columns - expected_fill = (term_width - ansi.ansi_safe_wcswidth(text)) * fill_char + expected_fill = (term_width - ansi.style_aware_wcswidth(text)) * fill_char aligned = cu.align_text(text, fill_char=fill_char, alignment=cu.TextAlignment.LEFT) assert aligned == text + expected_fill -- cgit v1.2.1 From 97b821f10913c82843c2278c70805e3d39cd2076 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Thu, 19 Dec 2019 17:31:50 -0500 Subject: Updated style regular expression to not require digits --- cmd2/ansi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/ansi.py b/cmd2/ansi.py index b5b78164..c80d3371 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -23,8 +23,8 @@ STYLE_ALWAYS = 'Always' allow_style = STYLE_TERMINAL # 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') +# This matches: colorama.ansi.CSI + 0 or more digits + m +ANSI_STYLE_RE = re.compile(r'\033\[[0-9]*m') # Foreground color presets FG_COLORS = { -- cgit v1.2.1 From 54ea40ab527174010c53ec9fa9d9ee651a2ebe51 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sat, 4 Jan 2020 19:24:37 -0500 Subject: Try setting a maximum coverage version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b59d72ff..90a3793d 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ EXTRAS_REQUIRE = { # Extra dependencies for running unit tests 'test': ["gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in tox env "mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module - 'codecov', 'pytest', 'pytest-cov', 'pytest-mock'], + 'codecov', 'pytest', 'pytest-cov', 'pytest-mock', 'coverage < 5.0'], # development only dependencies: install with 'pip install -e .[dev]' 'dev': ["mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'flake8', -- cgit v1.2.1 From b7c5946996a887b0b964c0112261c5ce6b0d89d5 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 5 Jan 2020 21:40:31 -0500 Subject: Added minimum version of setuptools_scm and added dependency on setuptools --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 90a3793d..9b78fc5b 100755 --- a/setup.py +++ b/setup.py @@ -29,9 +29,9 @@ Programming Language :: Python :: Implementation :: CPython Topic :: Software Development :: Libraries :: Python Modules """.splitlines()))) # noqa: E128 -SETUP_REQUIRES = ['setuptools_scm'] +SETUP_REQUIRES = ['setuptools_scm >= 3.0.0'] -INSTALL_REQUIRES = ['pyperclip >= 1.6', 'colorama >= 0.3.7', 'attrs >= 16.3.0', 'wcwidth >= 0.1.7'] +INSTALL_REQUIRES = ['attrs >= 16.3.0', 'colorama >= 0.3.7', 'pyperclip >= 1.6', 'setuptools >= 34.4', 'wcwidth >= 0.1.7'] EXTRAS_REQUIRE = { # Windows also requires pyreadline to ensure tab completion works -- cgit v1.2.1 From 112d8c4c05df07e7ea8e9e9a72100c4e99381275 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 5 Jan 2020 21:46:31 -0500 Subject: Updated CHANGELOG --- CHANGELOG.md | 1 + CONTRIBUTING.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20427976..fec93d02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.9.23 (TBD, 2019) * Bug Fixes * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted + * Added missing implicit dependency on `setuptools` due to build with `setuptools_scm` ## 0.9.22 (December 9, 2019) * Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6055fa7d..96c2d312 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,7 @@ The tables below list all prerequisites along with the minimum required version | [attrs](https://github.com/python-attrs/attrs) | `16.3` | | [colorama](https://github.com/tartley/colorama) | `0.3.7` | | [pyperclip](https://github.com/asweigart/pyperclip) | `1.6` | +| [setuptools](https://pypi.org/project/setuptools/) | `34.4` | | [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` | -- cgit v1.2.1 From 6b6809a394b1e0baffad46d7b91f61d77ece128d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Mon, 6 Jan 2020 11:52:27 -0500 Subject: Updated dynamic commands example to add help category --- examples/dynamic_commands.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py index 69816d40..620acb7f 100755 --- a/examples/dynamic_commands.py +++ b/examples/dynamic_commands.py @@ -3,13 +3,32 @@ """A simple example demonstrating how do_* commands can be created in a loop. """ import functools + import cmd2 -COMMAND_LIST = ['foo', 'bar', 'baz'] +from cmd2.constants import COMMAND_FUNC_PREFIX, HELP_FUNC_PREFIX + +COMMAND_LIST = ['foo', 'bar'] +CATEGORY = 'Dynamic Commands' class CommandsInLoop(cmd2.Cmd): """Example of dynamically adding do_* commands.""" def __init__(self): + # Add dynamic commands before calling cmd2.Cmd's init since it validates command names + for command in COMMAND_LIST: + # Create command function and add help category to it + cmd_func = functools.partial(self.send_text, text=command) + cmd2.categorize(cmd_func, CATEGORY) + + # Add command function to CLI object + cmd_func_name = COMMAND_FUNC_PREFIX + command + setattr(self, cmd_func_name, cmd_func) + + # Add help function to CLI object + help_func = functools.partial(self.text_help, text=command) + help_func_name = HELP_FUNC_PREFIX + command + setattr(self, help_func_name, help_func) + super().__init__(use_ipython=True) def send_text(self, args: cmd2.Statement, *, text: str): @@ -21,11 +40,6 @@ class CommandsInLoop(cmd2.Cmd): self.poutput("Simulate sending {!r} to a server and printing the response".format(text)) -for command in COMMAND_LIST: - setattr(CommandsInLoop, 'do_{}'.format(command), functools.partialmethod(CommandsInLoop.send_text, text=command)) - setattr(CommandsInLoop, 'help_{}'.format(command), functools.partialmethod(CommandsInLoop.text_help, text=command)) - - if __name__ == '__main__': app = CommandsInLoop() app.cmdloop() -- cgit v1.2.1 From 7242aea81167786be6ec9c138e50d75453d8c073 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Mon, 6 Jan 2020 21:41:38 -0500 Subject: Replace non-ASCII quotes in .coveragerc with ASCII ones --- .coveragerc | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 307e3a9a..96bf5d72 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,5 +20,5 @@ precision = 1 [html] -# (string, default “htmlcov”): where to write the HTML report files. +# (string, default "htmlcov"): where to write the HTML report files. directory = htmlcov diff --git a/setup.py b/setup.py index 9b78fc5b..b19ae7f5 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ EXTRAS_REQUIRE = { # Extra dependencies for running unit tests 'test': ["gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in tox env "mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module - 'codecov', 'pytest', 'pytest-cov', 'pytest-mock', 'coverage < 5.0'], + 'codecov', 'coverage', 'pytest', 'pytest-cov', 'pytest-mock'], # development only dependencies: install with 'pip install -e .[dev]' 'dev': ["mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'flake8', -- cgit v1.2.1 From f970c16756803ad8c03c5e544f09acd4431d8b16 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 7 Jan 2020 13:07:03 -0500 Subject: Added dim text style support --- CHANGELOG.md | 5 +++++ cmd2/ansi.py | 25 ++++++++++++++++--------- tests/test_ansi.py | 17 +++++++++++++---- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcb2a12..7d5c9745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ * Bug Fixes * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted * Added missing implicit dependency on `setuptools` due to build with `setuptools_scm` +* Enhancements + * Added dim text style support via `style()` function and `ansi.INTENSITY_DIM` setting. * Breaking changes * Renamed the following `ansi` members for accuracy in what types of ANSI escape sequences are handled * `ansi.allow_ansi` -> `ansi.allow_style` * `ansi.ansi_safe_wcswidth()` -> `ansi.style_aware_wcswidth()` * `ansi.ansi_aware_write()` -> `ansi.style_aware_write()` + * Renamed the following `ansi` members for clarification + * `ansi.BRIGHT` -> `ansi.INTENSITY_BRIGHT` + * `ansi.NORMAL` -> `ansi.INTENSITY_NORMAL` ## 0.9.22 (December 9, 2019) * Bug Fixes diff --git a/cmd2/ansi.py b/cmd2/ansi.py index c80d3371..b3dd7cc1 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -22,9 +22,8 @@ STYLE_ALWAYS = 'Always' # Controls when ANSI style style sequences are allowed in output allow_style = STYLE_TERMINAL -# Regular expression to match ANSI style sequences -# This matches: colorama.ansi.CSI + 0 or more digits + m -ANSI_STYLE_RE = re.compile(r'\033\[[0-9]*m') +# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors) +ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m') # Foreground color presets FG_COLORS = { @@ -72,8 +71,10 @@ FG_RESET = FG_COLORS['reset'] BG_RESET = BG_COLORS['reset'] RESET_ALL = Style.RESET_ALL -BRIGHT = Style.BRIGHT -NORMAL = Style.NORMAL +# Text intensities +INTENSITY_BRIGHT = Style.BRIGHT +INTENSITY_DIM = Style.DIM +INTENSITY_NORMAL = Style.NORMAL # ANSI style sequences not provided by colorama UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4) @@ -139,7 +140,8 @@ def bg_lookup(bg_name: str) -> str: return ansi_escape -def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underline: bool = False) -> str: +def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, + dim: bool = False, underline: bool = False) -> str: """ Apply ANSI colors and/or styles to a string and return it. The styling is self contained which means that at the end of the string reset code(s) are issued @@ -148,7 +150,8 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin :param text: Any object compatible with str.format() :param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name. Defaults to no color. :param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name. Defaults to no color. - :param bold: apply the bold style if True. Defaults to False. + :param bold: apply the bold style if True. Can be combined with dim. Defaults to False. + :param dim: apply the dim style if True. Can be combined with bold. Defaults to False. :param underline: apply the underline style if True. Defaults to False. :return: the stylized string """ @@ -171,8 +174,12 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin removals.append(BG_RESET) if bold: - additions.append(Style.BRIGHT) - removals.append(Style.NORMAL) + additions.append(INTENSITY_BRIGHT) + removals.append(INTENSITY_NORMAL) + + if dim: + additions.append(INTENSITY_DIM) + removals.append(INTENSITY_NORMAL) if underline: additions.append(UNDERLINE_ENABLE) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 2382c333..cb68cb28 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -45,10 +45,16 @@ def test_style_bg(): def test_style_bold(): base_str = HELLO_WORLD - ansi_str = ansi.BRIGHT + base_str + ansi.NORMAL + ansi_str = ansi.INTENSITY_BRIGHT + base_str + ansi.INTENSITY_NORMAL assert ansi.style(base_str, bold=True) == ansi_str +def test_style_dim(): + base_str = HELLO_WORLD + ansi_str = ansi.INTENSITY_DIM + base_str + ansi.INTENSITY_NORMAL + assert ansi.style(base_str, dim=True) == ansi_str + + def test_style_underline(): base_str = HELLO_WORLD ansi_str = ansi.UNDERLINE_ENABLE + base_str + ansi.UNDERLINE_DISABLE @@ -59,9 +65,12 @@ def test_style_multi(): base_str = HELLO_WORLD fg_color = 'blue' bg_color = 'green' - ansi_str = ansi.FG_COLORS[fg_color] + ansi.BG_COLORS[bg_color] + ansi.BRIGHT + ansi.UNDERLINE_ENABLE + \ - base_str + ansi.FG_RESET + ansi.BG_RESET + ansi.NORMAL + ansi.UNDERLINE_DISABLE - assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, underline=True) == ansi_str + ansi_str = (ansi.FG_COLORS[fg_color] + ansi.BG_COLORS[bg_color] + + ansi.INTENSITY_BRIGHT + ansi.INTENSITY_DIM + ansi.UNDERLINE_ENABLE + + base_str + + ansi.FG_RESET + ansi.BG_RESET + + ansi.INTENSITY_NORMAL + ansi.INTENSITY_NORMAL + ansi.UNDERLINE_DISABLE) + assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, dim=True, underline=True) == ansi_str def test_style_color_not_exist(): -- cgit v1.2.1 From 383853ee92f69c3e4ccadacd34e0f22e1e896345 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Tue, 7 Jan 2020 13:09:43 -0500 Subject: Updated documentation --- cmd2/cmd2.py | 2 +- docs/features/settings.rst | 2 +- tests/conftest.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f133381b..434ade5f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -207,7 +207,7 @@ class Cmd(cmd.Cmd): self.settable = \ { # 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 ' + 'allow_style': ('Allow ANSI text style sequences in output ' '(valid values: {}, {}, {})'.format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER)), diff --git a/docs/features/settings.rst b/docs/features/settings.rst index 7cc0d727..f305a68c 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -46,7 +46,7 @@ comments, is viewable from within a running application with:: (Cmd) set --long - allow_style: Terminal # Allow ANSI style sequences in output (valid values: Terminal, Always, Never) + allow_style: Terminal # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output diff --git a/tests/conftest.py b/tests/conftest.py index 3d4059d9..fe74ffe2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,7 +102,7 @@ timing: False """ SHOW_LONG = """ -allow_style: Terminal # Allow ANSI style sequences in output (valid values: Terminal, Always, Never) +allow_style: Terminal # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output -- cgit v1.2.1 From de485fc3478e675a1fa327c10a8297a1ea17f46a Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 9 Jan 2020 21:58:06 -0500 Subject: Update copyright through 2020 and restore documentation copyright to MIT license During recent work to upgrade the format of the Sphinx documentation, the license the docs are copyright under was accidentally changed from MIT to CreativeCommons. This restores the documentation to be under an MIT license so there is a single license governing both code and documentation. --- LICENSE | 2 +- docs/conf.py | 2 +- docs/copyright.rst | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index 7f916117..8c187203 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2008-2019 Catherine Devlin and others +Copyright (c) 2008-2020 Catherine Devlin and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index 5463d0bb..7a8da9d1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ master_doc = 'index' # General information about the project. project = 'cmd2' -copyright = '2010-2019, cmd2 contributors' +copyright = '2010-2020, cmd2 contributors' author = 'cmd2 contributors' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/copyright.rst b/docs/copyright.rst index b7183e62..30cf255c 100644 --- a/docs/copyright.rst +++ b/docs/copyright.rst @@ -1,7 +1,6 @@ Copyright ========= -The ``cmd2`` documentation is Copyright 2010-2019 by the `cmd2 contributors +The ``cmd2`` documentation is Copyright 2010-2020 by the `cmd2 contributors `_ and is licensed -under a `Creative Commons Attribution 4.0 International License -`_. +under an `MIT License `_. -- cgit v1.2.1 From d4556962799e68ea4d54ff86186428d17edcaef9 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Thu, 9 Jan 2020 22:20:54 -0500 Subject: Updated CHANGELOG for a release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5c9745..0523ee5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.9.23 (TBD, 2019) +## 0.9.23 (January 9, 2020) * Bug Fixes * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted * Added missing implicit dependency on `setuptools` due to build with `setuptools_scm` -- cgit v1.2.1