How to preserve newlines in argparse version output while letting argparse auto-format/wrap the remaining help message?

Question:

I wrote the following code.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', action='version',
                    version='%(prog)s 1.0nCopyright (c) 2016 Lone Learner')
parser.parse_args()

This produces the following output.

$ python foo.py --version
foo.py 1.0 Copyright (c) 2016 Lone Learner

You can see that the newline is lost. I wanted the copyright notice to appear on the next line.

How can I preserve the new lines in the version output message?

I still want argparse to compute how the output of python foo.py -h should be laid out with all the auto-wrapping it does. But I want the version output to be a multiline output with the newlines intact.

Asked By: Lone Learner

||

Answers:

RawTextHelpFormatter will turn off the automatic wrapping, allowing your explicit n to appear. But it will affect all the help lines. There’s no way of picking and choosing. Either accept the default wrapping, or put explicit newlines in all of your help lines.

You are getting to a level of pickiness about the help format that you need to study the HelpFormatter code for yourself.

Answered By: hpaulj

You’d be best off using a custom argparse.Action for this.

import argparse
import os
import sys

class MultilineVersionAction(argparse.Action):
    verbose_version = '1.0nCopyright (c) 2016 Lone Learner'

    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if nargs is not None:
            raise ValueError('nargs not allowed')
        # this is how argparse initialises `prog` by default
        self.prog = os.path.basename(sys.argv[0])
        super(MultilineVersionAction, self).__init__(option_strings, dest, nargs=0, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        parser.exit(message='{} {}n'.format(self.prog, self.verbose_version))

# ...

    self.parser.add_argument('-v', '--version', action=MultilineVersionAction)
Answered By: Hamish Downer

Just don’t use action='version'. The same pattern can be repeated for -h.

import os
import sys
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', action='store_true')
args = parser.parse_args()

if args.version:
    prog = os.path.basename(__file__)
    print('{} 1.0nCopyright (c) 2016 Lone Learner'.format(prog))
    sys.exit()
Answered By: anatoly techtonik

There’s also argparse.RawDescriptionHelpFormatter.

parser=argparse.ArgumentParser(add_help=True,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description="""an
already-wrapped
description
string""")

It leaves the description and epilog alone, and wraps only argument help strings. The OP wanted the opposite.

Answered By: Jim

While I agree with Hamish that you’re best off using a custom Action, it’s possible to leverage more of the argparse machinery, and to integrate more fully with it as well. For example:

import argparse
import sys

__version__ = 1.0
__copyright__ = "Copyright Nobody <[email protected]>"

class _MyVersionAction(argparse._VersionAction):
    """Customized _VersionAction with RawDescription-formatted output."""
    def __call__(self, parser, namespace, values, option_string=None):
        version = self.version
        if version is None:           # I have no idea why this is here¹
            version = parser.version  # ← (no such thing)
        formatter = argparse.RawDescriptionHelpFormatter(
            prog=parser.prog)
        formatter.add_text(version)
        parser._print_message(formatter.format_help(), sys.stdout)
        parser.exit()

parser = argparse.ArgumentParser()
parser.register('action', 'my_version', _MyVersionAction)

parser.add_argument(
    '-V', '--version', action='my_version',
    version=f"%(prog)s {__version__}n{__copyright__}")
parser.parse_args()

This _MyVersionAction class subclasses argparse._VersionAction, and is 90% a direct copy-paste of its code, warts and all.¹ (It doesn’t even define an __init__, just inherits the parent’s.)

But it makes a minimal adjustment to the __call__ method, forcing its formatter to be argparse.RawDescriptionHelpFormatter. The original code looked up the parser-wide one with parser._get_formatter(), but we don’t want that here.

Then, after we create the parser instance, we register the action string 'my_version' for _MyVersionAction so it can be used in add_argument(..., action='my_version') like any other action.

That allows you to, for example, use %(prog)s in the version string, or consume other data from the parser instance. It’s also possible to reuse action='my_version' in multiple programs, because the version string isn’t hardcoded. You just pass it to add_argument() like always.

Output

$ python3 /tmp/argtest.py --help
usage: argtest.py [-h] [-V]

options:
  -h, --help     show this help message and exit
  -V, --version  show program's version number and exit

$ python3 /tmp/argtest.py --version
argtest.py 1.0
Copyright Nobody <[email protected]>

Notes

  1. Like the comment says, I have no idea why the action tries to get parser.version if self.version isn’t set — there’s no supported API for setting a version attribute on the parser object. You’d have to forcibly set it by hand, with parser.version = X.Y. I suspect that check is just code-cruft carried forward from an older version, or even from the optparse days.
Answered By: FeRD
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.