From ed9787c275c84b9d6575259e98018b8847ec7559 Mon Sep 17 00:00:00 2001 From: David Steele Date: Sun, 6 Sep 2020 20:51:44 -0400 Subject: [PATCH 1/6] bpo12806: Add argparse FlexiHelpFormatter --- Doc/library/argparse.rst | 57 ++++++++++++- Lib/argparse.py | 82 ++++++++++++++++++- Lib/test/test_argparse.py | 67 +++++++++++++++ .../2020-09-07-00-00-51.bpo-12806.j1A_9o.rst | 3 + 4 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-09-07-00-00-51.bpo-12806.j1A_9o.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 8d0116d8c060b8..67c5a83c697811 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -295,11 +295,13 @@ classes: .. class:: RawDescriptionHelpFormatter RawTextHelpFormatter + FlexiHelpFormatter ArgumentDefaultsHelpFormatter MetavarTypeHelpFormatter -:class:`RawDescriptionHelpFormatter` and :class:`RawTextHelpFormatter` give -more control over how textual descriptions are displayed. +:class:`RawDescriptionHelpFormatter`, :class:`RawTextHelpFormatter`, and +:class:`FlexiHelpFormatter` give more control over how textual descriptions +are displayed. By default, :class:`ArgumentParser` objects line-wrap the description_ and epilog_ texts in command-line help messages:: @@ -354,6 +356,57 @@ including argument descriptions. However, multiple newlines are replaced with one. If you wish to preserve multiple blank lines, add spaces between the newlines. +:class:`FlexiHelpFormatter` wraps description and help text like the default +formatter, while preserving paragraphs and supporting bulleted lists. Bullet +list items are marked by the use of the "*", "-", "+", or ">" characters, or a +single non-whitespace character followed by a ".":: + + >>> parser = argparse.ArgumentParser( + ... prog='PROG', + ... formatter_class=argparse.FlexiHelpFormatter, + ... description=""" + ... The FlexiHelpFormatter will wrap text within paragraphs + ... when required to in order to make the text fit. + ... + ... Paragraphs are preserved. + ... + ... It also supports bulleted lists in a number of formats: + ... * stars + ... 1. numbers + ... - ... and so on + ... """) + >>> parser.add_argument( + ... "argument", + ... help=""" + ... Argument help text also supports flexible formatting, + ... with word wrap: + ... * See? + ... """) + >>> parser.print_help() + usage: PROG [-h] option + + The FlexiHelpFormatter will wrap text within paragraphs when required to in + order to make the text fit. + + Paragraphs are preserved. + + It also supports bulleted lists in a number of formats: + * stars + 1. numbers + - ... and so on + + positional arguments: + argument Argument help text also supports flexible formatting, with word + wrap: + * See? + + optional arguments: + -h, --help show this help message and exit + + +.. versionchanged:: 3.9 + :class:`FlexiHelpFormatter` class was added. + :class:`ArgumentDefaultsHelpFormatter` automatically adds information about default values to each of the argument help messages:: diff --git a/Lib/argparse.py b/Lib/argparse.py index d24fa72e573d4f..5839c3c27bf0bb 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -75,6 +75,7 @@ 'ArgumentDefaultsHelpFormatter', 'RawDescriptionHelpFormatter', 'RawTextHelpFormatter', + 'FlexiHelpFormatter', 'MetavarTypeHelpFormatter', 'Namespace', 'Action', @@ -513,7 +514,10 @@ def _format_action(self, action): help_lines = self._split_lines(help_text, help_width) parts.append('%*s%s\n' % (indent_first, '', help_lines[0])) for line in help_lines[1:]: - parts.append('%*s%s\n' % (help_position, '', line)) + if line.strip(): + parts.append('%*s%s\n' % (help_position, '', line)) + else: + parts.append("\n") # or add a newline if the description doesn't end with one elif not action_header.endswith('\n'): @@ -659,6 +663,82 @@ def _split_lines(self, text, width): return text.splitlines() +class FlexiHelpFormatter(HelpFormatter): + """Help message formatter which respects paragraphs and bulleted lists. + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + def _split_lines(self, text, width): + return self._para_reformat(text, width) + + def _fill_text(self, text, width, indent): + lines = self._para_reformat(text, width) + return "\n".join(lines) + + def _indents(self, line): + """Return line indent level and "sub_indent" for bullet list text.""" + + indent = len(_re.match(r"( *)", line).group(1)) + list_match = _re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line) + if list_match: + sub_indent = indent + len(list_match.group(2)) + else: + sub_indent = indent + + return (indent, sub_indent) + + def _split_paragraphs(self, text): + """Split text in to paragraphs of like-indented lines.""" + + import textwrap + + text = textwrap.dedent(text).strip() + text = _re.sub("\n\n[\n]+", "\n\n", text) + + last_sub_indent = None + paragraphs = list() + for line in text.splitlines(): + (indent, sub_indent) = self._indents(line) + is_text = len(line.strip()) > 0 + + if is_text and indent == sub_indent == last_sub_indent: + paragraphs[-1] += " " + line + else: + paragraphs.append(line) + + if is_text: + last_sub_indent = sub_indent + else: + last_sub_indent = None + + return paragraphs + + def _para_reformat(self, text, width): + """Reformat text, by paragraph.""" + + import textwrap + + lines = list() + for paragraph in self._split_paragraphs(text): + + (indent, sub_indent) = self._indents(paragraph) + + paragraph = self._whitespace_matcher.sub(" ", paragraph).strip() + new_lines = textwrap.wrap( + text=paragraph, + width=width, + initial_indent=" " * indent, + subsequent_indent=" " * sub_indent, + ) + + # Blank lines get eaten by textwrap, put it back + lines.extend(new_lines or [""]) + + return lines + + class ArgumentDefaultsHelpFormatter(HelpFormatter): """Help message formatter which adds default values to argument help. diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 488a3a4ed20fac..ea8b7a93223385 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -5272,6 +5272,73 @@ class TestHelpRawDescription(HelpTestCase): version = '' +class TestHelpFlexi(HelpTestCase): + """Test the FlexiHelpFormatter""" + + parser_signature = Sig( + prog='PROG', formatter_class=argparse.FlexiHelpFormatter, + description='This text should be wrapped as appropriate to keep\n' + 'things nice and very, very tidy.\n' + '\n' + 'Paragraphs should be preserved.\n' + ' * bullet list items\n' + ' should wrap to an appropriate place,\n' + ' should such wrapping be required.\n' + ' * short bullet\n' + ) + + argument_signatures = [ + Sig('--foo', help=' foo help should also\n' + 'appear as given here\n' + '\n' + 'along with a second paragraph, if called for\n' + ' * bullet'), + Sig('spam', help='spam help'), + ] + argument_group_signatures = [ + (Sig('title', description='short help text\n' + '\n' + 'Longer help text, containing useful\n' + 'contextual information for the var in\n' + 'question\n' + '* and a bullet\n'), + [Sig('--bar', help='bar help')]), + ] + usage = '''\ + usage: PROG [-h] [--foo FOO] [--bar BAR] spam + ''' + help = usage + '''\ + + This text should be wrapped as appropriate to keep things nice and very, very + tidy. + + Paragraphs should be preserved. + * bullet list items should wrap to an appropriate place, should such + wrapping be required. + * short bullet + + positional arguments: + spam spam help + + optional arguments: + -h, --help show this help message and exit + --foo FOO foo help should also appear as given here + + along with a second paragraph, if called for + * bullet + + title: + short help text + + Longer help text, containing useful contextual information for the var in + question + * and a bullet + + --bar BAR bar help + ''' + version = '' + + class TestHelpArgumentDefaults(HelpTestCase): """Test the ArgumentDefaultsHelpFormatter""" diff --git a/Misc/NEWS.d/next/Library/2020-09-07-00-00-51.bpo-12806.j1A_9o.rst b/Misc/NEWS.d/next/Library/2020-09-07-00-00-51.bpo-12806.j1A_9o.rst new file mode 100644 index 00000000000000..e74b4a05e4cc4f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-09-07-00-00-51.bpo-12806.j1A_9o.rst @@ -0,0 +1,3 @@ +The :mod:`argparse` module has a new :class:`argparse.FlexiHelpFormatter` +class that wraps help and description text while preserving paragraphs and +supporting bulleted lists. From 18b8311a7427efd958ea373a05af2f9e4fad3ba0 Mon Sep 17 00:00:00 2001 From: David Steele Date: Fri, 23 Oct 2020 10:25:38 -0400 Subject: [PATCH 2/6] Update FlexiFormatter release to v3.10 --- Doc/library/argparse.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 67c5a83c697811..34f7b1fcaa170a 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -404,7 +404,7 @@ single non-whitespace character followed by a ".":: -h, --help show this help message and exit -.. versionchanged:: 3.9 +.. versionadded:: 3.10 :class:`FlexiHelpFormatter` class was added. :class:`ArgumentDefaultsHelpFormatter` automatically adds information about From f692c5783cd6b9104cbb7c540cd7c354f7ac1dfa Mon Sep 17 00:00:00 2001 From: David Steele Date: Fri, 5 Feb 2021 10:29:37 -0500 Subject: [PATCH 3/6] FlexiHelpFormatter: Recognize "-" bullets --- Lib/argparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 5839c3c27bf0bb..82c98d4afe33aa 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -681,7 +681,7 @@ def _indents(self, line): """Return line indent level and "sub_indent" for bullet list text.""" indent = len(_re.match(r"( *)", line).group(1)) - list_match = _re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line) + list_match = _re.match(r"( *)(([*\-+>]+|\w+\)|\w+\.) +)", line) if list_match: sub_indent = indent + len(list_match.group(2)) else: From 757311dea3babec9614d6cbd15b4bed5586bac1a Mon Sep 17 00:00:00 2001 From: David Steele Date: Fri, 9 Apr 2021 12:39:38 -0400 Subject: [PATCH 4/6] Fix test --- Lib/test/test_argparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index ea8b7a93223385..ebdaba79ad39ba 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -5320,7 +5320,7 @@ class TestHelpFlexi(HelpTestCase): positional arguments: spam spam help - optional arguments: + options: -h, --help show this help message and exit --foo FOO foo help should also appear as given here From 6282547ea8a8c227db192607668b5a020208ded8 Mon Sep 17 00:00:00 2001 From: David Steele <565205+davesteele@users.noreply.github.com> Date: Fri, 9 Jul 2021 19:55:18 -0400 Subject: [PATCH 5/6] Set FlexiHelpFormatter release to 3.11 Rebase to force CI retest. The "addres sanitizer" test is failing with a timeout, on test_multiprocessing_fork.WithProcessesTestQueue(). It is not likely that this branch contributed to that failure. https://github.com/python/cpython/pull/22129/checks?check_run_id=3033564406 --- Doc/library/argparse.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 34f7b1fcaa170a..53da6d6b545bf3 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -404,7 +404,7 @@ single non-whitespace character followed by a ".":: -h, --help show this help message and exit -.. versionadded:: 3.10 +.. versionadded:: 3.11 :class:`FlexiHelpFormatter` class was added. :class:`ArgumentDefaultsHelpFormatter` automatically adds information about From a21117a153a66ab811f8e32252419284e68aea96 Mon Sep 17 00:00:00 2001 From: David Steele Date: Fri, 1 Mar 2024 20:35:40 -0500 Subject: [PATCH 6/6] Update FlexiHelpFormatter release to 3.13 --- Doc/library/argparse.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 53da6d6b545bf3..77ca41d1215fce 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -404,7 +404,7 @@ single non-whitespace character followed by a ".":: -h, --help show this help message and exit -.. versionadded:: 3.11 +.. versionadded:: 3.13 :class:`FlexiHelpFormatter` class was added. :class:`ArgumentDefaultsHelpFormatter` automatically adds information about