Skip to content

Commit cfabcef

Browse files
committed
bpo12806: Add argparse FlexiHelpFormatter
1 parent d00a449 commit cfabcef

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
@@ -381,11 +381,13 @@ classes:
381381

382382
.. class:: RawDescriptionHelpFormatter
383383
RawTextHelpFormatter
384+
FlexiHelpFormatter
384385
ArgumentDefaultsHelpFormatter
385386
MetavarTypeHelpFormatter
386387

387-
:class:`RawDescriptionHelpFormatter` and :class:`RawTextHelpFormatter` give
388-
more control over how textual descriptions are displayed.
388+
:class:`RawDescriptionHelpFormatter`, :class:`RawTextHelpFormatter`, and
389+
:class:`FlexiHelpFormatter` give more control over how textual descriptions
390+
are displayed.
389391
By default, :class:`ArgumentParser` objects line-wrap the description_ and
390392
epilog_ texts in command-line help messages::
391393

@@ -440,6 +442,57 @@ including argument descriptions. However, multiple new lines are replaced with
440442
one. If you wish to preserve multiple blank lines, add spaces between the
441443
newlines.
442444

445+
:class:`FlexiHelpFormatter` wraps description and help text like the default
446+
formatter, while preserving paragraphs and supporting bulleted lists. Bullet
447+
list items are marked by the use of the "*", "-", "+", or ">" characters, or a
448+
single non-whitespace character followed by a "."::
449+
450+
>>> parser = argparse.ArgumentParser(
451+
... prog='PROG',
452+
... formatter_class=argparse.FlexiHelpFormatter,
453+
... description="""
454+
... The FlexiHelpFormatter will wrap text within paragraphs
455+
... when required to in order to make the text fit.
456+
...
457+
... Paragraphs are preserved.
458+
...
459+
... It also supports bulleted lists in a number of formats:
460+
... * stars
461+
... 1. numbers
462+
... - ... and so on
463+
... """)
464+
>>> parser.add_argument(
465+
... "argument",
466+
... help="""
467+
... Argument help text also supports flexible formatting,
468+
... with word wrap:
469+
... * See?
470+
... """)
471+
>>> parser.print_help()
472+
usage: PROG [-h] option
473+
474+
The FlexiHelpFormatter will wrap text within paragraphs when required to in
475+
order to make the text fit.
476+
477+
Paragraphs are preserved.
478+
479+
It also supports bulleted lists in a number of formats:
480+
* stars
481+
1. numbers
482+
- ... and so on
483+
484+
positional arguments:
485+
argument Argument help text also supports flexible formatting, with word
486+
wrap:
487+
* See?
488+
489+
optional arguments:
490+
-h, --help show this help message and exit
491+
492+
493+
.. versionchanged:: 3.9
494+
:class:`FlexiHelpFormatter` class was added.
495+
443496
:class:`ArgumentDefaultsHelpFormatter` automatically adds information about
444497
default values to each of the argument help messages::
445498

Lib/argparse.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
'ArgumentDefaultsHelpFormatter',
7474
'RawDescriptionHelpFormatter',
7575
'RawTextHelpFormatter',
76+
'FlexiHelpFormatter',
7677
'MetavarTypeHelpFormatter',
7778
'Namespace',
7879
'Action',
@@ -531,7 +532,10 @@ def _format_action(self, action):
531532
help_lines = self._split_lines(help_text, help_width)
532533
parts.append('%*s%s\n' % (indent_first, '', help_lines[0]))
533534
for line in help_lines[1:]:
534-
parts.append('%*s%s\n' % (help_position, '', line))
535+
if line.strip():
536+
parts.append('%*s%s\n' % (help_position, '', line))
537+
else:
538+
parts.append("\n")
535539

536540
# or add a newline if the description doesn't end with one
537541
elif not action_header.endswith('\n'):
@@ -681,6 +685,82 @@ def _split_lines(self, text, width):
681685
return text.splitlines()
682686

683687

688+
class FlexiHelpFormatter(HelpFormatter):
689+
"""Help message formatter which respects paragraphs and bulleted lists.
690+
691+
Only the name of this class is considered a public API. All the methods
692+
provided by the class are considered an implementation detail.
693+
"""
694+
695+
def _split_lines(self, text, width):
696+
return self._para_reformat(text, width)
697+
698+
def _fill_text(self, text, width, indent):
699+
lines = self._para_reformat(text, width)
700+
return "\n".join(lines)
701+
702+
def _indents(self, line):
703+
"""Return line indent level and "sub_indent" for bullet list text."""
704+
705+
indent = len(_re.match(r"( *)", line).group(1))
706+
list_match = _re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line)
707+
if list_match:
708+
sub_indent = indent + len(list_match.group(2))
709+
else:
710+
sub_indent = indent
711+
712+
return (indent, sub_indent)
713+
714+
def _split_paragraphs(self, text):
715+
"""Split text in to paragraphs of like-indented lines."""
716+
717+
import textwrap
718+
719+
text = textwrap.dedent(text).strip()
720+
text = _re.sub("\n\n[\n]+", "\n\n", text)
721+
722+
last_sub_indent = None
723+
paragraphs = list()
724+
for line in text.splitlines():
725+
(indent, sub_indent) = self._indents(line)
726+
is_text = len(line.strip()) > 0
727+
728+
if is_text and indent == sub_indent == last_sub_indent:
729+
paragraphs[-1] += " " + line
730+
else:
731+
paragraphs.append(line)
732+
733+
if is_text:
734+
last_sub_indent = sub_indent
735+
else:
736+
last_sub_indent = None
737+
738+
return paragraphs
739+
740+
def _para_reformat(self, text, width):
741+
"""Reformat text, by paragraph."""
742+
743+
import textwrap
744+
745+
lines = list()
746+
for paragraph in self._split_paragraphs(text):
747+
748+
(indent, sub_indent) = self._indents(paragraph)
749+
750+
paragraph = self._whitespace_matcher.sub(" ", paragraph).strip()
751+
new_lines = textwrap.wrap(
752+
text=paragraph,
753+
width=width,
754+
initial_indent=" " * indent,
755+
subsequent_indent=" " * sub_indent,
756+
)
757+
758+
# Blank lines get eaten by textwrap, put it back
759+
lines.extend(new_lines or [""])
760+
761+
return lines
762+
763+
684764
class ArgumentDefaultsHelpFormatter(HelpFormatter):
685765
"""Help message formatter which adds default values to argument help.
686766

Lib/test/test_argparse.py

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

42304230

4231+
class TestHelpFlexi(HelpTestCase):
4232+
"""Test the FlexiHelpFormatter"""
4233+
4234+
parser_signature = Sig(
4235+
prog='PROG', formatter_class=argparse.FlexiHelpFormatter,
4236+
description='This text should be wrapped as appropriate to keep\n'
4237+
'things nice and very, very tidy.\n'
4238+
'\n'
4239+
'Paragraphs should be preserved.\n'
4240+
' * bullet list items\n'
4241+
' should wrap to an appropriate place,\n'
4242+
' should such wrapping be required.\n'
4243+
' * short bullet\n'
4244+
)
4245+
4246+
argument_signatures = [
4247+
Sig('--foo', help=' foo help should also\n'
4248+
'appear as given here\n'
4249+
'\n'
4250+
'along with a second paragraph, if called for\n'
4251+
' * bullet'),
4252+
Sig('spam', help='spam help'),
4253+
]
4254+
argument_group_signatures = [
4255+
(Sig('title', description='short help text\n'
4256+
'\n'
4257+
'Longer help text, containing useful\n'
4258+
'contextual information for the var in\n'
4259+
'question\n'
4260+
'* and a bullet\n'),
4261+
[Sig('--bar', help='bar help')]),
4262+
]
4263+
usage = '''\
4264+
usage: PROG [-h] [--foo FOO] [--bar BAR] spam
4265+
'''
4266+
help = usage + '''\
4267+
4268+
This text should be wrapped as appropriate to keep things nice and very, very
4269+
tidy.
4270+
4271+
Paragraphs should be preserved.
4272+
* bullet list items should wrap to an appropriate place, should such
4273+
wrapping be required.
4274+
* short bullet
4275+
4276+
positional arguments:
4277+
spam spam help
4278+
4279+
optional arguments:
4280+
-h, --help show this help message and exit
4281+
--foo FOO foo help should also appear as given here
4282+
4283+
along with a second paragraph, if called for
4284+
* bullet
4285+
4286+
title:
4287+
short help text
4288+
4289+
Longer help text, containing useful contextual information for the var in
4290+
question
4291+
* and a bullet
4292+
4293+
--bar BAR bar help
4294+
'''
4295+
version = ''
4296+
4297+
42314298
class TestHelpArgumentDefaults(HelpTestCase):
42324299
"""Test the ArgumentDefaultsHelpFormatter"""
42334300

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)