prefix_chars='-', fromfile_prefix_chars=None, \
argument_default=None, conflict_handler='error', \
add_help=True, allow_abbrev=True, exit_on_error=True, \
- suggest_on_error=False)
+ suggest_on_error=False, color=False)
Create a new :class:`ArgumentParser` object. All parameters should be passed
as keyword arguments. Each parameter has its own more detailed description
* add_help_ - Add a ``-h/--help`` option to the parser (default: ``True``)
* allow_abbrev_ - Allows long options to be abbreviated if the
- abbreviation is unambiguous. (default: ``True``)
+ abbreviation is unambiguous (default: ``True``)
* exit_on_error_ - Determines whether or not :class:`!ArgumentParser` exits with
error info when an error occurs. (default: ``True``)
* suggest_on_error_ - Enables suggestions for mistyped argument choices
and subparser names (default: ``False``)
+ * color_ - Allow color output (default: ``False``)
.. versionchanged:: 3.5
*allow_abbrev* parameter was added.
.. versionchanged:: 3.9
*exit_on_error* parameter was added.
+ .. versionchanged:: 3.14
+ *suggest_on_error* and *color* parameters were added.
+
The following sections describe how each of these are used.
``True``. Note that this only applies for arguments when the choices specified
are strings::
- >>> parser = argparse.ArgumentParser(description='Process some integers.', suggest_on_error=True)
+ >>> parser = argparse.ArgumentParser(description='Process some integers.',
+ suggest_on_error=True)
>>> parser.add_argument('--action', choices=['sum', 'max'])
>>> parser.add_argument('integers', metavar='N', type=int, nargs='+',
... help='an integer for the accumulator')
.. versionadded:: 3.14
+color
+^^^^^
+
+By default, the help message is printed in plain text. If you want to allow
+color in help messages, you can enable it by setting ``color`` to ``True``::
+
+ >>> parser = argparse.ArgumentParser(description='Process some integers.',
+ ... color=True)
+ >>> parser.add_argument('--action', choices=['sum', 'max'])
+ >>> parser.add_argument('integers', metavar='N', type=int, nargs='+',
+ ... help='an integer for the accumulator')
+ >>> parser.parse_args(['--help'])
+
+Even if a CLI author has enabled color, it can be
+:ref:`controlled using environment variables <using-on-controlling-color>`.
+
+If you're writing code that needs to be compatible with older Python versions
+and want to opportunistically use ``color`` when it's available, you
+can set it as an attribute after initializing the parser instead of using the
+keyword argument::
+
+ >>> parser = argparse.ArgumentParser(description='Process some integers.')
+ >>> parser.color = True
+
+.. versionadded:: next
+
+
The add_argument() method
-------------------------
provided by the class are considered an implementation detail.
"""
- def __init__(self,
- prog,
- indent_increment=2,
- max_help_position=24,
- width=None):
-
+ def __init__(
+ self,
+ prog,
+ indent_increment=2,
+ max_help_position=24,
+ width=None,
+ prefix_chars='-',
+ color=False,
+ ):
# default setting for width
if width is None:
import shutil
width = shutil.get_terminal_size().columns
width -= 2
+ from _colorize import ANSIColors, NoColors, can_colorize, decolor
+
+ if color and can_colorize():
+ self._ansi = ANSIColors()
+ self._decolor = decolor
+ else:
+ self._ansi = NoColors
+ self._decolor = lambda text: text
+
+ self._prefix_chars = prefix_chars
self._prog = prog
self._indent_increment = indent_increment
self._max_help_position = min(max_help_position,
# add the heading if the section was non-empty
if self.heading is not SUPPRESS and self.heading is not None:
+ bold_blue = self.formatter._ansi.BOLD_BLUE
+ reset = self.formatter._ansi.RESET
+
current_indent = self.formatter._current_indent
heading_text = _('%(heading)s:') % dict(heading=self.heading)
- heading = '%*s%s\n' % (current_indent, '', heading_text)
+ heading = (
+ f'{" " * current_indent}'
+ f'{bold_blue}{heading_text}{reset}\n'
+ )
else:
heading = ''
if part and part is not SUPPRESS])
def _format_usage(self, usage, actions, groups, prefix):
+ bold_blue = self._ansi.BOLD_BLUE
+ bold_magenta = self._ansi.BOLD_MAGENTA
+ magenta = self._ansi.MAGENTA
+ reset = self._ansi.RESET
+
if prefix is None:
prefix = _('usage: ')
# if usage is specified, use that
if usage is not None:
- usage = usage % dict(prog=self._prog)
+ usage = (
+ magenta
+ + usage
+ % {"prog": f"{bold_magenta}{self._prog}{reset}{magenta}"}
+ + reset
+ )
# if no optionals or positionals are available, usage is just prog
elif usage is None and not actions:
- usage = '%(prog)s' % dict(prog=self._prog)
+ usage = f"{bold_magenta}{self._prog}{reset}"
# if optionals and positionals are available, calculate usage
elif usage is None:
# wrap the usage parts if it's too long
text_width = self._width - self._current_indent
- if len(prefix) + len(usage) > text_width:
+ if len(prefix) + len(self._decolor(usage)) > text_width:
# break usage into wrappable parts
opt_parts = self._get_actions_usage_parts(optionals, groups)
else:
line_len = indent_length - 1
for part in parts:
- if line_len + 1 + len(part) > text_width and line:
+ part_len = len(self._decolor(part))
+ if line_len + 1 + part_len > text_width and line:
lines.append(indent + ' '.join(line))
line = []
line_len = indent_length - 1
line.append(part)
- line_len += len(part) + 1
+ line_len += part_len + 1
if line:
lines.append(indent + ' '.join(line))
if prefix is not None:
return lines
# if prog is short, follow it with optionals or positionals
- if len(prefix) + len(prog) <= 0.75 * text_width:
- indent = ' ' * (len(prefix) + len(prog) + 1)
+ prog_len = len(self._decolor(prog))
+ if len(prefix) + prog_len <= 0.75 * text_width:
+ indent = ' ' * (len(prefix) + prog_len + 1)
if opt_parts:
lines = get_lines([prog] + opt_parts, indent, prefix)
lines.extend(get_lines(pos_parts, indent))
# join lines into usage
usage = '\n'.join(lines)
+ usage = usage.removeprefix(prog)
+ usage = f"{bold_magenta}{prog}{reset}{usage}"
+
# prefix with 'usage:'
- return '%s%s\n\n' % (prefix, usage)
+ return f'{bold_blue}{prefix}{reset}{usage}\n\n'
def _format_actions_usage(self, actions, groups):
return ' '.join(self._get_actions_usage_parts(actions, groups))
+ def _is_long_option(self, string):
+ return len(string) >= 2 and string[1] in self._prefix_chars
+
+ def _is_short_option(self, string):
+ return (
+ not self._is_long_option(string)
+ and len(string) >= 1
+ and string[0] in self._prefix_chars
+ )
+
def _get_actions_usage_parts(self, actions, groups):
# find group indices and identify actions in groups
group_actions = set()
# collect all actions format strings
parts = []
+ cyan = self._ansi.CYAN
+ green = self._ansi.GREEN
+ yellow = self._ansi.YELLOW
+ reset = self._ansi.RESET
for action in actions:
# suppressed arguments are marked with None
# produce all arg strings
elif not action.option_strings:
default = self._get_default_metavar_for_positional(action)
- part = self._format_args(action, default)
+ part = green + self._format_args(action, default) + reset
# if it's in a group, strip the outer []
if action in group_actions:
# -s or --long
if action.nargs == 0:
part = action.format_usage()
+ if self._is_long_option(part):
+ part = f"{cyan}{part}{reset}"
+ elif self._is_short_option(part):
+ part = f"{green}{part}{reset}"
# if the Optional takes a value, format is:
# -s ARGS or --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
- part = '%s %s' % (option_string, args_string)
+ if self._is_long_option(option_string):
+ option_string = f"{cyan}{option_string}"
+ elif self._is_short_option(option_string):
+ option_string = f"{green}{option_string}"
+ part = f"{option_string} {yellow}{args_string}{reset}"
# make it look optional if it's not required or in a group
if not action.required and action not in group_actions:
help_width = max(self._width - help_position, 11)
action_width = help_position - self._current_indent - 2
action_header = self._format_action_invocation(action)
+ action_header_no_color = self._decolor(action_header)
# no help; start on same line and add a final newline
if not action.help:
action_header = '%*s%s\n' % tup
# short action name; start on the same line and pad two spaces
- elif len(action_header) <= action_width:
- tup = self._current_indent, '', action_width, action_header
+ elif len(action_header_no_color) <= action_width:
+ # calculate widths without color codes
+ action_header_color = action_header
+ tup = self._current_indent, '', action_width, action_header_no_color
action_header = '%*s%-*s ' % tup
+ # swap in the colored header
+ action_header = action_header.replace(
+ action_header_no_color, action_header_color
+ )
indent_first = 0
# long action name; start on the next line
return self._join_parts(parts)
def _format_action_invocation(self, action):
+ bold_green = self._ansi.BOLD_GREEN
+ bold_cyan = self._ansi.BOLD_CYAN
+ bold_yellow = self._ansi.BOLD_YELLOW
+ reset = self._ansi.RESET
+
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
- return ' '.join(self._metavar_formatter(action, default)(1))
+ return (
+ bold_green
+ + ' '.join(self._metavar_formatter(action, default)(1))
+ + reset
+ )
else:
+ def color_option_strings(strings):
+ parts = []
+ for s in strings:
+ if self._is_long_option(s):
+ parts.append(f"{bold_cyan}{s}{reset}")
+ elif self._is_short_option(s):
+ parts.append(f"{bold_green}{s}{reset}")
+ else:
+ parts.append(s)
+ return parts
+
# if the Optional doesn't take a value, format is:
# -s, --long
if action.nargs == 0:
- return ', '.join(action.option_strings)
+ option_strings = color_option_strings(action.option_strings)
+ return ', '.join(option_strings)
# if the Optional takes a value, format is:
# -s, --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
- args_string = self._format_args(action, default)
- return ', '.join(action.option_strings) + ' ' + args_string
+ option_strings = color_option_strings(action.option_strings)
+ args_string = (
+ f"{bold_yellow}{self._format_args(action, default)}{reset}"
+ )
+ return ', '.join(option_strings) + ' ' + args_string
def _metavar_formatter(self, action, default_metavar):
if action.metavar is not None:
self._name_parser_map = {}
self._choices_actions = []
self._deprecated = set()
+ self._color = False
super(_SubParsersAction, self).__init__(
option_strings=option_strings,
if kwargs.get('prog') is None:
kwargs['prog'] = '%s %s' % (self._prog_prefix, name)
+ # set color
+ if kwargs.get('color') is None:
+ kwargs['color'] = self._color
+
aliases = kwargs.pop('aliases', ())
if name in self._name_parser_map:
- exit_on_error -- Determines whether or not ArgumentParser exits with
error info when an error occurs
- suggest_on_error - Enables suggestions for mistyped argument choices
- and subparser names. (default: ``False``)
+ and subparser names (default: ``False``)
+ - color - Allow color output in help messages (default: ``False``)
"""
def __init__(self,
add_help=True,
allow_abbrev=True,
exit_on_error=True,
- suggest_on_error=False):
-
+ suggest_on_error=False,
+ *,
+ color=False,
+ ):
superinit = super(ArgumentParser, self).__init__
superinit(description=description,
prefix_chars=prefix_chars,
self.allow_abbrev = allow_abbrev
self.exit_on_error = exit_on_error
self.suggest_on_error = suggest_on_error
+ self.color = color
add_group = self.add_argument_group
self._positionals = add_group(_('positional arguments'))
# create the parsers action and add it to the positionals list
parsers_class = self._pop_action_class(kwargs, 'parsers')
action = parsers_class(option_strings=[], **kwargs)
+ action._color = self.color
self._check_help(action)
self._subparsers._add_action(action)
return formatter.format_help()
def _get_formatter(self):
- return self.formatter_class(prog=self.prog)
+ if isinstance(self.formatter_class, type) and issubclass(
+ self.formatter_class, HelpFormatter
+ ):
+ return self.formatter_class(
+ prog=self.prog,
+ prefix_chars=self.prefix_chars,
+ color=self.color,
+ )
+ else:
+ return self.formatter_class(prog=self.prog)
# =====================
# Help-printing methods
# Author: Steven J. Bethard <steven.bethard@gmail.com>.
+import _colorize
import contextlib
import functools
import inspect
self.assertMsgidsEqual(argparse)
+# ===========
+# Color tests
+# ===========
+
+
+class TestColorized(TestCase):
+
+ def setUp(self):
+ super().setUp()
+ # Ensure color even if ran with NO_COLOR=1
+ _colorize.can_colorize = lambda *args, **kwargs: True
+ self.ansi = _colorize.ANSIColors()
+
+ def test_argparse_color(self):
+ # Arrange: create a parser with a bit of everything
+ parser = argparse.ArgumentParser(
+ color=True,
+ description="Colorful help",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ prefix_chars="-+",
+ prog="PROG",
+ )
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ "-v", "--verbose", action="store_true", help="more spam"
+ )
+ group.add_argument(
+ "-q", "--quiet", action="store_true", help="less spam"
+ )
+ parser.add_argument("x", type=int, help="the base")
+ parser.add_argument(
+ "y", type=int, help="the exponent", deprecated=True
+ )
+ parser.add_argument(
+ "this_indeed_is_a_very_long_action_name",
+ type=int,
+ help="the exponent",
+ )
+ parser.add_argument(
+ "-o", "--optional1", action="store_true", deprecated=True
+ )
+ parser.add_argument("--optional2", help="pick one")
+ parser.add_argument("--optional3", choices=("X", "Y", "Z"))
+ parser.add_argument(
+ "--optional4", choices=("X", "Y", "Z"), help="pick one"
+ )
+ parser.add_argument(
+ "--optional5", choices=("X", "Y", "Z"), help="pick one"
+ )
+ parser.add_argument(
+ "--optional6", choices=("X", "Y", "Z"), help="pick one"
+ )
+ parser.add_argument(
+ "-p",
+ "--optional7",
+ choices=("Aaaaa", "Bbbbb", "Ccccc", "Ddddd"),
+ help="pick one",
+ )
+
+ parser.add_argument("+f")
+ parser.add_argument("++bar")
+ parser.add_argument("-+baz")
+ parser.add_argument("-c", "--count")
+
+ subparsers = parser.add_subparsers(
+ title="subcommands",
+ description="valid subcommands",
+ help="additional help",
+ )
+ subparsers.add_parser("sub1", deprecated=True, help="sub1 help")
+ sub2 = subparsers.add_parser("sub2", deprecated=True, help="sub2 help")
+ sub2.add_argument("--baz", choices=("X", "Y", "Z"), help="baz help")
+
+ heading = self.ansi.BOLD_BLUE
+ label, label_b = self.ansi.YELLOW, self.ansi.BOLD_YELLOW
+ long, long_b = self.ansi.CYAN, self.ansi.BOLD_CYAN
+ pos, pos_b = short, short_b = self.ansi.GREEN, self.ansi.BOLD_GREEN
+ sub = self.ansi.BOLD_GREEN
+ prog = self.ansi.BOLD_MAGENTA
+ reset = self.ansi.RESET
+
+ # Act
+ help_text = parser.format_help()
+
+ # Assert
+ self.assertEqual(
+ help_text,
+ textwrap.dedent(
+ f"""\
+ {heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}-v{reset} | {short}-q{reset}] [{short}-o{reset}] [{long}--optional2 {label}OPTIONAL2{reset}] [{long}--optional3 {label}{{X,Y,Z}}{reset}]
+ [{long}--optional4 {label}{{X,Y,Z}}{reset}] [{long}--optional5 {label}{{X,Y,Z}}{reset}] [{long}--optional6 {label}{{X,Y,Z}}{reset}]
+ [{short}-p {label}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}] [{short}+f {label}F{reset}] [{long}++bar {label}BAR{reset}] [{long}-+baz {label}BAZ{reset}]
+ [{short}-c {label}COUNT{reset}]
+ {pos}x{reset} {pos}y{reset} {pos}this_indeed_is_a_very_long_action_name{reset} {pos}{{sub1,sub2}} ...{reset}
+
+ Colorful help
+
+ {heading}positional arguments:{reset}
+ {pos_b}x{reset} the base
+ {pos_b}y{reset} the exponent
+ {pos_b}this_indeed_is_a_very_long_action_name{reset}
+ the exponent
+
+ {heading}options:{reset}
+ {short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
+ {short_b}-v{reset}, {long_b}--verbose{reset} more spam (default: False)
+ {short_b}-q{reset}, {long_b}--quiet{reset} less spam (default: False)
+ {short_b}-o{reset}, {long_b}--optional1{reset}
+ {long_b}--optional2{reset} {label_b}OPTIONAL2{reset}
+ pick one (default: None)
+ {long_b}--optional3{reset} {label_b}{{X,Y,Z}}{reset}
+ {long_b}--optional4{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
+ {long_b}--optional5{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
+ {long_b}--optional6{reset} {label_b}{{X,Y,Z}}{reset} pick one (default: None)
+ {short_b}-p{reset}, {long_b}--optional7{reset} {label_b}{{Aaaaa,Bbbbb,Ccccc,Ddddd}}{reset}
+ pick one (default: None)
+ {short_b}+f{reset} {label_b}F{reset}
+ {long_b}++bar{reset} {label_b}BAR{reset}
+ {long_b}-+baz{reset} {label_b}BAZ{reset}
+ {short_b}-c{reset}, {long_b}--count{reset} {label_b}COUNT{reset}
+
+ {heading}subcommands:{reset}
+ valid subcommands
+
+ {sub}{{sub1,sub2}}{reset} additional help
+ {sub}sub1{reset} sub1 help
+ {sub}sub2{reset} sub2 help
+ """
+ ),
+ )
+
+ def test_argparse_color_usage(self):
+ # Arrange
+ parser = argparse.ArgumentParser(
+ add_help=False,
+ color=True,
+ description="Test prog and usage colors",
+ prog="PROG",
+ usage="[prefix] %(prog)s [suffix]",
+ )
+ heading = self.ansi.BOLD_BLUE
+ prog = self.ansi.BOLD_MAGENTA
+ reset = self.ansi.RESET
+ usage = self.ansi.MAGENTA
+
+ # Act
+ help_text = parser.format_help()
+
+ # Assert
+ self.assertEqual(
+ help_text,
+ textwrap.dedent(
+ f"""\
+ {heading}usage: {reset}{usage}[prefix] {prog}PROG{reset}{usage} [suffix]{reset}
+
+ Test prog and usage colors
+ """
+ ),
+ )
+
+
def tearDownModule():
# Remove global references to avoid looking like we have refleaks.
RFile.seen = {}