Skip to content

Before and after: argparse help

argparse colourises the parts it generates (usage lines, section headers, metavars) but the text you supply stays undifferentiated. The generated frame is richer than the content it frames.

It also mangles your paragraphs. The default HelpFormatter collapses all whitespace, so a description with two paragraphs comes out as one. RawDescriptionHelpFormatter fixes that by reflowing nothing at all, which breaks on narrow terminals. There is no middle option.

Simplemark is that middle option, in about ten lines.

The formatter

smhelp.py:

import argparse

import simplemark


class SimpleMarkHelpFormatter(argparse.HelpFormatter):
    """Render descriptions, epilogs and per-option help as simplemark."""

    def _fill_text(self, text, width, indent):
        rendered = simplemark.render_text(simplemark.parse(text), width=width)
        return "\n".join(indent + line for line in rendered.split("\n"))

    def _split_lines(self, text, width):
        return simplemark.render_text(simplemark.parse(text), width=width).split("\n")

_fill_text handles description and epilog; _split_lines handles each option's help. Those are the only two hooks argparse offers, and they are enough.

A parser to compare

arc.py:

import argparse

from smhelp import SimpleMarkHelpFormatter

DESCRIPTION = """Pack and unpack archives.

Reads ``member`` names using the encoding given by ``--metadata-encoding``.
Set *strict* to fail on the first bad entry rather than skipping it."""

ENCODING_HELP = (
    "Encoding of member names, used by ``-l``, ``-e`` and ``-t``. "
    "Defaults to *utf-8* on every platform."
)


def build(formatter):
    parser = argparse.ArgumentParser(
        prog="arc", description=DESCRIPTION, formatter_class=formatter
    )
    parser.add_argument("--metadata-encoding", metavar="<encoding>", help=ENCODING_HELP)
    parser.add_argument("--strict", action="store_true", help="Fail on the first bad entry.")
    return parser


if __name__ == "__main__":
    import sys

    chosen = SimpleMarkHelpFormatter if sys.argv[1] == "after" else argparse.HelpFormatter
    build(chosen).print_help()

Before

The default formatter, on a 64-column terminal:

COLUMNS=64 python arc.py before
usage: arc [-h] [--metadata-encoding <encoding>] [--strict]

Pack and unpack archives. Reads ``member`` names using the
encoding given by ``--metadata-encoding``. Set *strict* to
fail on the first bad entry rather than skipping it.

options:
  -h, --help            show this help message and exit
  --metadata-encoding <encoding>
                        Encoding of member names, used by
                        ``-l``, ``-e`` and ``-t``. Defaults to
                        *utf-8* on every platform.
  --strict              Fail on the first bad entry.

Two things are wrong there. The blank line between the two paragraphs of the description is gone; argparse collapsed it. And the markup is showing: member, *strict*, -l are all printed literally at the user.

After

COLUMNS=64 python arc.py after
usage: arc [-h] [--metadata-encoding <encoding>] [--strict]

Pack and unpack archives.

Reads 'member' names using the encoding given by
'--metadata-encoding'. Set strict to fail on the first bad
entry rather than skipping it.

options:
  -h, --help            show this help message and exit
  --metadata-encoding <encoding>
                        Encoding of member names, used by
                        '-l', '-e' and '-t'. Defaults to utf-8
                        on every platform.
  --strict              Fail on the first bad entry.

The paragraph break is back, the markup is gone, and the option names in the help text are now visibly option names rather than words. On a colour terminal they would be bold instead of quoted; argparse passes the real terminal width, and simplemark's style="auto" picks styling when stdout is a TTY and plain text when it is a pipe, honouring NO_COLOR.

Note that the option help column is only about 40 characters wide, and the text reflowed into it correctly. _split_lines receives the available width and simplemark uses it; there is no hard-coded assumption about 80 columns anywhere.

A narrower terminal

The same command at 48 columns, where the default formatter's fixed layout would hurt most:

COLUMNS=48 python arc.py after
usage: arc [-h]
           [--metadata-encoding <encoding>]
           [--strict]

Pack and unpack archives.

Reads 'member' names using the encoding given
by '--metadata-encoding'. Set strict to fail
on the first bad entry rather than skipping
it.

options:
  -h, --help            show this help message
                        and exit
  --metadata-encoding <encoding>
                        Encoding of member
                        names, used by '-l',
                        '-e' and '-t'.
                        Defaults to utf-8 on
                        every platform.
  --strict              Fail on the first bad
                        entry.

Inline-only parsing

A one-line help= string has no block structure worth speaking of, and parse_inline skips straight to the inline layer:

python -c "
import simplemark
print(simplemark.parse_inline('Encoding used by \`\`-l\`\` and \`\`-t\`\`.'))
"
Code(text='-l')

Use it when you are formatting a fragment rather than a document: it does no dedenting, produces no blocks, and returns a flat tuple of inline nodes you can style yourself.

Next

  • Python API: parse_inline, render_text and the node types
  • Syntax: what you can write in a help string