mirror of
https://github.com/python/cpython.git
synced 2025-09-25 01:43:11 +00:00
[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 734e15b70d
)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
c11fc4bc96
commit
c15980b57b
3 changed files with 154 additions and 43 deletions
|
@ -167,7 +167,6 @@ class HelpFormatter(object):
|
||||||
indent_increment=2,
|
indent_increment=2,
|
||||||
max_help_position=24,
|
max_help_position=24,
|
||||||
width=None,
|
width=None,
|
||||||
prefix_chars='-',
|
|
||||||
color=False,
|
color=False,
|
||||||
):
|
):
|
||||||
# default setting for width
|
# default setting for width
|
||||||
|
@ -176,16 +175,7 @@ class HelpFormatter(object):
|
||||||
width = shutil.get_terminal_size().columns
|
width = shutil.get_terminal_size().columns
|
||||||
width -= 2
|
width -= 2
|
||||||
|
|
||||||
from _colorize import can_colorize, decolor, get_theme
|
self._set_color(color)
|
||||||
|
|
||||||
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._prog = prog
|
self._prog = prog
|
||||||
self._indent_increment = indent_increment
|
self._indent_increment = indent_increment
|
||||||
self._max_help_position = min(max_help_position,
|
self._max_help_position = min(max_help_position,
|
||||||
|
@ -202,6 +192,16 @@ class HelpFormatter(object):
|
||||||
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
|
self._whitespace_matcher = _re.compile(r'\s+', _re.ASCII)
|
||||||
self._long_break_matcher = _re.compile(r'\n\n\n+')
|
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
|
# Section and indentation methods
|
||||||
# ===============================
|
# ===============================
|
||||||
|
@ -415,14 +415,7 @@ class HelpFormatter(object):
|
||||||
return ' '.join(self._get_actions_usage_parts(actions, groups))
|
return ' '.join(self._get_actions_usage_parts(actions, groups))
|
||||||
|
|
||||||
def _is_long_option(self, string):
|
def _is_long_option(self, string):
|
||||||
return len(string) >= 2 and string[1] in self._prefix_chars
|
return len(string) > 2
|
||||||
|
|
||||||
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):
|
def _get_actions_usage_parts(self, actions, groups):
|
||||||
# find group indices and identify actions in groups
|
# find group indices and identify actions in groups
|
||||||
|
@ -471,25 +464,22 @@ class HelpFormatter(object):
|
||||||
# produce the first way to invoke the option in brackets
|
# produce the first way to invoke the option in brackets
|
||||||
else:
|
else:
|
||||||
option_string = action.option_strings[0]
|
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:
|
# if the Optional doesn't take a value, format is:
|
||||||
# -s or --long
|
# -s or --long
|
||||||
if action.nargs == 0:
|
if action.nargs == 0:
|
||||||
part = action.format_usage()
|
part = action.format_usage()
|
||||||
if self._is_long_option(part):
|
part = f"{option_color}{part}{t.reset}"
|
||||||
part = f"{t.summary_long_option}{part}{t.reset}"
|
|
||||||
elif self._is_short_option(part):
|
|
||||||
part = f"{t.summary_short_option}{part}{t.reset}"
|
|
||||||
|
|
||||||
# if the Optional takes a value, format is:
|
# if the Optional takes a value, format is:
|
||||||
# -s ARGS or --long ARGS
|
# -s ARGS or --long ARGS
|
||||||
else:
|
else:
|
||||||
default = self._get_default_metavar_for_optional(action)
|
default = self._get_default_metavar_for_optional(action)
|
||||||
args_string = self._format_args(action, default)
|
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 = (
|
part = (
|
||||||
f"{option_color}{option_string} "
|
f"{option_color}{option_string} "
|
||||||
f"{t.summary_label}{args_string}{t.reset}"
|
f"{t.summary_label}{args_string}{t.reset}"
|
||||||
|
@ -606,10 +596,8 @@ class HelpFormatter(object):
|
||||||
for s in strings:
|
for s in strings:
|
||||||
if self._is_long_option(s):
|
if self._is_long_option(s):
|
||||||
parts.append(f"{t.long_option}{s}{t.reset}")
|
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:
|
else:
|
||||||
parts.append(s)
|
parts.append(f"{t.short_option}{s}{t.reset}")
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
# if the Optional doesn't take a value, format is:
|
# if the Optional doesn't take a value, format is:
|
||||||
|
@ -2723,16 +2711,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
return formatter.format_help()
|
return formatter.format_help()
|
||||||
|
|
||||||
def _get_formatter(self):
|
def _get_formatter(self):
|
||||||
if isinstance(self.formatter_class, type) and issubclass(
|
formatter = self.formatter_class(prog=self.prog)
|
||||||
self.formatter_class, HelpFormatter
|
formatter._set_color(self.color)
|
||||||
):
|
return formatter
|
||||||
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
|
# Help-printing methods
|
||||||
|
|
|
@ -5469,11 +5469,60 @@ class TestHelpMetavarTypeFormatter(HelpTestCase):
|
||||||
version = ''
|
version = ''
|
||||||
|
|
||||||
|
|
||||||
class TestHelpUsageLongSubparserCommand(TestCase):
|
class TestHelpCustomHelpFormatter(TestCase):
|
||||||
"""Test that subparser commands are formatted correctly in help"""
|
|
||||||
maxDiff = None
|
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):
|
def custom_formatter(prog):
|
||||||
return argparse.RawTextHelpFormatter(prog, max_help_position=50)
|
return argparse.RawTextHelpFormatter(prog, max_help_position=50)
|
||||||
|
|
||||||
|
@ -7053,6 +7102,7 @@ class TestTranslations(TestTranslationsBase):
|
||||||
|
|
||||||
|
|
||||||
class TestColorized(TestCase):
|
class TestColorized(TestCase):
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
@ -7211,6 +7261,79 @@ class TestColorized(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 CustomFormatter(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():
|
def tearDownModule():
|
||||||
# Remove global references to avoid looking like we have refleaks.
|
# Remove global references to avoid looking like we have refleaks.
|
||||||
|
|
|
@ -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…
Add table
Add a link
Reference in a new issue