8000 gh-133653: Fix argparse.ArgumentParser with the formatter_class argument by serhiy-storchaka · Pull Request #133813 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-133653: Fix argparse.ArgumentParser with the formatter_class argument #133813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 21 additions & 40 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ def __init__(
indent_increment=2,
max_help_position=24,
width=None,
prefix_chars='-',
color=False,
):
# default setting for width
Expand All @@ -176,16 +175,7 @@ def __init__(
width = shutil.get_terminal_size().columns
width -= 2

from _colorize import can_colorize, decolor, get_theme

if color and can_colorize():
self._theme = get_theme(force_color=True).argparse
self._decolor = decolor
else:
self._theme = get_theme(force_no_color=True).argparse
self._decolor = lambda text: text

self._prefix_chars = prefix_chars
self._set_color(color)
self._prog = prog
self._indent_increment = indent_increment
self._max_help_position = min(max_help_position,
Expand All @@ -202,6 +192,16 @@ def __init__(
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
self._long_break_matcher = _re.compile(r'\n\n\n+')

def _set_color(self, color):
from _colorize import can_colorize, decolor, get_theme

if color and can_colorize():
self._theme = get_theme(force_color=True).argparse
self._decolor = decolor
else:
self._theme = get_theme(force_no_color=True).argparse
self._decolor = lambda text: text

# ===============================
# Section and indentation methods
# ===============================
Expand Down Expand Up @@ -415,14 +415,7 @@ 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
)
return len(string) > 2

def _get_actions_usage_parts(self, actions, groups):
# find group indices and identify actions in groups
Expand Down Expand Up @@ -471,25 +464,22 @@ def _get_actions_usage_parts(self, actions, groups):
# produce the first way to invoke the option in brackets
else:
option_string = action.option_strings[0]
if self._is_long_option(option_string):
option_color = t.summary_long_option
else:
option_color = t.summary_short_option

# if the Optional doesn't take a value, format is:
# -s or --long
if action.nargs == 0:
part = action.format_usage()
if self._is_long_option(part):
part = f"{t.summary_long_option}{part}{t.reset}"
elif self._is_short_option(part):
part = f"{t.summary_short_option}{part}{t.reset}"
part = f"{option_color}{part}{t.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)
if self._is_long_option(option_string):
option_color = t.summary_long_option
elif self._is_short_option(option_string):
option_color = t.summary_short_option
part = (
f"{option_color}{option_string} "
f"{t.summary_label}{args_string}{t.reset}"
Expand Down Expand Up @@ -606,10 +596,8 @@ def color_option_strings(strings):
for s in strings:
if self._is_long_option(s):
parts.append(f"{t.long_option}{s}{t.reset}")
elif self._is_short_option(s):
parts.append(f"{t.short_option}{s}{t.reset}")
else:
parts.append(s)
parts.append(f"{t.short_option}{s}{t.reset}")
return parts

# if the Optional doesn't take a value, format is:
Expand Down Expand Up @@ -2723,16 +2711,9 @@ def format_help(self):
return formatter.format_help()

def _get_formatter(self):
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)
formatter = self.formatter_class(prog=self.prog)
formatter._set_color(self.color)
return formatter

# =====================
# Help-printing methods
Expand Down
129 changes: 126 additions & 3 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5469,11 +5469,60 @@ def custom_type(string):
version = ''


class TestHelpUsageLongSubparserCommand(TestCase):
"""Test that subparser commands are formatted correctly in help"""
class TestHelpCustomHelpFormatter(TestCase):
maxDiff = None

def test_parent_help(self):
def test_custom_formatter_function(self):
def custom_formatter(prog):
return argparse.RawTextHelpFormatter(prog, indent_increment=5)

parser = argparse.ArgumentParser(
prog='PROG',
prefix_chars='-+',
formatter_class=custom_formatter
)
parser.add_argument('+f', '++foo', help="foo help")
parser.add_argument('spam', help="spam help")

parser_help = parser.format_help()
self.assertEqual(parser_help, textwrap.dedent('''\
usage: PROG [-h] [+f FOO] spam

positional arguments:
spam spam help

options:
-h, --help show this help message and exit
+f, ++foo FOO foo help
'''))

def test_custom_formatter_class(self):
class CustomFormatter(argparse.RawTextHelpFormatter):
def __init__(self, prog):
super().__init__(prog, indent_increment=5)

parser = argparse.ArgumentParser(
prog='PROG',
prefix_chars='-+',
formatter_class=CustomFormatter
)
parser.add_argument('+f', '++foo', help="foo help")
parser.add_argument('spam', help="spam help")

parser_help = parser.format_help()
self.assertEqual(parser_help, textwrap.dedent('''\
usage: PROG [-h] [+f FOO] spam

positional arguments:
spam spam help

options:
-h, --help show this help message and exit
+f, ++foo FOO foo help
'''))

def test_usage_long_subparser_command(self):
"""Test that subparser commands are formatted correctly in help"""
def custom_formatter(prog):
return argparse.RawTextHelpFormatter(prog, max_help_position=50)

Expand Down Expand Up @@ -7053,6 +7102,7 @@ def test_translations(self):


class TestColorized(TestCase):
maxDiff = None

def setUp(self):
super().setUp()
Expand Down Expand Up @@ -7211,6 +7261,79 @@ def test_argparse_color_usage(self):
),
)

def test_custom_formatter_function(self):
def custom_formatter(prog):
return argparse.RawTextHelpFormatter(prog, indent_increment=5)

parser = argparse.ArgumentParser(
prog="PROG",
prefix_chars="-+",
formatter_class=custom_formatter,
color=True,
)
parser.add_argument('+f', '++foo', help="foo help")
parser.add_argument('spam', help="spam help")

prog = self.theme.prog
heading = self.theme.heading
short = self.theme.summary_short_option
label = self.theme.summary_label
pos = self.theme.summary_action
long_b = self.theme.long_option
short_b = self.theme.short_option
label_b = self.theme.label
pos_b = self.theme.action
reset = self.theme.reset

parser_help = parser.format_help()
self.assertEqual(parser_help, textwrap.dedent(f'''\
{heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}+f {label}FOO{reset}] {pos}spam{reset}

{heading}positional arguments:{reset}
{pos_b}spam{reset} spam help

{heading}options:{reset}
{short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
{short_b}+f{reset}, {long_b}++foo{reset} {label_b}FOO{reset} foo help
'''))

def test_custom_formatter_class(self):
class CustomFormat 6D47 ter(argparse.RawTextHelpFormatter):
def __init__(self, prog):
super().__init__(prog, indent_increment=5)

parser = argparse.ArgumentParser(
prog="PROG",
prefix_chars="-+",
formatter_class=CustomFormatter,
color=True,
)
parser.add_argument('+f', '++foo', help="foo help")
parser.add_argument('spam', help="spam help")

prog = self.theme.prog
heading = self.theme.heading
short = self.theme.summary_short_option
label = self.theme.summary_label
pos = self.theme.summary_action
long_b = self.theme.long_option
short_b = self.theme.short_option
label_b = self.theme.label
pos_b = self.theme.action
reset = self.theme.reset

parser_help = parser.format_help()
self.assertEqual(parser_help, textwrap.dedent(f'''\
{heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}+f {label}FOO{reset}] {pos}spam{reset}

{heading}positional arguments:{reset}
{pos_b}spam{reset} spam help

{heading}options:{reset}
{short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
{short_b}+f{reset}, {long_b}++foo{reset} {label_b}FOO{reset} foo help
'''))


def tearDownModule():
# Remove global references to avoid looking like we have refleaks.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fix :class:`argparse.ArgumentParser` with the *formatter_class* argument.
Fix TypeError when *formatter_class* is a custom subclass of
:class:`!HelpFormatter`.
Fix TypeError when *formatter_class* is not a subclass of
:class:`!HelpFormatter` and non-standard *prefix_char* is used.
Fix support of colorizing when *formatter_class* is not a subclass of
:class:`!HelpFormatter`.
Loading
0