Skip to content

Commit c15980b

Browse files
[3.14] gh-133653: Fix argparse.ArgumentParser with the formatter_class argument (GH-133813) (GH-133941)
* Fix TypeError when formatter_class is a custom subclass of HelpFormatter. * Fix TypeError when formatter_class is not a subclass of HelpFormatter and non-standard prefix_char is used. * Fix support of colorizing when formatter_class is not a subclass of HelpFormatter. * Remove the prefix_chars parameter of HelpFormatter. (cherry picked from commit 734e15b) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent c11fc4b commit c15980b

File tree

3 files changed

+154
-43
lines changed

3 files changed

+154
-43
lines changed

Lib/argparse.py

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ def __init__(
167167
indent_increment=2,
168168
max_help_position=24,
169169
width=None,
170-
prefix_chars='-',
171170
color=False,
172171
):
173172
# default setting for width
@@ -176,16 +175,7 @@ def __init__(
176175
width = shutil.get_terminal_size().columns
177176
width -= 2
178177

179-
from _colorize import can_colorize, decolor, get_theme
180-
181-
if color and can_colorize():
182-
self._theme = get_theme(force_color=True).argparse
183-
self._decolor = decolor
184-
else:
185-
self._theme = get_theme(force_no_color=True).argparse
186-
self._decolor = lambda text: text
187-
188-
self._prefix_chars = prefix_chars
178+
self._set_color(color)
189179
self._prog = prog
190180
self._indent_increment = indent_increment
191181
self._max_help_position = min(max_help_position,
@@ -202,6 +192,16 @@ def __init__(
202192
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
203193
self._long_break_matcher = _re.compile(r'\n\n\n+')
204194

195+
def _set_color(self, color):
196+
from _colorize import can_colorize, decolor, get_theme
197+
198+
if color and can_colorize():
199+
self._theme = get_theme(force_color=True).argparse
200+
self._decolor = decolor
201+
else:
202+
self._theme = get_theme(force_no_color=True).argparse
203+
self._decolor = lambda text: text
204+
205205
# ===============================
206206
# Section and indentation methods
207207
# ===============================
@@ -415,14 +415,7 @@ def _format_actions_usage(self, actions, groups):
415415
return ' '.join(self._get_actions_usage_parts(actions, groups))
416416

417417
def _is_long_option(self, string):
418-
return len(string) >= 2 and string[1] in self._prefix_chars
419-
420-
def _is_short_option(self, string):
421-
return (
422-
not self._is_long_option(string)
423-
and len(string) >= 1
424-
and string[0] in self._prefix_chars
425-
)
418+
return len(string) > 2
426419

427420
def _get_actions_usage_parts(self, actions, groups):
428421
# find group indices and identify actions in groups
@@ -471,25 +464,22 @@ def _get_actions_usage_parts(self, actions, groups):
471464
# produce the first way to invoke the option in brackets
472465
else:
473466
option_string = action.option_strings[0]
467+
if self._is_long_option(option_string):
468+
option_color = t.summary_long_option
469+
else:
470+
option_color = t.summary_short_option
474471

475472
# if the Optional doesn't take a value, format is:
476473
# -s or --long
477474
if action.nargs == 0:
478475
part = action.format_usage()
479-
if self._is_long_option(part):
480-
part = f"{t.summary_long_option}{part}{t.reset}"
481-
elif self._is_short_option(part):
482-
part = f"{t.summary_short_option}{part}{t.reset}"
476+
part = f"{option_color}{part}{t.reset}"
483477

484478
# if the Optional takes a value, format is:
485479
# -s ARGS or --long ARGS
486480
else:
487481
default = self._get_default_metavar_for_optional(action)
488482
args_string = self._format_args(action, default)
489-
if self._is_long_option(option_string):
490-
option_color = t.summary_long_option
491-
elif self._is_short_option(option_string):
492-
option_color = t.summary_short_option
493483
part = (
494484
f"{option_color}{option_string} "
495485
f"{t.summary_label}{args_string}{t.reset}"
@@ -606,10 +596,8 @@ def color_option_strings(strings):
606596
for s in strings:
607597
if self._is_long_option(s):
608598
parts.append(f"{t.long_option}{s}{t.reset}")
609-
elif self._is_short_option(s):
610-
parts.append(f"{t.short_option}{s}{t.reset}")
611599
else:
612-
parts.append(s)
600+
parts.append(f"{t.short_option}{s}{t.reset}")
613601
return parts
614602

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

27252713
def _get_formatter(self):
2726-
if isinstance(self.formatter_class, type) and issubclass(
2727-
self.formatter_class, HelpFormatter
2728-
):
2729-
return self.formatter_class(
2730-
prog=self.prog,
2731-
prefix_chars=self.prefix_chars,
2732-
color=self.color,
2733-
)
2734-
else:
2735-
return self.formatter_class(prog=self.prog)
2714+
formatter = self.formatter_class(prog=self.prog)
2715+
formatter._set_color(self.color)
2716+
return formatter
27362717

27372718
# =====================
27382719
# Help-printing methods

Lib/test/test_argparse.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5469,11 +5469,60 @@ def custom_type(string):
54695469
version = ''
54705470

54715471

5472-
class TestHelpUsageLongSubparserCommand(TestCase):
5473-
"""Test that subparser commands are formatted correctly in help"""
5472+
class TestHelpCustomHelpFormatter(TestCase):
54745473
maxDiff = None
54755474

5476-
def test_parent_help(self):
5475+
def test_custom_formatter_function(self):
5476+
def custom_formatter(prog):
5477+
return argparse.RawTextHelpFormatter(prog, indent_increment=5)
5478+
5479+
parser = argparse.ArgumentParser(
5480+
prog='PROG',
5481+
prefix_chars='-+',
5482+
formatter_class=custom_formatter
5483+
)
5484+
parser.add_argument('+f', '++foo', help="foo help")
5485+
parser.add_argument('spam', help="spam help")
5486+
5487+
parser_help = parser.format_help()
5488+
self.assertEqual(parser_help, textwrap.dedent('''\
5489+
usage: PROG [-h] [+f FOO] spam
5490+
5491+
positional arguments:
5492+
spam spam help
5493+
5494+
options:
5495+
-h, --help show this help message and exit
5496+
+f, ++foo FOO foo help
5497+
'''))
5498+
5499+
def test_custom_formatter_class(self):
5500+
class CustomFormatter(argparse.RawTextHelpFormatter):
5501+
def __init__(self, prog):
5502+
super().__init__(prog, indent_increment=5)
5503+
5504+
parser = argparse.ArgumentParser(
5505+
prog='PROG',
5506+
prefix_chars='-+',
5507+
formatter_class=CustomFormatter
5508+
)
5509+
parser.add_argument('+f', '++foo', help="foo help")
5510+
parser.add_argument('spam', help="spam help")
5511+
5512+
parser_help = parser.format_help()
5513+
self.assertEqual(parser_help, textwrap.dedent('''\
5514+
usage: PROG [-h] [+f FOO] spam
5515+
5516+
positional arguments:
5517+
spam spam help
5518+
5519+
options:
5520+
-h, --help show this help message and exit
5521+
+f, ++foo FOO foo help
5522+
'''))
5523+
5524+
def test_usage_long_subparser_command(self):
5525+
"""Test that subparser commands are formatted correctly in help"""
54775526
def custom_formatter(prog):
54785527
return argparse.RawTextHelpFormatter(prog, max_help_position=50)
54795528

@@ -7053,6 +7102,7 @@ def test_translations(self):
70537102

70547103

70557104
class TestColorized(TestCase):
7105+
maxDiff = None
70567106

70577107
def setUp(self):
70587108
super().setUp()
@@ -7211,6 +7261,79 @@ def test_argparse_color_usage(self):
72117261
),
72127262
)
72137263

7264+
def test_custom_formatter_function(self):
7265+
def custom_formatter(prog):
7266+
return argparse.RawTextHelpFormatter(prog, indent_increment=5)
7267+
7268+
parser = argparse.ArgumentParser(
7269+
prog="PROG",
7270+
prefix_chars="-+",
7271+
formatter_class=custom_formatter,
7272+
color=True,
7273+
)
7274+
parser.add_argument('+f', '++foo', help="foo help")
7275+
parser.add_argument('spam', help="spam help")
7276+
7277+
prog = self.theme.prog
7278+
heading = self.theme.heading
7279+
short = self.theme.summary_short_option
7280+
label = self.theme.summary_label
7281+
pos = self.theme.summary_action
7282+
long_b = self.theme.long_option
7283+
short_b = self.theme.short_option
7284+
label_b = self.theme.label
7285+
pos_b = self.theme.action
7286+
reset = self.theme.reset
7287+
7288+
parser_help = parser.format_help()
7289+
self.assertEqual(parser_help, textwrap.dedent(f'''\
7290+
{heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}+f {label}FOO{reset}] {pos}spam{reset}
7291+
7292+
{heading}positional arguments:{reset}
7293+
{pos_b}spam{reset} spam help
7294+
7295+
{heading}options:{reset}
7296+
{short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
7297+
{short_b}+f{reset}, {long_b}++foo{reset} {label_b}FOO{reset} foo help
7298+
'''))
7299+
7300+
def test_custom_formatter_class(self):
7301+
class CustomFormatter(argparse.RawTextHelpFormatter):
7302+
def __init__(self, prog):
7303+
super().__init__(prog, indent_increment=5)
7304+
7305+
parser = argparse.ArgumentParser(
7306+
prog="PROG",
7307+
prefix_chars="-+",
7308+
formatter_class=CustomFormatter,
7309+
color=True,
7310+
)
7311+
parser.add_argument('+f', '++foo', help="foo help")
7312+
parser.add_argument('spam', help="spam help")
7313+
7314+
prog = self.theme.prog
7315+
heading = self.theme.heading
7316+
short = self.theme.summary_short_option
7317+
label = self.theme.summary_label
7318+
pos = self.theme.summary_action
7319+
long_b = self.theme.long_option
7320+
short_b = self.theme.short_option
7321+
label_b = self.theme.label
7322+
pos_b = self.theme.action
7323+
reset = self.theme.reset
7324+
7325+
parser_help = parser.format_help()
7326+
self.assertEqual(parser_help, textwrap.dedent(f'''\
7327+
{heading}usage: {reset}{prog}PROG{reset} [{short}-h{reset}] [{short}+f {label}FOO{reset}] {pos}spam{reset}
7328+
7329+
{heading}positional arguments:{reset}
7330+
{pos_b}spam{reset} spam help
7331+
7332+
{heading}options:{reset}
7333+
{short_b}-h{reset}, {long_b}--help{reset} show this help message and exit
7334+
{short_b}+f{reset}, {long_b}++foo{reset} {label_b}FOO{reset} foo help
7335+
'''))
7336+
72147337

72157338
def tearDownModule():
72167339
# Remove global references to avoid looking like we have refleaks.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix :class:`argparse.ArgumentParser` with the *formatter_class* argument.
2+
Fix TypeError when *formatter_class* is a custom subclass of
3+
:class:`!HelpFormatter`.
4+
Fix TypeError when *formatter_class* is not a subclass of
5+
:class:`!HelpFormatter` and non-standard *prefix_char* is used.
6+
Fix support of colorizing when *formatter_class* is not a subclass of
7+
:class:`!HelpFormatter`.

0 commit comments

Comments
 (0)