Argparse: Check if any arguments have been passed - python

My script should start a demo mode, when the no parameters are given. I tried this:
args = parser.parse_args()
if len(args) == 0:
run_demo()
else:
# evaluate args
Which gives a *** TypeError: object of type 'Namespace' has no len() as args is no list.
How would I achieve what I want?

If your goal is to detect when no argument has been given to the command, then doing this via argparse is the wrong approach (as Ben has nicely pointed out).
Think simple! :-) I believe that argparse does not depopulate sys.argv. So, if not len(sys.argv) > 1, then no argument has been provided by the user.

argparse lets you set (inside a Namespace object) all the variables mentioned in the arguments you added to the parser, based on your specification and the command line being parsed. If you set a default, then those variables will have that default value if they weren't seen on the command line, they won't be absent from the Namespace object. And if you don't specify a default, then there is an implicit default of None. So checking the length of the Namespace object, however you manage to do it, doesn't make sense as a way to check whether any arguments were parsed; it should always have the same length.
Instead, if you know you've got a bunch of arguments with no defaults and you want to check whether any of them were set to any non-None value... do that. You can use a list comprehension and the vars function to loop over them without having to duplicate the list of names from the add_argument calls, as shown in Martijn's answer.
It gets a little trickier if some of your arguments have default values, and more so if they have default values that could be explicitly provided on the command line (e.g. a numeric argument that defaults to 0 makes it impossible to tell the default from the user providing 0). In that case I'm not sure that there's a general solution that always works without knowledge of what the arguments are.

Don't use argparse. Instead just use sys.argv. argparse creates a Namespace, so it will always give you a "dict" with their values, depending on what arguments you used when you called the script.
Here's what I've done in the past:
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
sys.exit()
return args

If one really needs the argument number (for whatever reason).
I have found this code very helpful (but do not know how much optimised it is, and I'd appreciate any comment on it).
args = parser.parse_args()
print( len( vars(args) ) )
This version counts only the -xx parameters and not any additional value passed.
If one wants everything (also the values passed), then just use len(sys.argv) as previously mentioned.

I know it's an old thread but I found a more direct solution that might be useful for others as well:
You can check if any arguments have been passed:
if any(vars(args).values()):
# evaluate args
Or, if no arguments have been passed(note the not operator):
if not any(vars(args).values()):
run_demo()
Explanation:
parse_args() returns a "Namespace" object containing every argument name and their associated value.
Example: Namespace(arg1='myfile.txt', arg2='some/path/to/some/folder')
If no arguments have been passed, parse_args() will return the same object but with all the values as None.
Example: Namespace(arg1=None, arg2=None)
This object is not iterable, though, so you have to use vars() to turn it into a dict so we can access the values.
Finally, as we now have a dict on hands, we can get all the values(in a list), with .values(), and use the built-in any() function to check if any of the values is not None.
To make it clearer: any() returns False if there isn't a single value that is not None, False or 0(check the docs for reference) in the list you've fed to it.
Hope it helps.

Let us assume the following example to extend Yours for completeness:
#!/usr/bin/env python3
import argparse
...
def main():
parser = argparse.ArgumentParser()
parser.add_argument('input', nargs='?' action='store')
parser.add_argument('-l', '--length', type=int, action='store')
parser.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args()
if (args.input == None and args.length == None):
parser.print_help()
else:
print(args)
if __name__ == '__main__':
main()
Your Namespace object, mentioned by #Ben, in this example is args. From the strings in parser.add_argument a variable is created. You can access it through args.input or args.length or args.verbose. You can verify this by executing print(args) which will actually show something like this:
Namespace(input=None, length=None, verbose=False)
since verbose is set to True, if present and input and length are just variables, which don't have to be instantiated (no arguments provided).
Also helpful can be group = parser.add_mutually_exclusive_group() if you want to ensure, two attributes cannot be provided simultaneously.
For further reference, please refer to:
https://docs.python.org/3/howto/argparse.html
Well structured HOWTO
https://docs.python.org/3/library/argparse.html
Argparse module documentation

I expanded 2dvisio's concept to count non zero or None arguments:
vm_opts = parser.parse_args()
v = vars(vm_opts)
n_args = sum([ 1 for a in v.values( ) if a])

For the simplest case where you want to check whether a single type of argument that is the same among all the inputs has been passed, you can do it in three steps with argparse and numpy.
import argparse
import numpy as np
args = parser.parse_args()
# namespace to dictionary
args_dict = vars(args)
# unpack values from dictionary, pass to array
values = np.array([*args_dict.values()])
# Check if the defaults have changed
args_indices = np.where(values != default)[0]
# Did we pass any arguments?
if len(values) == len(args_indices):
print("No arguments were passed")
The length is used as a proxy to check if any or no arguments have been passed. If you want to know which one has been passed you'd unpack the keys and check the changed index.
np.array() accepts logical operators for more complex cases.

Related

how to perform automatic type conversion in dynamic argument parsing

Most of my python codes I send a (sometimes long!) list of arguments to, which I know how to parse using getopt. All well and good so far...
However I find it tiresome to type out a long if-then-else structure to pass all the arguments to a list of variables (usually of the same name as the passed argument), so I wanted to write a function that simply dynamically accepts arguments and places them in dynamical variable names, which seemed easiest to do using a dictionary, so that the pre-defined dictionary provided the default possible option list as well as default argument values.
I did this in the following way:
import getopt, sys
def get_args(runpars):
"""routine to dynamically accept arguments"""
try:
opts, args = getopt.getopt(sys.argv[1:],"h",[key+"=" for key in runpars])
except getopt.GetoptError:
print ('Use these arguments ',runpars)
sys.exit(2)
for opt, arg in opts:
runpars[opt[2:]]=arg
return(runpars)
if __name__ == "__main__":
# default argument values:
runpars={"a":1,"animal":"cat","f":3.3}
runpars=get_args(runpars)
print(runpars)
This essentially works:
If I pass an argument not in the dictionary, it bombs as intended with the key list:
getopts_test.py --a=4000 --animal="dog" --c=2.1 --dfrfrfr=23
Use these arguments {'a': 1, 'animal': 'cat', 'f': 3.3}
If I pass some arguments it correctly overrides my options as desired
getopts_test.py --a=4000 --animal="dog"
{'a': '4000', 'animal': 'dog', 'f': 3.3}
BUT! as arguments are all passed as strings, my type has now changed to a string... In my old fashioned clunky if-then-else manually parsing, I of course would convert each argument manually with e.g.
param=int(arg)
etc etc, but now I can't do this. So my questions is, is there a way somehow of testing the original dictionary key type and using that to convert the argument from a string.
I essentially want to find some "as_type" method, like
runpars[opt[2:]]=arg.as_type(original key this opt matched)
You found a really neat way of solving this using argparse. You can take advantage of the Namespace object which can be passed to the parse_args() method to better handle the defaults. The idea is simple: instead of creating a new namespace object from the passed arguments and then augmenting it with the defaults, simply pre-create a namespace object from the defaults, and have argparse update it with the passed arguments.
This will reduce your code a bit:
def get_args(defaultpars):
parser = argparse.ArgumentParser(description='Dynamic arguments')
namespace = argparse.Namespace(**defaultpars)
# add each key of the default dictionary as an argument expecting the same type
for key, val in defaultpars.items():
parser.add_argument('--'+key, type=type(val))
parser.parse_args(namespace=namespace)
return vars(namespace)
Thanks to #Wolfgang Kuehn for his pointer to argparse. That has the useful "type" option in add_argument, which solved my problem!
So here is my new solution written from scratch using argparse:
import argparse
def get_args(defaultpars):
parser = argparse.ArgumentParser(description='Dynamic arguments')
# add each key of the default dictionary as an argument expecting the same type
for key,val in defaultpars.items():
parser.add_argument('--'+key,type=type(val))
newpars=vars(parser.parse_args())
# Missing arguments=None need to be overwritten with default value
for key,val in newpars.items():
if val==None:
newpars[key]=defaultpars[key]
return(newpars)
if __name__ == "__main__":
# default values:
runpars={"a":1,"animal":"cat","f":3.3}
runpars=get_args(runpars)
print(runpars)
giving the correct types in the resulting dictionary:
getopts_test.py --a=4000 --animal="dog"
{'a':4000, 'animal': 'dog', 'f': 3.3}

Python: explicitly use default arguments

Under normal circumstances one calls a function with its default arguments by omitting those arguments. However if I'm generating arguments on the fly, omitting one isn't always easy or elegant. Is there a way to use a function's default argument explicitly? That is, to pass an argument which points back to the default argument.
So something like this except with ~use default~ replaced with something intelligent.
def function(arg='default'):
print(arg)
arg_list= ['not_default', ~use default~ ]
for arg in arg_list:
function(arg=arg)
# output:
# not_default
# default
I don't know if it's even possible and given the term "default argument" all my searches come up with is coders first tutorial. If this functionality is not supported that's ok too, I'd just like to know.
Unfortunately there is no such feature in Python. There are hackarounds, but they're not very likable.
The simple and popular pattern is to move the default into the function body:
def function(arg=None):
if arg is None:
arg = 'default'
...
Now you can either omit the argument or pass arg=None explicitly to take on the default value.
There is no general purpose way to omit an argument; you can specialize to particular functions by explicitly passing the appropriate default value, but otherwise, your only option is to fail to pass the argument.
The closest you could come is to replace your individual values with tuples or dicts that omit the relevant argument, then unpack them at call time. So for your example, you'd change arglist's definition to:
arg_list = [{'arg': 'not_default'}, {}]
then use it like so:
for arg in arg_list:
function(**arg)
A slightly uglier approach is to use a sentinel when you don't want to pass the argument, use that in your arg_list, and test for it, e.g.:
USEDEFAULT = object()
arg_list = ['not_default', USEDEFAULT]
for arg in arg_list:
if arg is USEDEFAULT:
function()
else:
function(arg=arg)
Obviously a bit less clean, but possibly more appropriate for specific use cases.

How can I most efficiently parse these arguments in python?

So I'm trying to come up with a strategy using the argparse library.
Here's how I want to interface with my program:
$ program list [<number>]
$ program check <name>
$ program watch <name> [<quality>]
Right now I have an argument parser like the following:
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('list')
group.add_argument('check')
group.add_argument('watch')
But how can I add an optional argument, say an integer, to an existing argument?
Meaning the user could invoke the list command in the following ways:
program list
Where the list action would be called with a default value, or:
program list 10
Where the list action would be called with an argument of 10.
I saw the subcommands option in the documentation, but I ran into the problem where I would have a sub parser for list arguments, but then I would have to add a flag, such as -n and then provide the number. Perhaps this is a better way of doing it? But I like the idea of just providing the number if you want to, or omitting it if you don't.
Is what I'm trying to achieve good practice? Is it possible with argparse?
This sample set me off in the wrong direction. I've sketched a subparser implementation at the end that I think does the trick.
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument('list')
group.add_argument('check')
group.add_argument('watch')
It expects 3 strings, and will assign them to the 3 attributes.
However, you cannot put 3 'positional` arguments in a mutually exclusive group. One optional positional (yes the terminology is confusing) can be in such a group, but the rest must 'optionals' (flagged).
Going back to your initial list. Are these different patterns that you'd like to accept
program list [integer]
program check name
program watch name [quality]
where 'list','check','watch' are literal strings, while 'integer','name','quality' are variable names.
If that is the case, then subparsers is probably the best choice. nargs='?' can be used to make positional arguments 'optional'.
parser = argparse.ArgumentParser()
sp = parser.add_subparsers(dest='cmd') # put the command string in `cmd` attribute
p1 = sp.add_parser('list')
p2 = sp.add_parser('check')
p3 = sp.add_parser('watch')
p1.add_argument('value',dtype=int, nargs='?') # ok with 0 or 1 values
p2.add_argument('name')
p3.add_argument('name')
p3.add_argument('quality',nargs='?')
value and quality will get the default value if none is explicitly given. The default default is None. But you can define a meaningful alternative, e.g. default=0 for the integer value.
You could also define a parser like:
parser = ...
parser.add_argument('command', choices=['list','check','watch'])
parser.add_argument('rest', nargs='*')
This will expect one of the 3 'command' strings, and put anything else in the 'rest' attribute (as a list of strings). You could then interpret those strings however you want.

Custom type or action on default argument with argparse

I want to convert arguments I pass to argparse in an other format; the idea is to pass a number (a float typically) and to convert it in a string (Fortran notation for a float, typically).
First, I tried to create a custom type
def FortranFloat(value):
vfloat = float(value)
vfloat = str(vfloat) + "d0"
return vfloat
parser = argparse.ArgumentParser()
parser.add_argument("--xmin=", dest="xmin", type=FortranFloat, default=-2.0)
args = parser.parse_args()
It works well... if I actually give the --xmin= argument. Else, it just keeps the -2.0, without converting it in -2.0d0 as I wish. I therefore thought that it could be easier to give the default value the format I want, so replacing 2.0 by '2.0d0'... The interesting thing is that it is evaluated, because it crashes and raise the following error
argparse.ArgumentError: argument --xmin=: invalid FortranFloat value: '-2.0d0'
To get around this problem I tried to create a custom action
class FortranConverter(argparse.Action):
def __call__(self, parser, namespace, values, option_strings=None):
if type(values) is float:
values = str(values) + "d0"
setattr(args, self.dest, values)
parser = argparse.ArgumentParser()
parser.add_argument("--xmin=", dest="xmin", type=float, default=-2.0, action=FortranConverter)
args = parser.parse_args()
That didn't work either. It doesn't even seem to go through my action (if I print something in the __call__, it will just ignore it).
Any thought, idea or explanation would be appreciated!
If the default is a string, it is passed through the type function. Anything else is used as is: setattr(namespace, dest, default). Have you tried '-2.0'? Your FortranFloat should convert that to '-2.0d0'.
Another option is to modify your FortranFloat so it recognizes a string with 'd0', and passes it through unchanged.
The argument string is passed through type before it is passed to the action. And if I recall the code correctly, a default never goes through the action; it is set directly.
You don't need it, but to work, your FortranConverter function should use setattr(namespace, self.dest, values).
What were you trying to accomplish with nargs=0? Actions like 'store_true' use that to indicated that they don't take any arguments. Otherwise it isn't a useful value for the programmer.

Use another variable when it's not specified in Python's argparse module

Is it feasible to use a variable which is also another argument variable when the first argument is not specified in argparse?
So in the following code:
import argparse
parser = argparse.ArgumentParser()
parser.add_option('-f', '--file', dest='outputfile')
parser.add_option('-d', '--db', dest='outpufDB')
And when I run the above script via script.py -f file_name, and don't specify the argument on outputDB, then I want to set the same value on outputDB as on outputfile. I know using default argument enables to set default value, but is it also feasible to set default value derived from another argument?
Thanks.
The traditional way to do this (the way people did it going back to old Unix getopt in C) is to give outputDB a useless "sentinel" default value. Then, after you do the parse, if outputDB matches the sentinel, use outputfile's value instead.
See default in the docs for full details on all of the options available. But the simplest—as long as it doesn't break any of your other params—seems to be to leave it off, and pass argument_default=SUPPRESS. If you do that, args.outputDB just won't exist if nothing was passed, so you can just check for that with hasattr or in or try.
Alternatively, you can pass an empty string as the default, but then of course a user can always trigger the same thing with --outputDB='', which you may not want to allow.
To get around that, you can not give it a type, which means you can give any default value that isn't a string, and there's no way the user can pass you the same thing. The pythonic way to get a sentinel when None or the falsey value of the appropriate type isn't usable is:
sentinel = object()
x = do_stuff(default_value=sentinel)
# ...
if x is sentinel:
# x got the default value
But here I don't think that's necessary. The default None should be perfectly fine (there's no way a user can specify that, since they can only specify strings).
(It's quite possible that argparse has an even cooler way to do this that I haven't discovered, so you might want to wait a while for other answers.)
The simple approach - do your own checking after parse_args.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-f', '--file', dest='outputfile')
parser.add_argument('-d', '--db', dest='outpufDB')
args = parser.parse_args()
if args.outpufDB is None:
args.outpufDB = args.outputfile
The default default value is None, which is easy test for.
Doing something entirely within argparse is useful if it is reflected in the help or you want the standardize error message. But you are just filling in a default after parsing.
I can imagine doing the same with a custom '-f' Action, but the logic would be more complex.
I'm not sure if this is a good practice, but I tried this on a similar problem. I have adapted the following snippet for your code, hope it helps.
import argparse
parser = argparse.ArgumentParser()
parser.add_option('-f', '--file', dest='outputfile', default='dummy_file')
parser.add_option('-d', '--db', dest='outputDB', default=parser.parse_args().outputfile)
If you don't pass any arguments the name of both, the 'outputfile' and 'outputDB' will be 'dummy_file'.

Categories