Skip to content

bpo-12806: Add argparse FlexiHelpFormatter #22129

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
57 changes: 55 additions & 2 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down Expand Up @@ -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::

Expand Down
82 changes: 81 additions & 1 deletion Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
'ArgumentDefaultsHelpFormatter',
'RawDescriptionHelpFormatter',
'RawTextHelpFormatter',
'FlexiHelpFormatter',
'MetavarTypeHelpFormatter',
'Namespace',
'Action',
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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.

Expand Down
67 changes: 67 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading