How can I most efficiently parse these arguments in python? - 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.

Related

Is it possible to run type after action in ArgumentParser?

Generally, my goal is to validate the argument after it has been sanitized. Maybe my pipeline is wrong, but as I see it, ArgumentParser doesn't allow this to be done in a snap. To sanitize the argument I'm using a custom action and for validation, I'm using a custom type.
In the example below, I have the following args: 2 days ago. I want it to be validated as a single argument. So I've made a custom action, which joins [2, 'days', 'ago'] in one string. I'd love to validate it after with test_type function.
from argparse import ArgumentParser, Action
parser = ArgumentParser()
def test_type(value):
print (value)
return value
class ActionJoinString(Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, ' '.join(values))
parser.add_argument('timerange', nargs='+', action=ActionJoinString, type=test_type)
options = parser.parse_args('2 days ago'.split())
print(options)
The output is the following:
$ python asd.py
2
days
ago
Namespace(timerange='2 days ago')
So as you see, even though timerange was concatenated in a single string, the type-validation was performed on the array of values. Is there any easy way to make it done my way? I thought it would be great to have special nargs to concatenate arguments in a string, but as I see it doesn't exist.
Thanks for any tips!
Based on the nargs the parser allocates a list of strings to this Action. You'll see in the get_values method that for '+' the strings are passed one at a time to get_value where they pass through type ( and may be tested against choices). Only after this are they passed to the Action _call__ as values.
So type is only for one string in sys.argv.
But your action call can do anything with the values. You can also use an ordinary store, and process the values after parsing. You don't get extra points for doing everything in the parser.

Ordered and repeated flags using argparse

Trying to implement a CLI using argparse without writing my own parser if possible. Basically I would like to use the command line in order to invoke different methods sequentially in a program, i.e.:
my_program.py --doA --doB --doA --doA
I've seen threads with good ideas on how to obtain ordered arguments, but I suppose I would end up with a list like this ['doA', 'doB', 'doA', 'doB'] where I would still need to parse the arguments myself.
Is there any way to exploit tools in argparse in order to help execute these commands in order? Thanks!
Here's a parser that will collect these arguments repeatedly and in order:
In [1]: import argparse
In [2]: parser = argparse.ArgumentParser()
In [3]: parser.add_argument('--doA', dest='cmd', const='doA', action='append_con
...: st');
In [4]: parser.add_argument('--doB', dest='cmd', const='doB', action='append_con
...: st');
test:
In [5]: args = parser.parse_args('--doA --doB --doA --doA'.split())
In [6]: print(args)
Namespace(cmd=['doA', 'doB', 'doA', 'doA'])
do something:
In [7]: for action in args.cmd:
...: print('action:', action)
...:
action: doA
action: doB
action: doA
action: doA
If I had defined:
def doA(*args):
# do something
pass
and used const=doA, then the args.cmd list would be
Namespace(cmd=[doA, doB, ...])
and we could write
for action in args.cmd:
action(arguments)
argparse docs has something like this for subparses and the set_default command.
If these are the only arguments you accept this would be over kill. But if there are other options that need argparse this would fit in. But accepting a
parser.add_argument('--cmd', nargs='*', ...)
would be fine. It could use choices to restrict the input strings, and even a type function to translate the strings into function objects.
Command line arguments can separated into 3 groups:
Options - prefixed by - or -- and usually specify how the command should behave (--verbose) and may or may not take values, and typically the order does not matter
Sub commands - are one of a few program defined keywords telling to program to perform a certain subset of the program's full suite of functionality (pull in git pull), usually only one can be specified at a time
Arguments - Are the values usually passed at the end of the command, and specify the values to use during execution, and typically these values are simply consumed without any modification
Most argument parsers treat options as unordered and tend to simply set values to variables or properties in a namespace for the programmer to use. This makes ordered options generally not possible without writing a custom parser.
For your tool you just want to specify an ordered list of actions to take, so it might make more sense to pass them in as either values to an option, or simple arguments.
my_program.py --actions doA doB doC doD
my_program.py doA doB doC doD
Argparsers (like python's argparse) do not mutate the values given to options or values passed as arguments, meaning the order of the actions would be preserved.

Argparse: mutually exclusive groups with defaults

Maybe there was an answer to one of the similar questions, but I couldn't find it.
What I need.
I have a table to read from: table with most recent data (fast), table with data for one day (day).
I want to read from day-table by default and from fast-table if I provide an argument -f in the command line.
Then I have defaults for each argument.
So if I don't provide any arguments ("-s" or "-f"), I want to use "day" with the default value.
If I use something like "-s 20161001" or "-f 1452557323", I want to use that value.
If use "-f", I want to use "-f" default value.
All I have right now is:
table_choice = parser.add_mutually_exclusive_group(required=True)
table_choice.add_argument(
'-s', '--day-table',
dest='day',
help='day table data',
default="path/" + day(),
)
table_choice.add_argument(
'-f', '--fast-table',
dest='fast',
help='fast table data',
default=fast(),
)
But sadly it doesn't work like I want.
"script -f" returns:
Script: error: argument -f/--fast-table: expected one argument
Only works if I have provided a value.
If nargs='?', then you get a 3 way action - the default, a constant, or the value.
table_choice.add_argument(
'-f', '--fast-table',
dest='fast',
help='fast table data',
default=fast(),
nargs='?',
const='value_if_noarg'
)
This nargs plays nicely with mutually exclusive groups (including the 'required' one).
By making the group required, you will have to use either -s or -f. Omit the required=True is you want the option of using neither.
The namespace will have values for both day and fast regardless of what is in the commandline. If you want the day value to have priority of the fast one, choose the defaults so you can distinguish between the default and a given value. The default default None is handy for that.
You could use default=argparse.SUPPRESS to keep a default out of the namespace, but that's harder to test than args.fast is None.
As discussed in Python argparse --toggle --no-toggle flag your arguments could even share the dest. Whether that makes the following logic easier or not is questionable.

python argparse: how to use other parsed argument as parameter at calling function in type keyword?

I am trying to create an user interface using argparse module.
One of the argument need to be converted, so I use the type keyword:
add_argument('positional', ..., type=myfunction)
and there is another optional argument:
add_argument('-s', dest='switch', ...)
in addition, I have
parsed_argument=parse_args()
However, in myfunction, I hope I can use an additional parameter to control the behavior, which is the optional argument above, i.e.
def myfunction(positional, switch=parsed_argument.switch):
...
How can I achieve that?
Simple answer: You can’t. The arguments are parsed separately, and there is no real guarantee that some order is maintained. Instead of putting your logic into the argument type, just store it as a string and do your stuff after parsing the command line:
parser.add_argument('positional')
parser.add_argument('-s', '--switch')
args = parser.parse_args()
myfunction(args.positional, switch=args.switch)
I'm not sure I did understand correctly what you want to achieve, but if what you want to do is something that looks like:
myprog.py cmd1 --switcha
myprog.py cmd2 --switchb
yes you can, you need to use subparsers. I wrote a good example of it for a little PoC I wrote to access stackoverflow's API from CLI. The whole logic is a bit long to put thoroughly here, but mainly the idea is:
create your parser using parser = argparse.ArgumentParser(...)
create the subparsers using subparsers = parser.add_subparsers(...)
add the commands with things like `subparser.add_parser('mycommand', help='Its only a command').set_defaults(func=mycmd_fn) where
mycmd_fn takes args as parameters where you have all the switches you issued to the command!
the difference from what you ask, is that you'll need one function per command, and not one function with the positional argument as first argument. But you can leverage that easily by having mycmd_fn being like: mycmd_fn = lambda *args: myfunction('mycmd', *args)
HTH
From the documentation:
type= can take any callable that takes a single string argument and returns the converted value:
Python functions like int and float are good examples of a type function should be like. int takes a string and returns a number. If it can't convert the string it raises a ValueError. Your function could do the same. argparse.ArgumentTypeError is another option. argparse isn't going to pass any optional arguments to it. Look at the code for argparse.FileType to see a more elaborate example of a custom type.
action is another place where you can customize behavior. The documentation has an example of a custom Action. Its arguments include the namespace, the object where the parser is collecting the values it will return to you. This object contains any arguments have already been set. In theory your switch value will be available there - if it occurs first.
There are many SO answers that give custom Actions.
Subparsers are another good way of customizing the handling of arguments.
Often it is better to check for the interaction of arguments after parse_args. In your case 'switch' could occur after the positional and still have effect. And argparse.Error lets you use the argparse error mechanism (e.g. displaying the usage)

Argparse: Check if any arguments have been passed

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.

Categories