diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 8d0116d8c060b8..77ca41d1215fce 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 + + +.. versionadded:: 3.13 + :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..82c98d4afe33aa 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..ebdaba79ad39ba 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 + + options: + -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.