From d2fd437db7305f0e4c1479c84fdce2fd8499a38c Mon Sep 17 00:00:00 2001 From: kotfu Date: Sat, 11 Aug 2018 12:27:11 -0600 Subject: =?UTF-8?q?Update=20help=20for=20=E2=80=98quiet=E2=80=99=20setting?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/unfreefeatures.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst index 41144c8f..a2eff50a 100644 --- a/docs/unfreefeatures.rst +++ b/docs/unfreefeatures.rst @@ -146,16 +146,18 @@ Text output can be colored by wrapping it in the ``colorize`` method. .. automethod:: cmd2.cmd2.Cmd.colorize -.. _quiet: +.. _quiet: +Supressing non-essential output +=============================== -quiet -===== +The ``quiet`` setting controls whether ``self.pfeedback()`` actually produces +any output. If ``quiet`` is ``False``, then the output will be produced. If +``quiet`` is ``True``, no output will be produced. -Controls whether ``self.pfeedback('message')`` output is suppressed; -useful for non-essential feedback that the user may not always want -to read. ``quiet`` is only relevant if -``app.pfeedback`` is sometimes used. +This makes ``self.pfeedback()`` useful for non-essential output like status +messages. Users can control whether they would like to see these messages by changing +the value of the ``quiet`` setting. select -- cgit v1.2.1 From 0053f18fade2f2148daf19975581e8372ca7a69b Mon Sep 17 00:00:00 2001 From: kotfu Date: Sat, 11 Aug 2018 12:40:04 -0600 Subject: Colors now defaults to true on all platforms --- cmd2/cmd2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 94b75e5f..34a28048 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -317,7 +317,7 @@ class Cmd(cmd.Cmd): reserved_words = [] # Attributes which ARE dynamically settable at runtime - colors = (platform.system() != 'Windows') + colors = True continuation_prompt = '> ' debug = False echo = False @@ -337,7 +337,7 @@ class Cmd(cmd.Cmd): # To make an attribute settable with the "do_set" command, add it to this ... # This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key - settable = {'colors': 'Colorized output (*nix only)', + settable = {'colors': 'Allow colorized output', 'continuation_prompt': 'On 2nd+ line of input', 'debug': 'Show full error stack on error', 'echo': 'Echo command issued into output', -- cgit v1.2.1 From 1daf5d2cb63145408520f39c799425a82b2ec948 Mon Sep 17 00:00:00 2001 From: kotfu Date: Sat, 11 Aug 2018 19:54:10 -0600 Subject: First pass at improved colorizing - colors setting now allows Always, Terminal, Never values - poutput() and perror() honor these new settings --- cmd2/cmd2.py | 52 ++++++++++----- cmd2/constants.py | 5 ++ cmd2/transcript.py | 3 + docs/settingchanges.rst | 4 +- docs/unfreefeatures.rst | 25 +++++-- examples/colors.py | 143 ++++++++++++++++++++++++++++++++++++++++ tests/conftest.py | 18 ++--- tests/scripts/postcmds.txt | 2 +- tests/scripts/precmds.txt | 2 +- tests/test_cmd2.py | 143 ++++++++++++++++++++++++++++++++++++++-- tests/transcripts/regex_set.txt | 4 +- 11 files changed, 358 insertions(+), 43 deletions(-) create mode 100755 examples/colors.py diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 34a28048..1cdec0b1 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -40,7 +40,7 @@ import platform import re import shlex import sys -from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO from . import constants from . import utils @@ -317,7 +317,7 @@ class Cmd(cmd.Cmd): reserved_words = [] # Attributes which ARE dynamically settable at runtime - colors = True + colors = constants.COLORS_TERMINAL continuation_prompt = '> ' debug = False echo = False @@ -544,25 +544,42 @@ class Cmd(cmd.Cmd): # Make sure settable parameters are sorted alphabetically by key self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0])) - def poutput(self, msg: str, end: str='\n') -> None: - """Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present. + def decolorized_write(self, fileobj: IO, msg: str): + """Write a string to a fileobject, stripping ANSI escape sequences if necessary - Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and - that process terminates before the cmd2 command is finished executing. - - :param msg: message to print to current stdout - anything convertible to a str with '{}'.format() is OK - :param end: string appended after the end of the message if not already present, default a newline + Honor the current colors setting, which requires us to check whether the + fileobject is a tty. + """ + if self.colors == constants.COLORS_NEVER: + msg = utils.strip_ansi(msg) + if self.colors == constants.COLORS_TERMINAL: + if not fileobj.isatty(): + msg = utils.strip_ansi(msg) + fileobj.write(msg) + + def poutput(self, msg: Any, end: str='\n') -> None: + """Smarter self.stdout.write(); color aware and adds newline of not present. + + Also handles BrokenPipeError exceptions for when a commands's output has + been piped to another process and that process terminates before the + cmd2 command is finished executing. + + :param msg: message to print to current stdout - anything convertible to + a str with '{}'.format() is OK :param end: string appended after the end + of the message if not already present, default a newline """ if msg is not None and msg != '': try: msg_str = '{}'.format(msg) - self.stdout.write(msg_str) + self.decolorized_write(self.stdout, msg_str) if not msg_str.endswith(end): self.stdout.write(end) except BrokenPipeError: - # This occurs if a command's output is being piped to another process and that process closes before the - # command is finished. If you would like your application to print a warning message, then set the - # broken_pipe_warning attribute to the message you want printed. + # This occurs if a command's output is being piped to another + # process and that process closes before the command is + # finished. If you would like your application to print a + # warning message, then set the broken_pipe_warning attribute + # to the message you want printed. if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) @@ -579,14 +596,15 @@ class Cmd(cmd.Cmd): if isinstance(err, Exception): err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err) - sys.stderr.write(self.colorize(err_msg, 'red')) else: - err_msg = self.colorize("ERROR: {}\n".format(err), 'red') - sys.stderr.write(err_msg) + err_msg = "ERROR: {}\n".format(err) + err_msg = Fore.RED + err_msg + Fore.RESET + self.decolorized_write(sys.stderr, err_msg) if traceback_war: war = "To enable full traceback, run the following command: 'set debug true'\n" - sys.stderr.write(self.colorize(war, 'yellow')) + war = Fore.YELLOW + war + Fore.RESET + self.decolorized_write(sys.stderr, war) def pfeedback(self, msg: str) -> None: """For printing nonessential feedback. Can be silenced with `quiet`. diff --git a/cmd2/constants.py b/cmd2/constants.py index d3e8a125..3c133b70 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -17,3 +17,8 @@ REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND] ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m') LINE_FEED = '\n' + +# values for colors setting +COLORS_NEVER = 'Never' +COLORS_TERMINAL = 'Terminal' +COLORS_ALWAYS = 'Always' diff --git a/cmd2/transcript.py b/cmd2/transcript.py index 5ba8d20d..a6b4cb1a 100644 --- a/cmd2/transcript.py +++ b/cmd2/transcript.py @@ -224,3 +224,6 @@ class OutputTrap(object): result = self.contents self.contents = '' return result + + def isatty(self) -> bool: + return True diff --git a/docs/settingchanges.rst b/docs/settingchanges.rst index 02955273..e08b6026 100644 --- a/docs/settingchanges.rst +++ b/docs/settingchanges.rst @@ -137,7 +137,7 @@ comments, is viewable from within a running application with:: (Cmd) set --long - colors: True # Colorized output (*nix only) + colors: Terminal # Allow colorized output continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output @@ -150,5 +150,5 @@ with:: Any of these user-settable parameters can be set while running your app with the ``set`` command like so:: - set colors False + set colors Never diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst index a2eff50a..93249425 100644 --- a/docs/unfreefeatures.rst +++ b/docs/unfreefeatures.rst @@ -139,12 +139,29 @@ instead. These methods have these advantages: .. automethod:: cmd2.cmd2.Cmd.ppaged -color -===== +Colored Output +============== -Text output can be colored by wrapping it in the ``colorize`` method. +The output methods in the previous section all honor the ``colors`` setting, +which has three possible values: -.. automethod:: cmd2.cmd2.Cmd.colorize +Never + poutput() and pfeedback() strip all ANSI escape sequences + which instruct the terminal to colorize output + +Terminal + (the default value) poutput() and pfeedback() 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, preferably using some python color library + like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`. + +Always + poutput() and pfeedback() never strip ANSI escape sequences, regardless of + the output destination + + +The previously recommended ``colorize`` method is now deprecated. .. _quiet: diff --git a/examples/colors.py b/examples/colors.py new file mode 100755 index 00000000..59e108b6 --- /dev/null +++ b/examples/colors.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +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 colors setting has three possible values: + +Never + poutput() and pfeedback() strip all ANSI escape sequences + which instruct the terminal to colorize output + +Terminal + (the default value) poutput() and pfeedback() 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, preferably using some python color library + like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`. + +Always + poutput() and pfeedback() never strip ANSI escape sequences, regardless of + the output destination + + +""" + +import random +import argparse + +import cmd2 +from colorama import Fore, Back + +FG_COLORS = { + 'black': Fore.BLACK, + 'red': Fore.RED, + 'green': Fore.GREEN, + 'yellow': Fore.YELLOW, + 'blue': Fore.BLUE, + 'magenta': Fore.MAGENTA, + 'cyan': Fore.CYAN, + 'white': Fore.WHITE, +} +BG_COLORS ={ + 'black': Back.BLACK, + 'red': Back.RED, + 'green': Back.GREEN, + 'yellow': Back.YELLOW, + 'blue': Back.BLUE, + 'magenta': Back.MAGENTA, + 'cyan': Back.CYAN, + 'white':Back.WHITE, +} + +class CmdLineApp(cmd2.Cmd): + """Example cmd2 application demonstrating colorized output.""" + + # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist + # default_to_shell = True + MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] + MUMBLE_FIRST = ['so', 'like', 'well'] + MUMBLE_LAST = ['right?'] + + def __init__(self): + self.multiline_commands = ['orate'] + self.maxrepeats = 3 + + # Add stuff to settable and shortcuts before calling base class initializer + self.settable['maxrepeats'] = 'max repetitions for speak command' + self.shortcuts.update({'&': 'speak'}) + + # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell + super().__init__(use_ipython=False) + + speak_parser = argparse.ArgumentParser() + speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + speak_parser.add_argument('-f', '--fg', help='foreground color to apply to output') + speak_parser.add_argument('-b', '--bg', help='background color to apply to output') + speak_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(speak_parser) + def do_speak(self, args): + """Repeats what you tell me to.""" + words = [] + for word in args.words: + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + words.append(word) + + repetitions = args.repeat or 1 + + color_on = '' + if args.fg and args.fg in FG_COLORS: + color_on += FG_COLORS[args.fg] + if args.bg and args.bg in BG_COLORS: + color_on += BG_COLORS[args.bg] + color_off = Fore.RESET + Back.RESET + + for i in range(min(repetitions, self.maxrepeats)): + # .poutput handles newlines, and accommodates output redirection too + self.poutput(color_on + ' '.join(words) + color_off) + + do_say = do_speak # now "say" is a synonym for "speak" + do_orate = do_speak # another synonym, but this one takes multi-line input + + mumble_parser = argparse.ArgumentParser() + mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat') + mumble_parser.add_argument('-f', '--fg', help='foreground color to apply to output') + mumble_parser.add_argument('-b', '--bg', help='background color to apply to output') + mumble_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(mumble_parser) + def do_mumble(self, args): + """Mumbles what you tell me to.""" + color_on = '' + if args.fg and args.fg in FG_COLORS: + color_on += FG_COLORS[args.fg] + if args.bg and args.bg in BG_COLORS: + color_on += BG_COLORS[args.bg] + color_off = Fore.RESET + Back.RESET + + repetitions = args.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + output = [] + if random.random() < .33: + output.append(random.choice(self.MUMBLE_FIRST)) + for word in args.words: + if random.random() < .40: + output.append(random.choice(self.MUMBLES)) + output.append(word) + if random.random() < .25: + output.append(random.choice(self.MUMBLE_LAST)) + self.poutput(color_on + ' '.join(output) + color_off) + + +if __name__ == '__main__': + c = CmdLineApp() + c.cmdloop() diff --git a/tests/conftest.py b/tests/conftest.py index 3f3b862e..9ca506af 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,11 +80,8 @@ SHORTCUTS_TXT = """Shortcuts for other commands: @@: _relative_load """ -expect_colors = True -if sys.platform.startswith('win'): - expect_colors = False # Output from the show command with default settings -SHOW_TXT = """colors: {} +SHOW_TXT = """colors: Terminal continuation_prompt: > debug: False echo: False @@ -94,14 +91,10 @@ locals_in_py: False prompt: (Cmd) quiet: False timing: False -""".format(expect_colors) +""" -if expect_colors: - color_str = 'True ' -else: - color_str = 'False' SHOW_LONG = """ -colors: {} # Colorized output (*nix only) +colors: Terminal # Allow colorized output continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output @@ -111,7 +104,7 @@ locals_in_py: False # Allow access to your application in py via self prompt: (Cmd) # The prompt issued to solicit input quiet: False # Don't print nonessential feedback timing: False # Report execution times -""".format(color_str) +""" class StdOut(object): @@ -128,6 +121,9 @@ class StdOut(object): def clear(self): self.buffer = '' + def isatty(self): + return True + def normalize(block): """ Normalize a block of text to perform comparison. diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt index 2b478b57..dea8f265 100644 --- a/tests/scripts/postcmds.txt +++ b/tests/scripts/postcmds.txt @@ -1 +1 @@ -set colors off +set colors Never diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt index d0b27fb6..0ae7eae8 100644 --- a/tests/scripts/precmds.txt +++ b/tests/scripts/precmds.txt @@ -1 +1 @@ -set colors on +set colors Always diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 0ec993e9..efdfee7e 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -13,6 +13,7 @@ import os import sys import tempfile +import colorama import pytest # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available @@ -561,11 +562,11 @@ def test_load_nested_loads(base_app, request): expected = """ %s _relative_load precmds.txt -set colors on +set colors Always help shortcuts _relative_load postcmds.txt -set colors off""" % initial_load +set colors Never""" % initial_load assert run_cmd(base_app, 'history -s') == normalize(expected) @@ -583,11 +584,11 @@ def test_base_runcmds_plus_hooks(base_app, request): 'load ' + postfilepath]) expected = """ load %s -set colors on +set colors Always help shortcuts load %s -set colors off""" % (prefilepath, postfilepath) +set colors Never""" % (prefilepath, postfilepath) assert run_cmd(base_app, 'history -s') == normalize(expected) @@ -820,7 +821,6 @@ def test_base_colorize(base_app): else: assert color_test == '\x1b[31mTest\x1b[39m' - def _expected_no_editor_error(): expected_exception = 'OSError' # If PyPy, expect a different exception than with Python 3 @@ -1916,3 +1916,136 @@ def test_bad_history_file_path(capsys, request): else: # GNU readline raises an exception upon trying to read the directory as a file assert 'readline cannot read' in err + + +class ColorsApp(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def do_echo(self, args): + self.poutput(args) + self.perror(args, False) + + def do_echo_error(self, args): + color_on = colorama.Fore.RED + colorama.Back.BLACK + color_off = colorama.Style.RESET_ALL + self.poutput(color_on + args + color_off) + # perror uses colors by default + self.perror(args, False) + +def test_colors_default(): + app = ColorsApp() + assert app.colors == cmd2.constants.COLORS_TERMINAL + +def test_colors_pouterr_always_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_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 + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + # errors always have colors + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_pouterr_always_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_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 + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + # errors always have colors + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_terminal_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_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 + out, err = capsys.readouterr() + assert len(out) > len('oopsie\n') + assert 'oopsie' in out + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + + # but this one shouldn't + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert len(err) > len('Error: oopsie\n') + assert 'ERROR: oopsie' in err + +def test_colors_terminal_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_TERMINAL + 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() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + +def test_colors_never_tty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_NEVER + 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() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + +def test_colors_never_notty(mocker, capsys): + app = ColorsApp() + app.colors = cmd2.constants.COLORS_NEVER + 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() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' + + app.onecmd_plus_hooks('echo oopsie') + out, err = capsys.readouterr() + assert out == 'oopsie\n' + assert err == 'ERROR: oopsie\n' diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index b818c464..d45672a7 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -1,10 +1,10 @@ # Run this transcript with "python example.py -t transcript_regex.txt" -# The regex for colors is because no color on Windows. +# The regex for colors shows all possible settings for colors # The regex for editor will match whatever program you use. # Regexes on prompts just make the trailing space obvious (Cmd) set -colors: /(True|False)/ +colors: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False -- cgit v1.2.1 From c43392601d30df6e79e4a9c044a16b7abe5f6ab1 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Sep 2018 23:06:55 -0700 Subject: Fixed unit test failures Fixed following unit test failures: - Doc build failure due to Sphinx warnings - Windows unit test failures related to colorize (hopefully) --- cmd2/cmd2.py | 5 ++--- docs/unfreefeatures.rst | 5 +++-- tests/test_cmd2.py | 17 ++++------------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 0bfc0037..2263d95f 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -567,9 +567,8 @@ class Cmd(cmd.Cmd): been piped to another process and that process terminates before the cmd2 command is finished executing. - :param msg: message to print to current stdout - anything convertible to - a str with '{}'.format() is OK :param end: string appended after the end - of the message if not already present, default a newline + :param msg: message to print to current stdout - anything convertible to a str with '{}'.format() is OK + :param end: string appended after the end of the message if not already present, default a newline """ if msg is not None and msg != '': try: diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst index 795c919e..0124132b 100644 --- a/docs/unfreefeatures.rst +++ b/docs/unfreefeatures.rst @@ -165,8 +165,9 @@ The previously recommended ``colorize`` method is now deprecated. .. _quiet: -Supressing non-essential output -=============================== + +Suppressing non-essential output +================================ The ``quiet`` setting controls whether ``self.pfeedback()`` actually produces any output. If ``quiet`` is ``False``, then the output will be produced. If diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 85e6c2f8..6fb64b86 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -813,11 +813,7 @@ def test_base_colorize(base_app): # But if we create a fresh Cmd() instance, it will fresh_app = cmd2.Cmd() color_test = fresh_app.colorize('Test', 'red') - # Actually, colorization only ANSI escape codes is only applied on non-Windows systems - if sys.platform == 'win32': - assert color_test == 'Test' - else: - assert color_test == '\x1b[31mTest\x1b[39m' + assert color_test == '\x1b[31mTest\x1b[39m' def _expected_no_editor_error(): expected_exception = 'OSError' @@ -1107,14 +1103,9 @@ def test_ansi_prompt_escaped(): readline_hack_end = "\x02" readline_safe_prompt = app._surround_ansi_escapes(color_prompt) - if sys.platform.startswith('win'): - # colorize() does nothing on Windows due to lack of ANSI color support - assert prompt == color_prompt - assert readline_safe_prompt == prompt - else: - assert prompt != color_prompt - assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end) - assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end) + assert prompt != color_prompt + assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end) + assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end) class HelpApp(cmd2.Cmd): -- cgit v1.2.1 From ce3b5fe450a4cdde2ebf89f1386395c0bad13980 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Sep 2018 23:11:18 -0700 Subject: Ensure pytest-mock is installed for Windows unit testing Need mocker fixture available for some of the new unit tests --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 40a8ba5e..1e8a4468 100644 --- a/tox.ini +++ b/tox.ini @@ -46,6 +46,7 @@ deps = pyperclip pyreadline pytest + pytest-mock commands = py.test -v [testenv:py36] @@ -68,6 +69,7 @@ deps = pyreadline pytest pytest-cov + pytest-mock commands = py.test {posargs} --cov codecov @@ -88,6 +90,7 @@ deps = pyreadline pytest pytest-cov + pytest-mock commands = py.test {posargs} --cov codecov -- cgit v1.2.1 From 52d76c18246e70a5311b94c452c236524d64586e Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 4 Sep 2018 23:29:21 -0700 Subject: Try to fix last failing Windows unit test by removing special behavior of _surround_ansi_escapes method --- cmd2/cmd2.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 2263d95f..37b0d2d9 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2096,10 +2096,6 @@ class Cmd(cmd.Cmd): :param end: end code to tell GNU Readline about end of invisible characters :return: prompt safe to pass to GNU Readline """ - # Windows terminals don't use ANSI escape codes and Windows readline isn't based on GNU Readline - if sys.platform == "win32": - return prompt - escaped = False result = "" -- cgit v1.2.1 From ccbe1276f786e94f1996877fdec6d070705f5ab8 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Wed, 19 Sep 2018 15:32:37 -0400 Subject: cmd2.Cmd.__init__ now initializes colorama and tells it to never strip ANSI codes since cmd2 deals with that Also: - Finished editing poutput(), ppaged(), and pfeedback() methods to strip ANSI color when appropriate - Changed attr.ib() factory usage so cmd2 is compatible with older versions of attrs --- cmd2/cmd2.py | 38 +++++++++++++++++++++----------------- cmd2/parsing.py | 4 ++-- examples/colors.py | 11 ++++++----- tests/conftest.py | 2 +- 4 files changed, 30 insertions(+), 25 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index db96228b..da5b528a 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -32,11 +32,11 @@ Git repository on GitHub at https://github.com/python-cmd2/cmd2 import argparse import cmd import collections +import colorama from colorama import Fore import glob import inspect import os -import platform import re import shlex import sys @@ -337,7 +337,7 @@ class Cmd(cmd.Cmd): # To make an attribute settable with the "do_set" command, add it to this ... # This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key - settable = {'colors': 'Allow colorized output', + settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)', 'continuation_prompt': 'On 2nd+ line of input', 'debug': 'Show full error stack on error', 'echo': 'Echo command issued into output', @@ -369,6 +369,9 @@ class Cmd(cmd.Cmd): except AttributeError: pass + # Override whether ansi codes should be stripped from the output since cmd2 has its own logic for doing this + colorama.init(strip=False) + # initialize plugin system # needs to be done before we call __init__(0) self._initialize_plugin_system() @@ -417,13 +420,13 @@ class Cmd(cmd.Cmd): self._STOP_AND_EXIT = True # cmd convention self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'}, - 'cyan': {True: '\x1b[36m', False: '\x1b[39m'}, - 'blue': {True: '\x1b[34m', False: '\x1b[39m'}, - 'red': {True: '\x1b[31m', False: '\x1b[39m'}, - 'magenta': {True: '\x1b[35m', False: '\x1b[39m'}, - 'green': {True: '\x1b[32m', False: '\x1b[39m'}, - 'underline': {True: '\x1b[4m', False: '\x1b[24m'}, - 'yellow': {True: '\x1b[33m', False: '\x1b[39m'}} + 'cyan': {True: Fore.CYAN, False: Fore.RESET}, + 'blue': {True: Fore.BLUE, False: Fore.RESET}, + 'red': {True: Fore.RED, False: Fore.RESET}, + 'magenta': {True: Fore.MAGENTA, False: Fore.RESET}, + 'green': {True: Fore.GREEN, False: Fore.RESET}, + 'underline': {True: '\x1b[4m', False: Fore.RESET}, + 'yellow': {True: Fore.YELLOW, False: Fore.RESET}} # Used load command to store the current script dir as a LIFO queue to support _relative_load command self._script_dir = [] @@ -547,17 +550,15 @@ class Cmd(cmd.Cmd): # Make sure settable parameters are sorted alphabetically by key self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0])) - def decolorized_write(self, fileobj: IO, msg: str): + def decolorized_write(self, fileobj: IO, msg: str) -> None: """Write a string to a fileobject, stripping ANSI escape sequences if necessary Honor the current colors setting, which requires us to check whether the fileobject is a tty. """ - if self.colors == constants.COLORS_NEVER: + if self.colors.lower() == constants.COLORS_NEVER.lower() or \ + (self.colors.lower() == constants.COLORS_TERMINAL.lower() and not fileobj.isatty()): msg = utils.strip_ansi(msg) - if self.colors == constants.COLORS_TERMINAL: - if not fileobj.isatty(): - msg = utils.strip_ansi(msg) fileobj.write(msg) def poutput(self, msg: Any, end: str='\n') -> None: @@ -575,7 +576,7 @@ class Cmd(cmd.Cmd): msg_str = '{}'.format(msg) self.decolorized_write(self.stdout, msg_str) if not msg_str.endswith(end): - self.stdout.write(end) + self.decolorized_write(self.stdout, end) except BrokenPipeError: # This occurs if a command's output is being piped to another # process and that process closes before the command is @@ -615,7 +616,7 @@ class Cmd(cmd.Cmd): if self.feedback_to_output: self.poutput(msg) else: - sys.stderr.write("{}\n".format(msg)) + self.decolorized_write(sys.stderr, "{}\n".format(msg)) def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None: """Print output using a pager if it would go off screen and stdout isn't currently being redirected. @@ -651,6 +652,9 @@ 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_py and not self._script_dir: + if self.colors.lower() == constants.COLORS_NEVER.lower(): + msg_str = utils.strip_ansi(msg_str) + pager = self.pager if chop: pager = self.pager_chop @@ -686,7 +690,7 @@ class Cmd(cmd.Cmd): is running on Windows, will return ``val`` unchanged. ``color`` should be one of the supported strings (or styles): red/blue/green/cyan/magenta, bold, underline""" - if self.colors and (self.stdout == self.initial_stdout): + if self.colors.lower() != constants.COLORS_NEVER.lower() and (self.stdout == self.initial_stdout): return self._colorcodes[color][True] + val + self._colorcodes[color][False] return val diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 8edfacb9..92dc1b40 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -90,7 +90,7 @@ class Statement(str): command = attr.ib(default='', validator=attr.validators.instance_of(str), type=str) # list of arguments to the command, not including any output redirection or terminators; quoted args remain quoted - arg_list = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[str]) + arg_list = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list), type=List[str]) # if the command is a multiline command, the name of the command, otherwise empty multiline_command = attr.ib(default='', validator=attr.validators.instance_of(str), type=str) @@ -102,7 +102,7 @@ class Statement(str): suffix = attr.ib(default='', validator=attr.validators.instance_of(str), type=str) # if output was piped to a shell command, the shell command as a list of tokens - pipe_to = attr.ib(factory=list, validator=attr.validators.instance_of(list), type=List[str]) + pipe_to = attr.ib(default=attr.Factory(list), validator=attr.validators.instance_of(list), type=List[str]) # if output was redirected, the redirection token, i.e. '>>' output = attr.ib(default='', validator=attr.validators.instance_of(str), type=str) diff --git a/examples/colors.py b/examples/colors.py index 59e108b6..fb2de045 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -53,6 +53,7 @@ BG_COLORS ={ 'white':Back.WHITE, } + class CmdLineApp(cmd2.Cmd): """Example cmd2 application demonstrating colorized output.""" @@ -71,14 +72,14 @@ class CmdLineApp(cmd2.Cmd): self.shortcuts.update({'&': 'speak'}) # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(use_ipython=False) + super().__init__(use_ipython=True) speak_parser = argparse.ArgumentParser() speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - speak_parser.add_argument('-f', '--fg', help='foreground color to apply to output') - speak_parser.add_argument('-b', '--bg', help='background color to apply to output') + speak_parser.add_argument('-f', '--fg', choices=FG_COLORS, help='foreground color to apply to output') + speak_parser.add_argument('-b', '--bg', choices=BG_COLORS, help='background color to apply to output') speak_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(speak_parser) @@ -95,9 +96,9 @@ class CmdLineApp(cmd2.Cmd): repetitions = args.repeat or 1 color_on = '' - if args.fg and args.fg in FG_COLORS: + if args.fg: color_on += FG_COLORS[args.fg] - if args.bg and args.bg in BG_COLORS: + if args.bg: color_on += BG_COLORS[args.bg] color_off = Fore.RESET + Back.RESET diff --git a/tests/conftest.py b/tests/conftest.py index c86748e8..e177496b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -94,7 +94,7 @@ timing: False """ SHOW_LONG = """ -colors: Terminal # Allow colorized output +colors: Terminal # Allow colorized 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 e17d0e96990e0d8215ff1f1d234c46ca7c8f01ac Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Fri, 21 Sep 2018 00:34:51 -0400 Subject: Updated a couple example transcripts now that colors is no longer True/False Also: - Minor whitespace tweaks to new colors.py example --- examples/colors.py | 6 ++---- examples/transcripts/exampleSession.txt | 2 +- examples/transcripts/transcript_regex.txt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/colors.py b/examples/colors.py index fb2de045..8765aee0 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -22,8 +22,6 @@ Terminal Always poutput() and pfeedback() never strip ANSI escape sequences, regardless of the output destination - - """ import random @@ -42,7 +40,7 @@ FG_COLORS = { 'cyan': Fore.CYAN, 'white': Fore.WHITE, } -BG_COLORS ={ +BG_COLORS = { 'black': Back.BLACK, 'red': Back.RED, 'green': Back.GREEN, @@ -50,7 +48,7 @@ BG_COLORS ={ 'blue': Back.BLUE, 'magenta': Back.MAGENTA, 'cyan': Back.CYAN, - 'white':Back.WHITE, + 'white': Back.WHITE, } diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt index 6318776f..38fb0659 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 -colors: /(True|False)/ +colors: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt index 08588ab1..6980fac6 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 -colors: /(True|False)/ +colors: /(Terminal|Always|Never)/ continuation_prompt: >/ / debug: False echo: False -- cgit v1.2.1 From d9fa1e1ea0d9d84f9ab607a40f181bbc1274d1e4 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 23 Sep 2018 10:58:24 -0400 Subject: Updated CHANGELOG with info based on color support changes --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf3c961..25c31e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ for formatting help/description text * Aliases are now sorted alphabetically * The **set** command now tab-completes settable parameter names + * Cross-platform colored output support + * ``colorama`` gets initialized properly in ``Cmd.__init()`` + * The ``Cmd.colors`` setting is no longer platform dependent and now has three values: + * Terminal (default) - output methods do not strip any ANSI escape sequences when output is a terminal, but + if the output is a pipe or a file the escape sequences are stripped + * Always - output methods **never** strip ANSI escape sequences, regardless of the output destination + * Never - output methods strip all ANSI escape sequences +* Deprecations + * Deprecated the builtin ``cmd2`` suport for colors including ``Cmd.colorize()`` and ``Cmd._colorcodes`` ## 0.9.4 (August 21, 2018) * Bug Fixes -- cgit v1.2.1 From b82015248f4a1c941864c6608ab4457da752df95 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Sun, 23 Sep 2018 11:22:35 -0400 Subject: Added plumbum_colors.py example of using plumbum.colors --- examples/plumbum_colors.py | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100755 examples/plumbum_colors.py diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py new file mode 100755 index 00000000..942eaf80 --- /dev/null +++ b/examples/plumbum_colors.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +A sample application for cmd2. Demonstrating colorized output using the plumbum package. + +Experiment with the command line options on the `speak` command to see how +different output colors ca + +The colors setting has three possible values: + +Never + poutput() and pfeedback() strip all ANSI escape sequences + which instruct the terminal to colorize output + +Terminal + (the default value) poutput() and pfeedback() 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, preferably using some python color library + like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`. + +Always + poutput() and pfeedback() never strip ANSI escape sequences, regardless of + the output destination + +WARNING: This example requires the plumbum package, which isn't normally required by cmd2. +""" + +import random +import argparse + +import cmd2 +from plumbum.colors import fg, bg, reset + +FG_COLORS = { + 'black': fg.Black, + 'red': fg.DarkRedA, + 'green': fg.MediumSpringGreen, + 'yellow': fg.LightYellow, + 'blue': fg.RoyalBlue1, + 'magenta': fg.Purple, + 'cyan': fg.SkyBlue1, + 'white': fg.White, +} +BG_COLORS = { + 'black': bg.BLACK, + 'red': bg.DarkRedA, + 'green': bg.MediumSpringGreen, + 'yellow': bg.LightYellow, + 'blue': bg.RoyalBlue1, + 'magenta': bg.Purple, + 'cyan': bg.SkyBlue1, + 'white': bg.White, +} + + +class CmdLineApp(cmd2.Cmd): + """Example cmd2 application demonstrating colorized output.""" + + # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist + # default_to_shell = True + MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] + MUMBLE_FIRST = ['so', 'like', 'well'] + MUMBLE_LAST = ['right?'] + + def __init__(self): + self.multiline_commands = ['orate'] + self.maxrepeats = 3 + + # Add stuff to settable and shortcuts before calling base class initializer + self.settable['maxrepeats'] = 'max repetitions for speak command' + self.shortcuts.update({'&': 'speak'}) + + # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell + super().__init__(use_ipython=True) + + speak_parser = argparse.ArgumentParser() + speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + speak_parser.add_argument('-f', '--fg', choices=FG_COLORS, help='foreground color to apply to output') + speak_parser.add_argument('-b', '--bg', choices=BG_COLORS, help='background color to apply to output') + speak_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(speak_parser) + def do_speak(self, args): + """Repeats what you tell me to.""" + words = [] + for word in args.words: + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + words.append(word) + + repetitions = args.repeat or 1 + + color_on = '' + if args.fg: + color_on += FG_COLORS[args.fg] + if args.bg: + color_on += BG_COLORS[args.bg] + color_off = reset + + for i in range(min(repetitions, self.maxrepeats)): + # .poutput handles newlines, and accommodates output redirection too + self.poutput(color_on + ' '.join(words) + color_off) + + do_say = do_speak # now "say" is a synonym for "speak" + do_orate = do_speak # another synonym, but this one takes multi-line input + + mumble_parser = argparse.ArgumentParser() + mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat') + mumble_parser.add_argument('-f', '--fg', help='foreground color to apply to output') + mumble_parser.add_argument('-b', '--bg', help='background color to apply to output') + mumble_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(mumble_parser) + def do_mumble(self, args): + """Mumbles what you tell me to.""" + color_on = '' + if args.fg and args.fg in FG_COLORS: + color_on += FG_COLORS[args.fg] + if args.bg and args.bg in BG_COLORS: + color_on += BG_COLORS[args.bg] + color_off = Fore.RESET + Back.RESET + + repetitions = args.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + output = [] + if random.random() < .33: + output.append(random.choice(self.MUMBLE_FIRST)) + for word in args.words: + if random.random() < .40: + output.append(random.choice(self.MUMBLES)) + output.append(word) + if random.random() < .25: + output.append(random.choice(self.MUMBLE_LAST)) + self.poutput(color_on + ' '.join(output) + color_off) + + +if __name__ == '__main__': + c = CmdLineApp() + c.cmdloop() -- cgit v1.2.1 From a2ad70bd79a78123d2bb67b1ff1ed0b815dd721d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Mon, 24 Sep 2018 11:13:42 -0400 Subject: Added optional color argument to poutput() for providing a color escape code to colorize the output with Also: - Added optional err_color and war_color arguments to perror() for providing color escape codes - Removed usage of deprecated colorize() method within the examples --- cmd2/cmd2.py | 23 ++++++++++++++--------- examples/pirate.py | 20 +++++++++++++++++--- examples/python_scripting.py | 4 +++- tests/test_cmd2.py | 9 +++++---- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b0ff35d4..37033ba4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -561,7 +561,7 @@ class Cmd(cmd.Cmd): msg = utils.strip_ansi(msg) fileobj.write(msg) - def poutput(self, msg: Any, end: str='\n') -> None: + def poutput(self, msg: Any, end: str='\n', color: str='') -> None: """Smarter self.stdout.write(); color aware and adds newline of not present. Also handles BrokenPipeError exceptions for when a commands's output has @@ -569,14 +569,17 @@ class Cmd(cmd.Cmd): cmd2 command is finished executing. :param msg: message to print to current stdout (anything convertible to a str with '{}'.format() is OK) - :param end: string appended after the end of the message if not already present, default a newline + :param end: (optional) string appended after the end of the message if not already present, default a newline + :param color: (optional) color escape to output this message with """ if msg is not None and msg != '': try: msg_str = '{}'.format(msg) - self.decolorized_write(self.stdout, msg_str) if not msg_str.endswith(end): - self.decolorized_write(self.stdout, end) + msg_str += end + if color: + msg_str = color + msg_str + Fore.RESET + self.decolorized_write(self.stdout, msg_str) except BrokenPipeError: # This occurs if a command's output is being piped to another # process and that process closes before the command is @@ -586,12 +589,14 @@ class Cmd(cmd.Cmd): if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) - def perror(self, err: Union[str, Exception], traceback_war: bool=True) -> None: + def perror(self, err: Union[str, Exception], traceback_war: bool=True, err_color: str=Fore.LIGHTRED_EX, + war_color: str=Fore.LIGHTYELLOW_EX) -> None: """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists. :param err: an Exception or error message to print out :param traceback_war: (optional) if True, print a message to let user know they can enable debug - :return: + :param err_color: (optional) color escape to output error with + :param war_color: (optional) color escape to output warning with """ if self.debug: import traceback @@ -601,12 +606,12 @@ class Cmd(cmd.Cmd): err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err) else: err_msg = "ERROR: {}\n".format(err) - err_msg = Fore.RED + err_msg + Fore.RESET + err_msg = err_color + err_msg + Fore.RESET self.decolorized_write(sys.stderr, err_msg) if traceback_war: war = "To enable full traceback, run the following command: 'set debug true'\n" - war = Fore.YELLOW + war + Fore.RESET + war = war_color + war + Fore.RESET self.decolorized_write(sys.stderr, war) def pfeedback(self, msg: str) -> None: @@ -679,7 +684,7 @@ class Cmd(cmd.Cmd): except BrokenPipeError: # This occurs if a command's output is being piped to another process and that process closes before the # command is finished. If you would like your application to print a warning message, then set the - # broken_pipe_warning attribute to the message you want printed. + # broken_pipe_warning attribute to the message you want printed.` if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) diff --git a/examples/pirate.py b/examples/pirate.py index 34906a9f..22274dbf 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -8,8 +8,21 @@ It demonstrates many features of cmd2. """ import argparse +from colorama import Fore + import cmd2 +COLORS = { + 'black': Fore.BLACK, + 'red': Fore.RED, + 'green': Fore.GREEN, + 'yellow': Fore.YELLOW, + 'blue': Fore.BLUE, + 'magenta': Fore.MAGENTA, + 'cyan': Fore.CYAN, + 'white': Fore.WHITE, +} + class Pirate(cmd2.Cmd): """A piratical example cmd2 application involving looting and drinking.""" @@ -17,10 +30,10 @@ class Pirate(cmd2.Cmd): self.default_to_shell = True self.multiline_commands = ['sing'] self.terminators = self.terminators + ['...'] - self.songcolor = 'blue' + self.songcolor = Fore.BLUE # Add stuff to settable and/or shortcuts before calling base class initializer - self.settable['songcolor'] = 'Color to ``sing`` in (red/blue/green/cyan/magenta, bold, underline)' + self.settable['songcolor'] = 'Color to ``sing`` in (black/red/green/yellow/blue/magenta/cyan/white)' self.shortcuts.update({'~': 'sing'}) """Initialize the base class as well as this one""" @@ -68,7 +81,8 @@ class Pirate(cmd2.Cmd): def do_sing(self, arg): """Sing a colorful song.""" - self.poutput(self.colorize(arg, self.songcolor)) + color_escape = COLORS.get(self.songcolor, default=Fore.RESET) + self.poutput(arg, color=color_escape) yo_parser = argparse.ArgumentParser() yo_parser.add_argument('--ho', type=int, default=2, help="How often to chant 'ho'") diff --git a/examples/python_scripting.py b/examples/python_scripting.py index ab5ecc2b..b6700cb7 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -17,6 +17,8 @@ This application and the "scripts/conditional.py" script serve as an example for import argparse import os +from colorama import Fore + import cmd2 @@ -33,7 +35,7 @@ class CmdLineApp(cmd2.Cmd): def _set_prompt(self): """Set prompt so it displays the current working directory.""" self.cwd = os.getcwd() - self.prompt = self.colorize('{!r} $ '.format(self.cwd), 'cyan') + self.prompt = Fore.CYAN + '{!r} $ '.format(self.cwd) + Fore.RESET def postcmd(self, stop: bool, line: str) -> bool: """Hook method executed just after a command dispatch is finished. diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 8c8787af..9cb869bd 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1094,18 +1094,19 @@ def test_ansi_prompt_not_esacped(base_app): def test_ansi_prompt_escaped(): + from colorama import Fore app = cmd2.Cmd() - color = 'cyan' + color = Fore.CYAN prompt = 'InColor' - color_prompt = app.colorize(prompt, color) + color_prompt = color + prompt + Fore.RESET readline_hack_start = "\x01" readline_hack_end = "\x02" readline_safe_prompt = app._surround_ansi_escapes(color_prompt) assert prompt != color_prompt - assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end) - assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end) + assert readline_safe_prompt.startswith(readline_hack_start + color + readline_hack_end) + assert readline_safe_prompt.endswith(readline_hack_start + Fore.RESET + readline_hack_end) class HelpApp(cmd2.Cmd): -- cgit v1.2.1 From 4ed47bd9178ea98f3e268d75e929d64f156de0d3 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Mon, 24 Sep 2018 23:24:28 -0400 Subject: Added a couple unit tests --- tests/test_cmd2.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 693d5092..91fb8b39 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -13,7 +13,7 @@ import os import sys import tempfile -import colorama +from colorama import Fore, Back, Style import pytest # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available @@ -1099,7 +1099,6 @@ def test_ansi_prompt_not_esacped(base_app): def test_ansi_prompt_escaped(): - from colorama import Fore app = cmd2.Cmd() color = Fore.CYAN prompt = 'InColor' @@ -1737,6 +1736,24 @@ def test_poutput_none(base_app): expected = '' assert out == expected +def test_poutput_color_always(base_app): + msg = 'Hello World' + color = Fore.CYAN + base_app.colors = 'Always' + base_app.poutput(msg, color=color) + out = base_app.stdout.getvalue() + expected = color + msg + '\n' + Fore.RESET + assert out == expected + +def test_poutput_color_never(base_app): + msg = 'Hello World' + color = Fore.CYAN + base_app.colors = 'Never' + base_app.poutput(msg, color=color) + out = base_app.stdout.getvalue() + expected = msg + '\n' + assert out == expected + def test_alias(base_app, capsys): # Create the alias @@ -2037,8 +2054,8 @@ class ColorsApp(cmd2.Cmd): self.perror(args, False) def do_echo_error(self, args): - color_on = colorama.Fore.RED + colorama.Back.BLACK - color_off = colorama.Style.RESET_ALL + color_on = Fore.RED + Back.BLACK + color_off = Style.RESET_ALL self.poutput(color_on + args + color_off) # perror uses colors by default self.perror(args, False) -- cgit v1.2.1 From 9eb7ab1eb24cd54259f6cefbba07383cfb91f4e2 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 25 Sep 2018 23:32:30 -0400 Subject: Attempt at fixing unit test failure on Windows --- tests/test_cmd2.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 2c3a6253..dece1ab4 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1114,8 +1114,13 @@ def test_ansi_prompt_escaped(): readline_safe_prompt = rl_make_safe_prompt(color_prompt) assert prompt != color_prompt - assert readline_safe_prompt.startswith(readline_hack_start + color + readline_hack_end) - assert readline_safe_prompt.endswith(readline_hack_start + Fore.RESET + readline_hack_end) + if sys.platform.startswith('win'): + # PyReadline on Windows doesn't suffer from the GNU readline bug which requires the hack + assert readline_safe_prompt.startswith(color) + assert readline_safe_prompt.endswith(Fore.RESET) + else: + assert readline_safe_prompt.startswith(readline_hack_start + color + readline_hack_end) + assert readline_safe_prompt.endswith(readline_hack_start + Fore.RESET + readline_hack_end) class HelpApp(cmd2.Cmd): -- cgit v1.2.1 From caa8a7aa38eb2ab57a52bb9d5594e6d538d1cef5 Mon Sep 17 00:00:00 2001 From: Lance Starr <43422383+lancestarr@users.noreply.github.com> Date: Wed, 26 Sep 2018 09:26:26 -0400 Subject: Fix README.md link in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4de4fd4..5ba66b14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Remember to feel free to ask for help by leaving a comment within the Issue. Working on your first pull request? You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). -###### If you've found a bug that is not on the board, [follow these steps](Readme.md#found-a-bug). +###### If you've found a bug that is not on the board, [follow these steps](README.md#found-a-bug). -------------------------------------------------------------------------------- -- cgit v1.2.1