Argparse: Way to include default values in '--help'? - python

Suppose I have the following argparse snippet:
diags.cmdln_parser.add_argument( '--scan-time',
action = 'store',
nargs = '?',
type = int,
default = 5,
help = "Wait SCAN-TIME seconds between status checks.")
Currently, --help returns:
usage: connection_check.py [-h]
[--version] [--scan-time [SCAN_TIME]]
Test the reliability/uptime of a connection.
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
I would prefer something like:
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
(Default = 5)
Peeking at the help formatter code revealed limited options. Is there a clever way to get argparse to print the default value for --scan-time in a similar fashion, or should I just subclass the help formatter?

Use the argparse.ArgumentDefaultsHelpFormatter formatter:
parser = argparse.ArgumentParser(
# ... other options ...
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
To quote the documentation:
The other formatter class available, ArgumentDefaultsHelpFormatter, will add information about the default value of each of the arguments.
Note that this only applies to arguments that have help text defined; with no help value for an argument, there is no help message to add information about the default value to.
The exact output for your scan-time option then becomes:
--scan-time [SCAN_TIME]
Wait SCAN-TIME seconds between status checks.
(default: 5)

Add '%(default)s' to the help parameter to control what is displayed.
parser.add_argument("--type", default="toto", choices=["toto","titi"],
help = "type (default: %(default)s)")
Notes:
It is %+ default in parenthesis + format characters (not to be confused with curly brackets {default} we find in format or f-string)
Don't forget to add the "specifier character" for the type representation at the end (i.e. s for strings, d for integers, f for floats, etc.)
You can also add the usual "printf" format specifiers (like number of digits for floats, leading zeros, etc.)
You can refer to printf documentation for more details.

Wrapper class
This is the most reliable and DRY approach I've found so far to both show defaults and use another formatter such as argparse.RawTextHelpFormatter at the same time:
#!/usr/bin/env python3
import argparse
class ArgumentParserWithDefaults(argparse.ArgumentParser):
def add_argument(self, *args, help=None, default=None, **kwargs):
if help is not None:
kwargs['help'] = help
if default is not None and args[0] != '-h':
kwargs['default'] = default
if help is not None:
kwargs['help'] += ' Default: {}'.format(default)
super().add_argument(*args, **kwargs)
parser = ArgumentParserWithDefaults(
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument('-a', default=13, help='''my help
for a''')
parser.add_argument('-b', default=42, help='''my help
for b''')
parser.add_argument('--no-default', help='''my help
for no-default''')
parser.add_argument('--no-help', default=101)
parser.print_help()
print()
print(parser.parse_args())
Output:
usage: main.py [-h] [-a A] [-b B] [--no-default NO_DEFAULT]
[--no-help NO_HELP]
optional arguments:
-h, --help show this help message and exit
-a A my help
for a Default: 13
-b B my help
for b Default: 42
--no-default NO_DEFAULT
my help
for no-default
--no-help NO_HELP
Namespace(a=13, b=42, no_default=None, no_help=101)
ArgumentDefaultsHelpFormatter + RawTextHelpFormatter multiple inheritance
Multiple inheritance just works, but it does not seem to be public API:
#!/usr/bin/env python3
import argparse
class RawTextArgumentDefaultsHelpFormatter(
argparse.ArgumentDefaultsHelpFormatter,
argparse.RawTextHelpFormatter
):
pass
parser = argparse.ArgumentParser(
formatter_class=RawTextArgumentDefaultsHelpFormatter
)
parser.add_argument('-a', default=13, help='''my help
for a''')
parser.add_argument('-b', default=42, help='''my help
for b''')
parser.print_help()
Output:
usage: a.py [-h] [-a A] [-b B]
optional arguments:
-h, --help show this help message and exit
-a A my help
for a (default: 13)
-b B my help
for b (default: 42)
It just works works because as we can see trivially from the sources https://github.com/python/cpython/blob/v3.6.5/Lib/argparse.py#L648 that:
RawTextHelpFormatter implements _split_lines
ArgumentDefaultsHelpFormatter implements _get_help_string
so we can guess that they will work together just fine.
However, this does not seem to be public API, and neither are the methods of formatter_class, so I don't think there is a public API way to do it currently. argparse docstring says:
All other classes in this module are considered implementation details.
(Also note that HelpFormatter and RawDescriptionHelpFormatter are only
considered public as object names -- the API of the formatter objects is
still considered an implementation detail.)
See also: Customize argparse help message
Tested on Python 3.6.5.

It is often useful to be able to automatically include the default values in the help output, but only those that were explicitly specified (with default=..). The methods already mentioned have some shortcomings in this respect:
The ArgumentDefaultsHelpFormatter method prints out (default: None) for every argument whose default was not explicitly specified, and (default: False) for 'flags' (action='store_true'). This clutters the help output. To avoid it, default=argparse.SUPPRESS needs to be manually added for each such argument.
The '%(default)s' method requires manually adding it to all the arguments' help strings that we do want printed in help.
Both methods end up needing manual intervention to print out only the "right" defaults.
One way to do this automatically is to augment the ArgumentDefaultsHelpFormatter to ignore the Nones and Falses default values:
class ExplicitDefaultsHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
def _get_help_string(self, action):
if action.default in (None, False):
return action.help
return super()._get_help_string(action)
Use it in place of ArgumentDefaultsHelpFormatter:
parser = argparse.ArgumentParser(
formatter_class=ExplicitDefaultsHelpFormatter
)
This will print only the explicitly set default values in the help output.
Note: if an argument's default was explicitly set as None or False, it won't be shown in help with this class; add %(default)s string to help for that argument if you want it in the help output.

Related

Python - argparse - (Singleton) Argument w/ optional parameter [duplicate]

I'm trying to implement the following argument dependency using the argparse module:
./prog [-h | [-v schema] file]
meaning the user must pass either -h or a file, if a file is passed the user can optionally pass -v schema.
That's what I have now but that doesn't seem to be working:
import argparse
parser = argparse.ArgumentParser()
mtx = parser.add_mutually_exclusive_group()
mtx.add_argument('-h', ...)
grp = mtx.add_argument_group()
grp.add_argument('-v', ...)
grp.add_argument('file', ...)
args = parser.parse_args()
It looks like you can't add an arg group to a mutex group or am I missing something?
If -h means the default help, then this is all you need (this help is already exclusive)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file')
parser.add_argument('-s','--schema')
parser.parse_args('-h'.split()) # parser.print_help()
producing
usage: stack23951543.py [-h] [-s SCHEMA] file
...
If by -h you mean some other action, lets rename it -x. This would come close to what you describe
parser = argparse.ArgumentParser()
parser.add_argument('-s','--schema', default='meaningful default value')
mxg = parser.add_mutually_exclusive_group(required=True)
mxg.add_argument('-x','--xxx', action='store_true')
mxg.add_argument('file', nargs='?')
parser.parse_args('-h'.split())
usage is:
usage: stack23951543.py [-h] [-s SCHEMA] (-x | file)
Now -x or file is required (but not both). -s is optional in either case, but with a meaningful default, it doesn't matter if it is omitted. And if -x is given, you can just ignore the -s value.
If necessary you could test args after parsing, to confirm that if args.file is not None, then args.schema can't be either.
Earlier I wrote (maybe over thinking the question):
An argument_group cannot be added to a mutually_exclusive_group. The two kinds of groups have different purposes and functions. There are previous SO discussions of this (see 'related'), as well as a couple of relevant Python bug issues. If you want tests that go beyond a simple mutually exclusive group, you probably should do your own testing after parse_args. That may also require your own usage line.
An argument_group is just a means of grouping and labeling arguments in the help section.
A mutually_exclusive_group affects the usage formatting (if it can), and also runs tests during parse_args. The use of 'group' for both implies that they are more connected than they really are.
http://bugs.python.org/issue11588 asks for nested groups, and the ability to test for 'inclusivity' as well. I tried to make the case that 'groups' aren't general enough to express all the kinds of testing that users want. But it's one thing to generalize the testing mechanism, and quite another to come up with an intuitive API. Questions like this suggest that argparse does need some sort of 'nested group' syntax.

Python argparse proper formatting of choice arguments with flag

I'm trying to create well formatted help messages for 'choice' type command line arguments with Python's argparse. For the command I allow the name '--operation' and the alias '-o'. Currently, argparse is printing the list of options next to both in the help message.
Please note that this question is different to the question of formatting the help messages of the options (That problem has a good answer here by Anthon: Python argparse: How to insert newline in the help text?)
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('-o', '--operation', help="operation to perform", type=str, choices=["create", "update", "delete"])
_StoreAction(option_strings=['-o', '--operation'], dest='operation', nargs=None, const=None, default=None, type=<class 'str'>, choices=['create', 'update', 'delete'], help='operation to perform', metavar=None)
>>> parser.print_help()
usage: [-h] [-o {create,update,delete}]
optional arguments:
-h, --help show this help message and exit
-o {create,update,delete}, --operation {create,update,delete}
operation to perform
>>>
My problem is this line:
-o {create,update,delete}, --operation {create,update,delete}
It's very clunky how the list of options is repeated twice. Especially since I will have lists that are even longer. It would be better I think to have this:
-o, --operation {create,update,delete}
This is assuming of course that there isn't some POSIX rule about how this has to work. I don't think there is.
How can I achieve the desired output? Or is there a good reason that I shouldn't be trying to?
This is quite a hack, but there doesn't appear to be a good place to hook into this.
Define your own formatter, which overrides (by basically copying) the _format_action_invocation method. The only change you'll make is to add the choices only to the last option string.
class MyHelpFormatter(HelpFormatter):
def _format_action_invocation(self, action):
if not action.option_strings:
default = self._get_default_metavar_for_positional(action)
metavar, = self._metavar_formatter(action, default)(1)
return metavar
else:
parts = []
# if the Optional doesn't take a value, format is:
# -s, --long
if action.nargs == 0:
parts.extend(action.option_strings)
# if the Optional takes a value, format is:
# -s ARGS, --long ARGS
else:
default = self._get_default_metavar_for_optional(action)
args_string = self._format_args(action, default)
for option_string in action.option_strings[:-1]:
parts.append('%s' % (option_string,))
parts.append('%s %s' % (action.option_strings[-1], args_string)
return ', '.join(parts)

Need to embed `-` character into arguments in python argparse

I am designing a tool to meet some spec. I have a scenario where I want the argument to contain - its string. Pay attention to arg-1 in the below line.
python test.py --arg-1 arg1Data
I am using the argparse library on python27. For some reason the argparse gets confused with the above trial.
My question is how to avoid this? How can I keep the - in my argument?
A sample program (containing the -, if this is removed everything works fine):
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--arg-1", help="increase output verbosity")
args = parser.parse_args()
if args.args-1:
print "verbosity turned on"
Python argparse module replace dashes by underscores, thus:
if args.arg_1:
print "verbosity turned on"
Python doc (second paragraph of section 15.4.3.11. dest) states:
Any internal - characters will be converted to _ characters to make
sure the string is a valid attribute name.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--arg-1", help="increase output verbosity")
parser.add_argument("arg-2")
args = parser.parse_args()
print(args)
produces:
1750:~/mypy$ python stack34970533.py -h
usage: stack34970533.py [-h] [--arg-1 ARG_1] arg-2
positional arguments:
arg-2
optional arguments:
-h, --help show this help message and exit
--arg-1 ARG_1 increase output verbosity
and
1751:~/mypy$ python stack34970533.py --arg-1 xxx yyy
Namespace(arg-2='yyy', arg_1='xxx')
The first argument is an optional. You can use '--arg-1' in commandline, but the value is stored as args.arg_1. Python would interpret args.arg-1 as args.arg - 1. There's a long history of unix commandlines allowing flags with a -. It tries to balance both traditions.
It leaves you in full control of the positionals dest attribute, and does not change the - to _. If you want to access that you have to use the getattr approach. There is bug/issue discussing whether this behavior should be changed or not. But for now, if you want to make it hard on yourself, that's your business.
Internally, argparse accesses the namespace with getattr and setattr to minimize restrictions on the attribute names.

python argparse print usage text after description

Is there a way to print usage text after the description text with python argparse? I have my cmd line argparse working, but i would like to print version info before usage info.
Edit:
version: 1.0
usage: blahcmd [-h] [-help]
some lovely help
The argparse module does not provide any option to add a "prolog". When the help is displayed it always start with usage:. The best you can do is to customize the usage text adding the version number, using the usage parameter when you instantiate the ArgumentParser:
import argparse
parser = argparse.ArgumentParser(usage='Any text you want\n')
Note that the help will still start with usage:.
A dirty workaround that might work is to start the usage message with a \r:
>>> import argparse
>>> usage = '\r{}\nusage: %(prog)s etc.'.format('Version a b'.ljust(len('usage:')))
>>> parser = argparse.ArgumentParser(usage=usage)
>>> parser.parse_args(['-h'])
Version a b
usage: etc.
optional arguments:
-h, --help show this help message and exit
I don't think that this usage of \r is portable. There are probably some terminals where this trick doesn't work. I've ljusted the version string to make sure that when the trick works, the whole usage: string disappears from string and you don't get things like v1.2e: when using short version strings.
Note: you must manually create the whole usage text now.
Here's an ugly hack (see my comment on the original question):
Define your own subclass of HelpFormatter to pass to the parser with the formatter_class option. The subclass should probably override the _format_usage method. This isn't entirely recommended, since the interface for defining your own formatting class was never made public.
from argparse import ArgumentParser, HelpFormatter
from gettext import gettext as _
class VersionedHelp(HelpFormatter):
def _format_usage(self, usage, actions, groups, prefix=None):
if prefix is None:
prefix = _('Version: x.y\n\nusage: ')
return HelpFormatter._format_usage(self, usage, actions, groups, prefix)
p = ArgumentParser(formatter_class=VersionedHelp)
p.parse_args()
A rough solution is to add the version text to your usage line. It's not perfect (note the extra 'usage' text), but its a start
In [64]: parser=argparse.ArgumentParser(description='description')
# 'usage' parameter just sets the 'usage' attribute
In [67]: parser.usage='version 1.0.1\n'+parser.format_usage()
In [68]: parser.print_help()
usage: version 1.0.1
usage: ipython [-h]
description
optional arguments:
-h, --help show this help message and exit
The order of components in the help are determined by the ArgumentParser.format_help method (quoting from the argparse.py file):
def format_help(self):
formatter = self._get_formatter()
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
# positionals, optionals and user-defined groups
for action_group in self._action_groups:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
# epilog
formatter.add_text(self.epilog)
# determine help from format above
return formatter.format_help()
I can imagine writing a custom method that adds your version information, e.g.
def format_help(self):
formatter = self._get_formatter()
# version info
formatter.add_text('version 1.0.1')
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
...
In ipython this function works:
In [74]: def format_help(parser):
formatter=parser._get_formatter()
formatter.add_text('version 1.0.1')
formatter.add_usage(parser.usage, parser._actions, parser._mutually_exclusive_groups)
formatter.add_text(parser.description)
return formatter.format_help()
In [75]: print format_help(parser)
version 1.0.1
usage: ipython [-h]
description

gem/git-style command line arguments in Python

Is there a Python module for doing gem/git-style command line arguments? What I mean by gem/git style is:
$ ./MyApp.py
The most commonly used MyApp commands are:
add Add file contents to the index
bisect Find by binary search the change that introduced a bug
branch List, create, or delete branches
checkout Checkout a branch or paths to the working tree
...
$ ./MyApp.py branch
* current-branch
master
With no arguments, the output tells you how you can proceed. And there is a special "help" command:
$ ./MyApp.py help branch
Which gets you deeper tips about the "branch" command.
Edit:
And by doing I mean it does the usage printing for you, exits with invalid input, runs your functions according to your CLI specification. Sort of a "URL mapper" for the command line.
Yes, argparse with add_subparsers().
It's all well explained in the Sub-commands section.
Copying one of the examples from there:
>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers()
>>> checkout = subparsers.add_parser('checkout', aliases=['co'])
>>> checkout.add_argument('foo')
>>> parser.parse_args(['checkout', 'bar'])
Namespace(foo='bar')
Edit: Unfortunately there's no self generated special help command, but you can get the verbose help message (that you seem to want) with -h or --help like one normally would after the command:
$ ./MyApp.py branch --help
By verbose I don't mean that is like a man page, it's like every other --help kind of help: listing all the arguments, etc...
Example:
>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers(description='Sub description')
>>> checkout = subparsers.add_parser('checkout', description='Checkout description')
>>> checkout.add_argument('foo', help='This is the foo help')
>>> parser.parse_args(['checkout', '--help'])
usage: checkout [-h] foo
Checkout description
positional arguments:
foo This is the foo help
optional arguments:
-h, --help show this help message and exit
If you need to, it should be easy to implement an help command that redirects to --help.
A reasonable hack to get the gem/git style "help" behavior (I just wrote this for what I'm working on anyway):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='sub_commands')
parser_branch = subparsers.add_parser('branch', description='list of branches')
parser_help = subparsers.add_parser('help')
parser_help.add_argument('command', nargs="?", default=None)
# I can't find a legitimate way to set a default subparser in the docs
# If you know of one, please let me know!
if len(sys.argv) < 2:
sys.argv.append('--help')
parsed = parser.parse_args()
if parsed.sub_commands == "help":
if not parsed.command:
parser.parse_args(['--help'])
else:
parser.parse_args([parsed.command, '--help'])
argparse is definitely a step up from optparse and other python solutions I've come across. But IMO the gem/git style of handling args is just a more logical and safer way to do things so it's annoying that it's not supported.
I wanted to do something similar to git commands, where I would load a second script based off of one of the command line options, and have that script populate more command line options, and also have the help work.
I was able to do this by disabling the help option, parse known args, add more arguments, re-enable the help option, and then parse the rest of the arguments.
This is what I came up with.
import argparse
#Note add_help=False
arg_parser = argparse.ArgumentParser(description='Add more arguments after parsing.',add_help=False)
arg_parser.add_argument('MODE', default='default',type=str, help='What commands to use')
args = arg_parser.parse_known_args()[0]
if args.MODE == 'branch':
arg_parser.add_argument('-d', '--delete', default='Delete a branch')
arg_parser.add_argument('-m', '--move', default='move a branch')
elif args.MODE == 'clone' :
arg_parser.add_argument('--local', '-l')
arg_parser.add_argument('--shared')
#Finally re-enable the help option, and reparse the arguments
arg_parser.add_argument(
'-h', '--help',
action='help', default=argparse.SUPPRESS,
help=argparse._('show this help message and exit'))
args = arg_parser.parse_args()

Categories

Resources