Customize argparse help message
Instead of relying on internal API (which is subject to change without notice), here is an alternative using public API only. It is arguably more complex but in turn gives you maximum control over what is printed:
class ArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super(ArgumentParser, self).__init__(*args, **kwargs)
self.program = { key: kwargs[key] for key in kwargs }
self.options = []
def add_argument(self, *args, **kwargs):
super(ArgumentParser, self).add_argument(*args, **kwargs)
option = {}
option["flags"] = [ item for item in args ]
for key in kwargs:
option[key] = kwargs[key]
self.options.append(option)
def print_help(self):
# Use data stored in self.program/self.options to produce
# custom help text
How it works:
- tap into constructor of
argparse.ArgumentParser
to capture and store program info (e.g. description, usage) inself.program
- tap into
argparse.ArgumentParser.add_argument()
to capture and store added arguments (e.g. flags, help, defaults) inself.options
- redefine
argparse.ArgumentParser.print_help()
and use previously stored program info / arguments to produce help text
Here is a full example covering some common use cases. Note that this is by no means complete (e.g. there is no support for positional arguments or options with more than one argument), but it should provide a good impression of what is possible:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import argparse
import textwrap
class ArgumentParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super(ArgumentParser, self).__init__(*args, **kwargs)
self.program = { key: kwargs[key] for key in kwargs }
self.options = []
def add_argument(self, *args, **kwargs):
super(ArgumentParser, self).add_argument(*args, **kwargs)
option = {}
option["flags"] = [ item for item in args ]
for key in kwargs:
option[key] = kwargs[key]
self.options.append(option)
def print_help(self):
wrapper = textwrap.TextWrapper(width=80)
# Print usage
if "usage" in self.program:
print("Usage: %s" % self.program["usage"])
else:
usage = []
for option in self.options:
usage += [ "[%s %s]" % (item, option["metavar"]) if "metavar" in option else "[%s %s]" % (item, option["dest"].upper()) if "dest" in option else "[%s]" % item for item in option["flags"] ]
wrapper.initial_indent = "Usage: %s " % os.path.basename(sys.argv[0])
wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
output = str.join(" ", usage)
output = wrapper.fill(output)
print(output)
print()
# Print description
if "description" in self.program:
print(self.program["description"])
print()
# Print options
print("Options:")
maxlen = 0
for option in self.options:
option["flags2"] = str.join(", ", [ "%s %s" % (item, option["metavar"]) if "metavar" in option else "%s %s" % (item, option["dest"].upper()) if "dest" in option else item for item in option["flags"] ])
if len(option["flags2"]) > maxlen:
maxlen = len(option["flags2"])
for option in self.options:
template = " %-" + str(maxlen) + "s "
wrapper.initial_indent = template % option["flags2"]
wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
if "help" in option and "default" in option:
output = option["help"]
output += " (default: '%s')" % option["default"] if isinstance(option["default"], str) else " (default: %s)" % str(option["default"])
output = wrapper.fill(output)
elif "help" in option:
output = option["help"]
output = wrapper.fill(output)
elif "default" in option:
output = "Default: '%s'" % option["default"] if isinstance(option["default"], str) else "Default: %s" % str(option["default"])
output = wrapper.fill(output)
else:
output = wrapper.initial_indent
print(output)
# Main
if (__name__ == "__main__"):
#parser = argparse.ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
#parser = argparse.ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
#parser = ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
parser = ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
parser.add_argument("-c", "--config-file", action="store", dest="config_file", metavar="file", type=str, default="config.ini")
parser.add_argument("-d", "--database-file", action="store", dest="database_file", metavar="file", type=str, help="SQLite3 database file to read/write", default="database.db")
parser.add_argument("-l", "--log-file", action="store", dest="log_file", metavar="file", type=str, help="File to write log to", default="debug.log")
parser.add_argument("-f", "--data-file", action="store", dest="data_file", metavar="file", type=str, help="Data file to read", default="data.bin")
parser.add_argument("-t", "--threads", action="store", dest="threads", type=int, help="Number of threads to spawn", default=3)
parser.add_argument("-p", "--port", action="store", dest="port", type=int, help="TCP port to listen on for access to the web interface", default="12345")
parser.add_argument("--max-downloads", action="store", dest="max_downloads", metavar="value", type=int, help="Maximum number of concurrent downloads", default=5)
parser.add_argument("--download-timeout", action="store", dest="download_timeout", metavar="value", type=int, help="Download timeout in seconds", default=120)
parser.add_argument("--max-requests", action="store", dest="max_requests", metavar="value", type=int, help="Maximum number of concurrent requests", default=10)
parser.add_argument("--request-timeout", action="store", dest="request_timeout", metavar="value", type=int, help="Request timeout in seconds", default=60)
parser.add_argument("--main-interval", action="store", dest="main_interval", metavar="value", type=int, help="Main loop interval in seconds", default=60)
parser.add_argument("--thread-interval", action="store", dest="thread_interval", metavar="value", type=int, help="Thread loop interval in milliseconds", default=500)
parser.add_argument("--console-output", action="store", dest="console_output", metavar="value", type=str.lower, choices=["stdout", "stderr"], help="Output to use for console", default="stdout")
parser.add_argument("--console-level", action="store", dest="console_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for console", default="info")
parser.add_argument("--logfile-level", action="store", dest="logfile_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for log file", default="info")
parser.add_argument("--console-color", action="store", dest="console_color", metavar="value", type=bool, help="Colorized console output", default=True)
parser.add_argument("--logfile-color", action="store", dest="logfile_color", metavar="value", type=bool, help="Colorized log file output", default=False)
parser.add_argument("--log-template", action="store", dest="log_template", metavar="value", type=str, help="Template to use for log lines", default="[%(created)d] [%(threadName)s] [%(levelname)s] %(message)s")
parser.add_argument("-h", "--help", action="help", help="Display this message")
args = parser.parse_args(["-h"])
Produced output:
Usage: argparse_custom_usage.py [-c file] [--config-file file] [-d file]
[--database-file file] [-l file] [--log-file
file] [-f file] [--data-file file] [-t THREADS]
[--threads THREADS] [-p PORT] [--port PORT]
[--max-downloads value] [--download-timeout
value] [--max-requests value] [--request-timeout
value] [--main-interval value] [--thread-
interval value] [--console-output value]
[--console-level value] [--logfile-level value]
[--console-color value] [--logfile-color value]
[--log-template value] [-h] [--help]
Download program based on some library.
Options:
-c file, --config-file file Default: 'config.ini'
-d file, --database-file file SQLite3 database file to read/write (default:
'database.db')
-l file, --log-file file File to write log to (default: 'debug.log')
-f file, --data-file file Data file to read (default: 'data.bin')
-t THREADS, --threads THREADS Number of threads to spawn (default: 3)
-p PORT, --port PORT TCP port to listen on for access to the web
interface (default: '12345')
--max-downloads value Maximum number of concurrent downloads
(default: 5)
--download-timeout value Download timeout in seconds (default: 120)
--max-requests value Maximum number of concurrent requests (default:
10)
--request-timeout value Request timeout in seconds (default: 60)
--main-interval value Main loop interval in seconds (default: 60)
--thread-interval value Thread loop interval in milliseconds (default:
500)
--console-output value Output to use for console (default: 'stdout')
--console-level value Log level to use for console (default: 'info')
--logfile-level value Log level to use for log file (default: 'info')
--console-color value Colorized console output (default: True)
--logfile-color value Colorized log file output (default: False)
--log-template value Template to use for log lines (default:
'[%(created)d] [%(threadName)s] [%(levelname)s]
%(message)s')
-h, --help Display this message
EDIT:
If have since extended the example significantly and will continue to do so on GitHub.
First of all: capitalising those phrases flies in the face of convention, and argparse
isn't really tooled to help you change these strings easily. You have three different classes of strings here: boilerplate text from the help formatter, section titles, and help text per specific option. All these strings are localisable; you could just provide a 'capitalised' translation for all of these strings via the gettext()
module support. That said, you can reach in and replace all these strings if you are determined enough and read the source code a little.
The version
action includes a default help
text, but you can supply your own by setting the help
argument. The same applies to the help
action; if you set the add_help
argument to False
you can add that action manually:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-v', '--version', action='version',
version='%(prog)s 1.0', help="Show program's version number and exit.")
parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
help='Show this help message and exit.')
Next, the optional arguments
message is a group title; each parser has two default groups, one for positional arguments, the other for optional. You can reach these by the attributes _positionals
and _optionals
, both of which have a title
attribute:
parser._positionals.title = 'Positional arguments'
parser._optionals.title = 'Optional arguments'
Be warned, by accessing names starting with an underscore you are venturing into the undocumented private API of the module, and your code may break in future updates.
Finally, to change the usage
string, you'll have to subclass the help formatter; pass the subclass in as the formatter_class
argument:
class CapitalisedHelpFormatter(argparse.HelpFormatter):
def add_usage(self, usage, actions, groups, prefix=None):
if prefix is None:
prefix = 'Usage: '
return super(CapitalisedHelpFormatter, self).add_usage(
usage, actions, groups, prefix)
parser = argparse.ArgumentParser(formatter_class=CapitalisedHelpFormatter)
Demo, putting these all together:
>>> import argparse
>>> class CapitalisedHelpFormatter(argparse.HelpFormatter):
... def add_usage(self, usage, actions, groups, prefix=None):
... if prefix is None:
... prefix = 'Usage: '
... return super(CapitalisedHelpFormatter, self).add_usage(
... usage, actions, groups, prefix)
...
>>> parser = argparse.ArgumentParser(add_help=False, formatter_class=CapitalisedHelpFormatter)
>>> parser._positionals.title = 'Positional arguments'
>>> parser._optionals.title = 'Optional arguments'
>>> parser.add_argument('-v', '--version', action='version',
... version='%(prog)s 1.0', help="Show program's version number and exit.")
_VersionAction(option_strings=['-v', '--version'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help="Show program's version number and exit.", metavar=None)
>>> parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
... help='Show this help message and exit.')
_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='Show this help message and exit.', metavar=None)
>>> print(parser.format_help())
Usage: [-v] [-h]
Optional arguments:
-v, --version Show program's version number and exit.
-h, --help Show this help message and exit.
Martijn has give a couple of the fixes that came to mind - the providing the help
parameter, and a custom Formatter class.
One other partial fix is to modify the help string after the argument is created. add_argument
creates and returns an Action
object that contains the parameters and defaults. You can save a link to this, and modify the Action
. You can also get a list of those actions, and act on that.
Let me illustrate, for a simple parser with the default help and one other argument, the action list is:
In [1064]: parser._actions
Out[1064]:
[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None),
_StoreAction(option_strings=['-f', '--foo'], dest='foo', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)]
I can view and modify the help
attribute of any of these:
In [1065]: parser._actions[0].help
Out[1065]: 'show this help message and exit'
In [1066]: parser._actions[0].help='Show this help message and exit.'
producing this help:
In [1067]: parser.parse_args(['-h'])
usage: ipython3 [-h] [-f FOO]
optional arguments:
-h, --help Show this help message and exit.
-f FOO, --foo FOO
Using the parser._actions
list uses a 'private' attribute, which some people consider unwise. But in Python that public/private distinction is not tight, and can be broken with care. Martijn is doing that by accessing the parser._positionals.title
.
Another way to change that group title is with custom argument groups
ogroup=parser.add_argument_group('Correct Optionals Title')
ogroup.add_argument('-v',...)
ogroup.add_argument('-h',...)