Python argparse help-like option - python

I am writing a python script that takes two arguments, and some options:
scriptname [-h] [-l] [-q|-d] arg1 arg2
The -q (quiet) and -d (debug) options change the verbosity level, and the -h option is the help option automatically created by argparse.
I would like the -l (list) option to behave similarly to the -h option in that it will not require that the (otherwise mandatory) arguments are present and list some useful information (different from the -h option). In practice, this means that the script could be called in the following three ways:
scriptmane [-q|-d] arg1 arg2
scriptname -l
scriptname -h
Two possible ways forward would be to:
Make the arguments optional (with nargs='?') and add code to verify that there are two arguments in all cases where there -l og -h options are not given.
Write a custom action class (not sure about the details).
But I hope there is a more straightforward way to inherit the "this option is all you need" behaviour from the help option.
Solution (based on samwyse's answer):
Based on the _HelpAction() in argparse.py:
class _ListAction(argparse.Action):
def __init__(self,
option_strings,
dest=argparse.SUPPRESS,
default=argparse.SUPPRESS,
help=None):
super(_ListAction, self).__init__(
option_strings=option_strings,
dest=dest,
default=default,
nargs=0,
help=help)
def __call__(self, parser, namespace, values, option_string=None):
print_list()
parser.exit()
and then, during parser setup:
parser.add_argument('-l', '--list', action=_ListAction,
help="List all available cases")

If the option "list" is intended to have different behavior from "help" then you need to write a custom action. The good news is that it is very simple to do this. The main page for argparse gives you hints, you only have to realize that the action is called as soon as the option is seen in the list of arguments. In the new action's call (that should have two underscores at each end but markdown uses those for emphasis) method, do whatever you need to do for your option and then call parser.exit() to short circuit the processing of any more arguments.
Look at the source for _HelpAction and _VersionAction here: https://github.com/ThomasWaldmann/argparse/blob/master/argparse.py (and probably just subclass one of them to skip writing the init code).

Related

Different levels of help verbosity in argparse

I have an application with several dozens of CLI parameters. I use python argparse to parse arguments. Most of them are rarely used, only 5 or 6 are frequent. I don't want to clutter up the output of --help for simple cases, however there still should be a possibility to see description for all parameters somehow.
It is possible to have several verbosity levels for help in argparse? Here's how I expect it to be.
$ myapp -h
optional arguments:
--foo Do foo stuff
--bar Do bar stuff
$ myapp -hh # or myapp --expert-mode -h
optional arguments:
--foo Do foo stuff
--bar Do bar stuff
expert arguments:
--use-warp-drive
--no-fasten-seatbelts
... 50 more arguments
My first idea was to create two ArgumentParser-s, the basic one and the verbose one. First, parse_args of the basic was called. If --expert flag is present, the verbose parser is created and called. The approach has two downsides. First, -h is unconditionally handled by the first parser, so I should implement my own analogous help flag. Second, expert options are not even parsed without --expert flag, and I want them to be parsed unconditionally (but not to pop up in help).
Note: the solution should be python2 compliant.
Update: using the ideas from Lior Cohen's answer and this question, I created a working example. In my case it was easier to make a whitelist of "simple" options, so it was even unnecessary to use tagged action types.
Illustrating code follows.
class ExpertHelpFormatter(argparse.HelpFormatter):
skip_expert_section = True
whitelist = set(("foo",))
def add_usage(self, usage, actions, groups, prefix=None):
if self.skip_expert_section:
actions = [action for action in actions if action.dest in self.whitelist]
ret = super(ExpertHelpFormatter, self).add_usage(usage, actions, groups, prefix)
if self.skip_expert_section:
self.add_text("Use -hh for detailed help.")
return ret
def add_argument(self, action):
if self.skip_expert_section and action.dest not in self.whitelist:
return
super(ExpertHelpFormatter, self).add_argument(action)
def main():
parser = argparse.ArgumentParser(add_help=False, formatter_class=ExpertHelpFormatter)
parser.add_argument("-h", "--help", action="count", default=0)
parser.add_argument("--foo")
parser.add_argument("--use-warp-drive", action="store_true")
args = parser.parse_args()
if args.help == 1:
print parser.format_help()
return
elif args.help > 1:
ExpertHelpFormatter.skip_expert_section = False
print parser.format_help()
return
Here is a layout that should give you what you need, though it is not "free of work".
Disable default help by add_help = False (see here). this will let you still use -h and --help with action you want.
You need to "tag" your expert arguments by type that will store somehow the fact that foo is "'simple" and use-warp-drive is "expert". You can add an attribute to the Action or save a global dict in the parser itself.
Write an Help Formatter (you can see example). Then, according to the above "tag", you can suppress the "expert" help by returning "" in case of "simple" mode.
Hope this is helping.

How do I create an optional argument that is an alias of another argument with a certain value?

I need to edit a Python 3 program, let's call it ./foo, that uses argeparse to parse its argument list.
Currently, this program allows one to specify an optional input file on the command line. If it is not specified, then foo reads the file setting.txt
$ ./foo
$ ./foo --input my_settings.txt
The code that does this pretty simple:
parser.add_argument(
'--input',
default='settings.txt'
)
The intent was to give personalized settings, but in practice, 99% of people who use the --input argument specify one particular file other_settings.txt. I'd like to add a mutually exclusive convenience argument, --common that is equivalent to the --input other_settings.txt
# These are the same.
$ ./foo --input other_settings.txt
$ $ ./foo --common
What is the recommended way to accomplish this? For example, should I have both arguments write to the same dest value? Do I have them write to separate destinations and then add some logic inside the body of the code, similar as was done here?
You can easily accomplish this by parsing the arguments, and then:
if common is not None:
input_file = "other_settings.txt"
The solution was to create a mutually exclusive argument group grp. I added two arguments to the group in this order:
The original argument
A new argument with
grp.add_argument(
'--input',
default='settings.txt'
)
grp.add_argument(
"--common",
action="store_const",
dest="input",
const="other_settings.txt",
)
This order seems to ensure that the value that is provided when neither of them are specified is settings.txt

Add some argument if and only if the parser has not any subparsers

The commandline interface that I want is something like this:
my-executable command REQUIRED_ARG
# or
my-executable command subcommand [--foo] [--bar]
So, I don't want REQUIRED_ARG to be required (and to be parsed) when the subcommand exists.
Mutually exclusive group is close to what I want, but I cannot add subparsers to it. Another approach was described here but that's not my case: I want REQUIRED_ARG not to be parsed at all in case of subcommand (and this also seems to be a hack).

How to indicate that at least one parameter is needed?

My script is accepting --full, --last and --check using ArgParse. If no option is provided, it just show the help message. But in that message, the parameters appear as optional.
usage: script.py [-h] [--full] [--last] [--check log_file]
If I use the keyword required, then the script will always expect the parameter, which is not correct.
usage: script.py [-h] --full --last --check log_file
So, how can I show something like:
usage: script.py [-h] (--full |--last |--check log_file)
Indicating that the help is optional but that at least one of those parameters is required.
On the question of customizing the usage:
The parser constructor takes a usage parameter. The immediate effect is to set an attribute:
parser = argparse.ArgumentParser( ... usage=custom_usage...)
print(parser.usage)
# should show None or the custom_usage string
Being a normal Python object attribute, you can change it after the parser was created.
usage_str = parser.format_usage()
The format_usage method ask the parser for create the usage that will be shown in the help (and error messages). If the parser.usage value is None, it formats it from the arguments. If a string, it is used as is (I think it fills in values like %(prog)s).
So you could write a usage string from scratch. Or you could set up the parser, get the current usage string, and edit that to suit your needs. Editing is most likely something you'd do during development, while testing the parser in an IDE. But it could be done on the fly.
A crude example:
In [441]: parser=argparse.ArgumentParser()
In [442]: g=parser.add_mutually_exclusive_group()
In [443]: g.add_argument('--foo')
In [444]: g.add_argument('--bar')
In [445]: ustr = parser.format_usage()
# 'usage: ipython3 [-h] [--foo FOO | --bar BAR]\n'
In [450]: parser.usage = ustr.replace('[','(').replace(']',')')
In [451]: parser.format_usage()
# 'usage: usage: ipython3 (-h) (--foo FOO | --bar BAR)\n'
I've replaced the [] with () (even on the -h :( ).
For now testing logical combinations of the args attributes is the best choice. Inside the parse_args functions the parser maintains a list (set actually) of arguments that it has seen. That is used to test for required arguments, and for mutually_exclusive_arguments, but it is not available outside that code.
For store_true (or false) arguments, just check their truth value. For others I like to test for the default None. If you use other default values, test accordingly. A nice thing about None is that the user cannot give you that value.
Perhaps the most general way to test for arguments is to count the number of attributes which are not None:
In [461]: args=argparse.Namespace(one=None, tow=2, three=None)
In [462]: ll = ['one','tow','three']
In [463]: sum([getattr(args,l,None) is not None for l in ll])
Out[463]: 1
0 means none are found; >0 at least one present; ==len(ll) all found; >1 violates mutually exclusivity; '==1' for required mutually exclusive.
As #doublep explained in his answer, if you want to use more than one option at a time:
Change the usage message manually to the one you want.
Add the following code from Python argparse: Make at least one argument required:
if not (args.full or args.last or args.check):
parse.error('[-] Error: DISPLAY_ERROR_MESSAGE')
You can use add_mutually_exclusive_group():
parser = argparse.ArgumentParser ()
group = parser.add_mutually_exclusive_group (required = True)
group.add_argument ('--foo')
group.add_argument ('--bar')
However, the main effect is that you won't be able to use more than one option at a time.

Python argparse toggle flags

Is there any way in argparse to parse flags like [+-]a,b,c,d?
foo.py +s -b
should store True in the dest of s and False in the dest of b, much like done by the Windows attrib or the Linux chmod.
Currently, I am using 2 separate arguments +s and -s with store_true and store_false, respectively. But it creates an ugly help with it listing each flag twice (+a & -a)
Another workaround would be to manually parse the extended arg with regex (which somehow seems a lot easier and use custom description, but before doing that I just wanted to look around if there was anything using which I could perform the same thing using argparse itself.
You can do this by passing both -s and +s to a single add_argument call, and using a custom action:
class ToggleAction(argparse.Action):
def __call__(self, parser, ns, values, option):
setattr(ns, self.dest, bool("-+".index(option[0])))
ap = ArgumentParser(prefix_chars='-+')
ap.add_argument('-s', '+s', action=ToggleAction, nargs=0)
ap.parse_args(['+s'])
Namespace(s=True)
ap.parse_args(['-s'])
Namespace(s=False)

Categories