Skip to content

Commit 3037335

Browse files
authored
Merge pull request #516 from python-cmd2/colorize
Colorize
2 parents 982d2f2 + 354cee4 commit 3037335

File tree

16 files changed

+583
-83
lines changed

16 files changed

+583
-83
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@
1414
* These allow you to provide feedback to the user in an asychronous fashion, meaning alerts can
1515
display when the user is still entering text at the prompt. See [async_printing.py](https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py)
1616
for an example.
17+
* Cross-platform colored output support
18+
* ``colorama`` gets initialized properly in ``Cmd.__init()``
19+
* The ``Cmd.colors`` setting is no longer platform dependent and now has three values:
20+
* Terminal (default) - output methods do not strip any ANSI escape sequences when output is a terminal, but
21+
if the output is a pipe or a file the escape sequences are stripped
22+
* Always - output methods **never** strip ANSI escape sequences, regardless of the output destination
23+
* Never - output methods strip all ANSI escape sequences
24+
* Deprecations
25+
* Deprecated the builtin ``cmd2`` suport for colors including ``Cmd.colorize()`` and ``Cmd._colorcodes``
1726
* Deletions
1827
* The ``preparse``, ``postparsing_precmd``, and ``postparsing_postcmd`` methods *deprecated* in the previous release
1928
have been deleted

cmd2/cmd2.py

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@
3232
import argparse
3333
import cmd
3434
import collections
35+
import colorama
3536
from colorama import Fore
3637
import glob
3738
import inspect
3839
import os
39-
import platform
4040
import re
4141
import shlex
4242
import sys
4343
import threading
44-
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union
44+
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO
4545

4646
from . import constants
4747
from . import utils
@@ -318,7 +318,7 @@ class Cmd(cmd.Cmd):
318318
reserved_words = []
319319

320320
# Attributes which ARE dynamically settable at runtime
321-
colors = (platform.system() != 'Windows')
321+
colors = constants.COLORS_TERMINAL
322322
continuation_prompt = '> '
323323
debug = False
324324
echo = False
@@ -338,7 +338,7 @@ class Cmd(cmd.Cmd):
338338

339339
# To make an attribute settable with the "do_set" command, add it to this ...
340340
# This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key
341-
settable = {'colors': 'Colorized output (*nix only)',
341+
settable = {'colors': 'Allow colorized output (valid values: Terminal, Always, Never)',
342342
'continuation_prompt': 'On 2nd+ line of input',
343343
'debug': 'Show full error stack on error',
344344
'echo': 'Echo command issued into output',
@@ -370,6 +370,9 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
370370
except AttributeError:
371371
pass
372372

373+
# Override whether ansi codes should be stripped from the output since cmd2 has its own logic for doing this
374+
colorama.init(strip=False)
375+
373376
# initialize plugin system
374377
# needs to be done before we call __init__(0)
375378
self._initialize_plugin_system()
@@ -418,13 +421,13 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
418421
self._STOP_AND_EXIT = True # cmd convention
419422

420423
self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'},
421-
'cyan': {True: '\x1b[36m', False: '\x1b[39m'},
422-
'blue': {True: '\x1b[34m', False: '\x1b[39m'},
423-
'red': {True: '\x1b[31m', False: '\x1b[39m'},
424-
'magenta': {True: '\x1b[35m', False: '\x1b[39m'},
425-
'green': {True: '\x1b[32m', False: '\x1b[39m'},
426-
'underline': {True: '\x1b[4m', False: '\x1b[24m'},
427-
'yellow': {True: '\x1b[33m', False: '\x1b[39m'}}
424+
'cyan': {True: Fore.CYAN, False: Fore.RESET},
425+
'blue': {True: Fore.BLUE, False: Fore.RESET},
426+
'red': {True: Fore.RED, False: Fore.RESET},
427+
'magenta': {True: Fore.MAGENTA, False: Fore.RESET},
428+
'green': {True: Fore.GREEN, False: Fore.RESET},
429+
'underline': {True: '\x1b[4m', False: Fore.RESET},
430+
'yellow': {True: Fore.YELLOW, False: Fore.RESET}}
428431

429432
# Used load command to store the current script dir as a LIFO queue to support _relative_load command
430433
self._script_dir = []
@@ -554,49 +557,69 @@ def _finalize_app_parameters(self) -> None:
554557
# Make sure settable parameters are sorted alphabetically by key
555558
self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0]))
556559

557-
def poutput(self, msg: str, end: str='\n') -> None:
558-
"""Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present.
560+
def decolorized_write(self, fileobj: IO, msg: str) -> None:
561+
"""Write a string to a fileobject, stripping ANSI escape sequences if necessary
562+
563+
Honor the current colors setting, which requires us to check whether the
564+
fileobject is a tty.
565+
"""
566+
if self.colors.lower() == constants.COLORS_NEVER.lower() or \
567+
(self.colors.lower() == constants.COLORS_TERMINAL.lower() and not fileobj.isatty()):
568+
msg = utils.strip_ansi(msg)
569+
fileobj.write(msg)
559570

560-
Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and
561-
that process terminates before the cmd2 command is finished executing.
571+
def poutput(self, msg: Any, end: str='\n', color: str='') -> None:
572+
"""Smarter self.stdout.write(); color aware and adds newline of not present.
573+
574+
Also handles BrokenPipeError exceptions for when a commands's output has
575+
been piped to another process and that process terminates before the
576+
cmd2 command is finished executing.
562577
563578
:param msg: message to print to current stdout (anything convertible to a str with '{}'.format() is OK)
564-
:param end: string appended after the end of the message if not already present, default a newline
579+
:param end: (optional) string appended after the end of the message if not already present, default a newline
580+
:param color: (optional) color escape to output this message with
565581
"""
566582
if msg is not None and msg != '':
567583
try:
568584
msg_str = '{}'.format(msg)
569-
self.stdout.write(msg_str)
570585
if not msg_str.endswith(end):
571-
self.stdout.write(end)
586+
msg_str += end
587+
if color:
588+
msg_str = color + msg_str + Fore.RESET
589+
self.decolorized_write(self.stdout, msg_str)
572590
except BrokenPipeError:
573-
# This occurs if a command's output is being piped to another process and that process closes before the
574-
# command is finished. If you would like your application to print a warning message, then set the
575-
# broken_pipe_warning attribute to the message you want printed.
591+
# This occurs if a command's output is being piped to another
592+
# process and that process closes before the command is
593+
# finished. If you would like your application to print a
594+
# warning message, then set the broken_pipe_warning attribute
595+
# to the message you want printed.
576596
if self.broken_pipe_warning:
577597
sys.stderr.write(self.broken_pipe_warning)
578598

579-
def perror(self, err: Union[str, Exception], traceback_war: bool=True) -> None:
599+
def perror(self, err: Union[str, Exception], traceback_war: bool=True, err_color: str=Fore.LIGHTRED_EX,
600+
war_color: str=Fore.LIGHTYELLOW_EX) -> None:
580601
""" Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists.
581602
582603
:param err: an Exception or error message to print out
583604
:param traceback_war: (optional) if True, print a message to let user know they can enable debug
584-
:return:
605+
:param err_color: (optional) color escape to output error with
606+
:param war_color: (optional) color escape to output warning with
585607
"""
586608
if self.debug:
587609
import traceback
588610
traceback.print_exc()
589611

590612
if isinstance(err, Exception):
591613
err_msg = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(type(err).__name__, err)
592-
sys.stderr.write(self.colorize(err_msg, 'red'))
593614
else:
594-
err_msg = self.colorize("ERROR: {}\n".format(err), 'red')
595-
sys.stderr.write(err_msg)
615+
err_msg = "ERROR: {}\n".format(err)
616+
err_msg = err_color + err_msg + Fore.RESET
617+
self.decolorized_write(sys.stderr, err_msg)
596618

597619
if traceback_war:
598620
war = "To enable full traceback, run the following command: 'set debug true'\n"
599-
sys.stderr.write(self.colorize(war, 'yellow'))
621+
war = war_color + war + Fore.RESET
622+
self.decolorized_write(sys.stderr, war)
600623

601624
def pfeedback(self, msg: str) -> None:
602625
"""For printing nonessential feedback. Can be silenced with `quiet`.
@@ -605,7 +628,7 @@ def pfeedback(self, msg: str) -> None:
605628
if self.feedback_to_output:
606629
self.poutput(msg)
607630
else:
608-
sys.stderr.write("{}\n".format(msg))
631+
self.decolorized_write(sys.stderr, "{}\n".format(msg))
609632

610633
def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
611634
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
@@ -641,6 +664,9 @@ def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
641664
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
642665
# Also only attempt to use a pager if actually running in a real fully functional terminal
643666
if functional_terminal and not self.redirecting and not self._in_py and not self._script_dir:
667+
if self.colors.lower() == constants.COLORS_NEVER.lower():
668+
msg_str = utils.strip_ansi(msg_str)
669+
644670
pager = self.pager
645671
if chop:
646672
pager = self.pager_chop
@@ -665,7 +691,7 @@ def ppaged(self, msg: str, end: str='\n', chop: bool=False) -> None:
665691
except BrokenPipeError:
666692
# This occurs if a command's output is being piped to another process and that process closes before the
667693
# command is finished. If you would like your application to print a warning message, then set the
668-
# broken_pipe_warning attribute to the message you want printed.
694+
# broken_pipe_warning attribute to the message you want printed.`
669695
if self.broken_pipe_warning:
670696
sys.stderr.write(self.broken_pipe_warning)
671697

@@ -676,7 +702,7 @@ def colorize(self, val: str, color: str) -> str:
676702
is running on Windows, will return ``val`` unchanged.
677703
``color`` should be one of the supported strings (or styles):
678704
red/blue/green/cyan/magenta, bold, underline"""
679-
if self.colors and (self.stdout == self.initial_stdout):
705+
if self.colors.lower() != constants.COLORS_NEVER.lower() and (self.stdout == self.initial_stdout):
680706
return self._colorcodes[color][True] + val + self._colorcodes[color][False]
681707
return val
682708

cmd2/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@
1717
ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
1818

1919
LINE_FEED = '\n'
20+
21+
# values for colors setting
22+
COLORS_NEVER = 'Never'
23+
COLORS_TERMINAL = 'Terminal'
24+
COLORS_ALWAYS = 'Always'

docs/settingchanges.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ comments, is viewable from within a running application
137137
with::
138138

139139
(Cmd) set --long
140-
colors: True # Colorized output (*nix only)
140+
colors: Terminal # Allow colorized output
141141
continuation_prompt: > # On 2nd+ line of input
142142
debug: False # Show full error stack on error
143143
echo: False # Echo command issued into output
@@ -150,5 +150,5 @@ with::
150150

151151
Any of these user-settable parameters can be set while running your app with the ``set`` command like so::
152152

153-
set colors False
153+
set colors Never
154154

docs/unfreefeatures.rst

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,43 @@ instead. These methods have these advantages:
139139
.. automethod:: cmd2.cmd2.Cmd.ppaged
140140

141141

142-
color
143-
=====
142+
Colored Output
143+
==============
144144

145-
Text output can be colored by wrapping it in the ``colorize`` method.
145+
The output methods in the previous section all honor the ``colors`` setting,
146+
which has three possible values:
147+
148+
Never
149+
poutput() and pfeedback() strip all ANSI escape sequences
150+
which instruct the terminal to colorize output
151+
152+
Terminal
153+
(the default value) poutput() and pfeedback() do not strip any ANSI escape
154+
sequences when the output is a terminal, but if the output is a pipe or a
155+
file the escape sequences are stripped. If you want colorized output you
156+
must add ANSI escape sequences, preferably using some python color library
157+
like `plumbum.colors`, `colorama`, `blessings`, or `termcolor`.
158+
159+
Always
160+
poutput() and pfeedback() never strip ANSI escape sequences, regardless of
161+
the output destination
162+
163+
164+
The previously recommended ``colorize`` method is now deprecated.
146165

147-
.. automethod:: cmd2.cmd2.Cmd.colorize
148166

149167
.. _quiet:
150168

169+
Suppressing non-essential output
170+
================================
151171

152-
quiet
153-
=====
172+
The ``quiet`` setting controls whether ``self.pfeedback()`` actually produces
173+
any output. If ``quiet`` is ``False``, then the output will be produced. If
174+
``quiet`` is ``True``, no output will be produced.
154175

155-
Controls whether ``self.pfeedback('message')`` output is suppressed;
156-
useful for non-essential feedback that the user may not always want
157-
to read. ``quiet`` is only relevant if
158-
``app.pfeedback`` is sometimes used.
176+
This makes ``self.pfeedback()`` useful for non-essential output like status
177+
messages. Users can control whether they would like to see these messages by changing
178+
the value of the ``quiet`` setting.
159179

160180

161181
select

0 commit comments

Comments
 (0)