Skip to content

Commit ed9787c

Browse files
committed
bpo12806: Add argparse FlexiHelpFormatter
1 parent 2762525 commit ed9787c

File tree

4 files changed

+206
-3
lines changed

4 files changed

+206
-3
lines changed

Doc/library/argparse.rst

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,13 @@ classes:
295295

296296
.. class:: RawDescriptionHelpFormatter
297297
RawTextHelpFormatter
298+
FlexiHelpFormatter
298299
ArgumentDefaultsHelpFormatter
299300
MetavarTypeHelpFormatter
300301

301-
:class:`RawDescriptionHelpFormatter` and :class:`RawTextHelpFormatter` give
302-
more control over how textual descriptions are displayed.
302+
:class:`RawDescriptionHelpFormatter`, :class:`RawTextHelpFormatter`, and
303+
:class:`FlexiHelpFormatter` give more control over how textual descriptions
304+
are displayed.
303305
By default, :class:`ArgumentParser` objects line-wrap the description_ and
304306
epilog_ texts in command-line help messages::
305307

@@ -354,6 +356,57 @@ including argument descriptions. However, multiple newlines are replaced with
354356
one. If you wish to preserve multiple blank lines, add spaces between the
355357
newlines.
356358

359+
:class:`FlexiHelpFormatter` wraps description and help text like the default
360+
formatter, while preserving paragraphs and supporting bulleted lists. Bullet
361+
list items are marked by the use of the "*", "-", "+", or ">" characters, or a
362+
single non-whitespace character followed by a "."::
363+
364+
>>> parser = argparse.ArgumentParser(
365+
... prog='PROG',
366+
... formatter_class=argparse.FlexiHelpFormatter,
367+
... description="""
368+
... The FlexiHelpFormatter will wrap text within paragraphs
369+
... when required to in order to make the text fit.
370+
...
371+
... Paragraphs are preserved.
372+
...
373+
... It also supports bulleted lists in a number of formats:
374+
... * stars
375+
... 1. numbers
376+
... - ... and so on
377+
... """)
378+
>>> parser.add_argument(
379+
... "argument",
380+
... help="""
381+
... Argument help text also supports flexible formatting,
382+
... with word wrap:
383+
... * See?
384+
... """)
385+
>>> parser.print_help()
386+
usage: PROG [-h] option
387+
388+
The FlexiHelpFormatter will wrap text within paragraphs when required to in
389+
order to make the text fit.
390+
391+
Paragraphs are preserved.
392+
393+
It also supports bulleted lists in a number of formats:
394+
* stars
395+
1. numbers
396+
- ... and so on
397+
398+
positional arguments:
399+
argument Argument help text also supports flexible formatting, with word
400+
wrap:
401+
* See?
402+
403+
optional arguments:
404+
-h, --help show this help message and exit
405+
406+
407+
.. versionchanged:: 3.9
408+
:class:`FlexiHelpFormatter` class was added.
409+
357410
:class:`ArgumentDefaultsHelpFormatter` automatically adds information about
358411
default values to each of the argument help messages::
359412

Lib/argparse.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
'ArgumentDefaultsHelpFormatter',
7676
'RawDescriptionHelpFormatter',
7777
'RawTextHelpFormatter',
78+
'FlexiHelpFormatter',
7879
'MetavarTypeHelpFormatter',
7980
'Namespace',
8081
'Action',
@@ -513,7 +514,10 @@ def _format_action(self, action):
513514
help_lines = self._split_lines(help_text, help_width)
514515
parts.append('%*s%s\n' % (indent_first, '', help_lines[0]))
515516
for line in help_lines[1:]:
516-
parts.append('%*s%s\n' % (help_position, '', line))
517+
if line.strip():
518+
parts.append('%*s%s\n' % (help_position, '', line))
519+
else:
520+
parts.append("\n")
517521

518522
# or add a newline if the description doesn't end with one
519523
elif not action_header.endswith('\n'):
@@ -659,6 +663,82 @@ def _split_lines(self, text, width):
659663
return text.splitlines()
660664

661665

666+
class FlexiHelpFormatter(HelpFormatter):
667+
"""Help message formatter which respects paragraphs and bulleted lists.
668+
669+
Only the name of this class is considered a public API. All the methods
670+
provided by the class are considered an implementation detail.
671+
"""
672+
673+
def _split_lines(self, text, width):
674+
return self._para_reformat(text, width)
675+
676+
def _fill_text(self, text, width, indent):
677+
lines = self._para_reformat(text, width)
678+
return "\n".join(lines)
679+
680+
def _indents(self, line):
681+
"""Return line indent level and "sub_indent" for bullet list text."""
682+
683+
indent = len(_re.match(r"( *)", line).group(1))
684+
list_match = _re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line)
685+
if list_match:
686+
sub_indent = indent + len(list_match.group(2))
687+
else:
688+
sub_indent = indent
689+
690+
return (indent, sub_indent)
691+
692+
def _split_paragraphs(self, text):
693+
"""Split text in to paragraphs of like-indented lines."""
694+
695+
import textwrap
696+
697+
text = textwrap.dedent(text).strip()
698+
text = _re.sub("\n\n[\n]+", "\n\n", text)
699+
700+
last_sub_indent = None
701+
paragraphs = list()
702+
for line in text.splitlines():
703+
(indent, sub_indent) = self._indents(line)
704+
is_text = len(line.strip()) > 0
705+
706+
if is_text and indent == sub_indent == last_sub_indent:
707+
paragraphs[-1] += " " + line
708+
else:
709+
paragraphs.append(line)
710+
711+
if is_text:
712+
last_sub_indent = sub_indent
713+
else:
714+
last_sub_indent = None
715+
716+
return paragraphs
717+
718+
def _para_reformat(self, text, width):
719+
"""Reformat text, by paragraph."""
720+
721+
import textwrap
722+
723+
lines = list()
724+
for paragraph in self._split_paragraphs(text):
725+
726+
(indent, sub_indent) = self._indents(paragraph)
727+
728+
paragraph = self._whitespace_matcher.sub(" ", paragraph).strip()
729+
new_lines = textwrap.wrap(
730+
text=paragraph,
731+
width=width,
732+
initial_indent=" " * indent,
733+
subsequent_indent=" " * sub_indent,
734+
)
735+
736+
# Blank lines get eaten by textwrap, put it back
737+
lines.extend(new_lines or [""])
738+
739+
return lines
740+
741+
662742
class ArgumentDefaultsHelpFormatter(HelpFormatter):
663743
"""Help message formatter which adds default values to argument help.
664744

Lib/test/test_argparse.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5272,6 +5272,73 @@ class TestHelpRawDescription(HelpTestCase):
52725272
version = ''
52735273

52745274

5275+
class TestHelpFlexi(HelpTestCase):
5276+
"""Test the FlexiHelpFormatter"""
5277+
5278+
parser_signature = Sig(
5279+
prog='PROG', formatter_class=argparse.FlexiHelpFormatter,
5280+
description='This text should be wrapped as appropriate to keep\n'
5281+
'things nice and very, very tidy.\n'
5282+
'\n'
5283+
'Paragraphs should be preserved.\n'
5284+
' * bullet list items\n'
5285+
' should wrap to an appropriate place,\n'
5286+
' should such wrapping be required.\n'
5287+
' * short bullet\n'
5288+
)
5289+
5290+
argument_signatures = [
5291+
Sig('--foo', help=' foo help should also\n'
5292+
'appear as given here\n'
5293+
'\n'
5294+
'along with a second paragraph, if called for\n'
5295+
' * bullet'),
5296+
Sig('spam', help='spam help'),
5297+
]
5298+
argument_group_signatures = [
5299+
(Sig('title', description='short help text\n'
5300+
'\n'
5301+
'Longer help text, containing useful\n'
5302+
'contextual information for the var in\n'
5303+
'question\n'
5304+
'* and a bullet\n'),
5305+
[Sig('--bar', help='bar help')]),
5306+
]
5307+
usage = '''\
5308+
usage: PROG [-h] [--foo FOO] [--bar BAR] spam
5309+
'''
5310+
help = usage + '''\
5311+
5312+
This text should be wrapped as appropriate to keep things nice and very, very
5313+
tidy.
5314+
5315+
Paragraphs should be preserved.
5316+
* bullet list items should wrap to an appropriate place, should such
5317+
wrapping be required.
5318+
* short bullet
5319+
5320+
positional arguments:
5321+
spam spam help
5322+
5323+
optional arguments:
5324+
-h, --help show this help message and exit
5325+
--foo FOO foo help should also appear as given here
5326+
5327+
along with a second paragraph, if called for
5328+
* bullet
5329+
5330+
title:
5331+
short help text
5332+
5333+
Longer help text, containing useful contextual information for the var in
5334+
question
5335+
* and a bullet
5336+
5337+
--bar BAR bar help
5338+
'''
5339+
version = ''
5340+
5341+
52755342
class TestHelpArgumentDefaults(HelpTestCase):
52765343
"""Test the ArgumentDefaultsHelpFormatter"""
52775344

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :mod:`argparse` module has a new :class:`argparse.FlexiHelpFormatter`
2+
class that wraps help and description text while preserving paragraphs and
3+
supporting bulleted lists.

0 commit comments

Comments
 (0)