Nested options with argparse - python

I'd like to have my python script take variable number of arguments depending on a particular choice. For example:
python run.py foo
python run.py bar X Y
where choosing the option bar requires two additional arguments, say integer inputs, but foo requires no additional arguments.
import argparse
argparser = argparse.ArgumentParser()
# Allow user to choose test to run
argparser.add_argument("test", choices=['foo', 'bar'], help="You may choose foo or bar.")
...
But how can I specify additional arguments required by bar? P.S. I'm working with Python 2.7, so if a solution requires Python 3, it won't be much help in my case.

You should use subparsers:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='subcommands')
parser_foo = subparsers.add_parser('foo')
parser_foo.set_defaults(target='foo')
parser_bar = subparsers.add_parser('bar')
parser_bar.add_argument('more')
parser_bar.set_defaults(target='bar')
Usage:
>>> parser.parse_args(['foo'])
Namespace(target='foo')
>>> parser.parse_args(['bar', '123'])
Namespace(target='bar', more='123')
Note that you could set the target to e.g. a function and call it directly. Here's some sample code that does this (extracted from Cactus' CLI, but that's a rather common pattern):
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title = 'subcommands')
parser_create = subparsers.add_parser('create')
parser_create.add_argument('path')
parser_create.add_argument('-s', '--skeleton')
parser_create.set_defaults(target=create)
parser_build = subparsers.add_parser('build')
parser_build.set_defaults(target = build)
args = parser.parse_args()
args.target(**{k: v for k, v in vars(args).items() if k != 'target'})

Related

How to create multiple tests for a function using sys.argv in pytest

I have a function that looks like this:
import argparse
import sys
def execute():
parser = argparse.ArgumentParser()
if (total_args := len(sys.argv)) == 1:
do_stuff()
if total_args == 2:
first = sys.argv[1]
do_stuff2()
if total_args == 3:
first, second = sys.argv[1:3]
do_stuff3()
if total_args > 3:
first, second = sys.argv[1:3]
del sys.argv[1:3]
add_args(parser)
parser.parse_args()
do_stuff4()
Which should have a test function test_execute that will try different given args, the question: is there a clean way to do it without manually modifying sys.argv using sys.argv.extend(some_test_args) and delete the args later?
Note: I can't use argparse optional positional arguments by setting nargs=? in parser.add_argument() because the first 2 arguments are optional and each case (1, 2, 3, > 3 arguments) executes different functions. To understand further, please check the example below ...
parser = argparse.ArgumentParser()
parser.add_argument('arg1', nargs='?')
parser.add_argument('arg2', nargs='?')
args = parser.parse_known_args()
print(args)
which if called like the following, will result to the wrong variable saved in the second position:
>>> python my_script.py --unknown-arg 999
Will print:
(Namespace(arg1='999', arg2=None), ['--unknown-arg'])
which is totally not what I need. I'm expecting arg1 to have a None value. The reason sometimes there will be unknown arguments is that argparse does not support parsing arguments by specifying a group. Let's say I have argument group A and argument group B and I need to parse only group A, I can't do parser.parse_group('A') I will have to create parser_a = argparse.ArgumentParser() and add group A arguments and parse them and repeat for parser_b.
Therefore the best solution I have so far is using sys.argv despite the fact this is inconvenient for testing. Also adding all options without grouping, will create another problem because group B arguments depend on values parsed from group A.
One workaround is to specify using --unknown-arg=999 but this will create inconsistencies in the documentation and usage of the script and is also not what I need.
Could you pass in sys.argv into execute()?
Something like this:
import argparse
import sys
def execute(argv):
parser = argparse.ArgumentParser()
if (total_args := len(argv)) == 1:
do_stuff()
if total_args == 2:
first = argv[1]
do_stuff2()
if total_args == 3:
first, second = argv[1:3]
do_stuff3()
if total_args > 3:
first, second = argv[1:3]
del argv[1:3]
add_args(parser)
parser.parse_args(argv)
do_stuff4()
if __name__ == "__main__":
execute(sys.argv)
In your tests you could then do something along the lines of:
def test_execute():
test_argv = ["some", "args", "list"]
execute(test_argv)
# assert something

How to get subset of argparse arguments in code

I would like to get subset of parsed arguments and send them to another function in python. I found this argument_group idea but I couldn't find to reach argument groups. This is what I want to try to do:
import argparse
def some_function(args2):
x = args2.bar_this
print(x)
def main():
parser = argparse.ArgumentParser(description='Simple example')
parser.add_argument('--name', help='Who to greet', default='World')
# Create two argument groups
foo_group = parser.add_argument_group(title='Foo options')
bar_group = parser.add_argument_group(title='Bar options')
# Add arguments to those groups
foo_group.add_argument('--bar_this')
foo_group.add_argument('--bar_that')
bar_group.add_argument('--foo_this')
bar_group.add_argument('--foo_that')
args = parser.parse_args()
# How can I get the foo_group arguments for example only ?
args2 = args.foo_group
some_function(args2)
I don't know whether a simpler solution exists, but you can create a custom "namespace" object selecting only the keys arguments you need from the parsed arguments.
args2 = argparse.Namespace(**{k: v for k, v in args._get_kwargs()
if k.startswith("foo_")})
You can customize the if clause to your needs and possibly change the argument names k, e.g. removing the foo_ prefix.

Using argparse argument names as variable names

I want to use name of arguments in argparse as variable names. Now I'm doing something like:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('a')
parser.add_argument('b')
parser.add_argument('c')
parser.add_argument('--d')
parser.add_argument('--e')
args = parser.parse_args()
a = args.a
b = args.b
c = args.c
d = args.d
e = args.e
But this is so inefficient and I may end up with over 10 arguments in total. Is there an efficient way to do this?
This is best I have found to reduce lines of code when renaming arg vars:
a,b,c,d,e = args.a, args.b, args.c, args.d, args.e
For me it's an acceptable solution, where no additional processing is required on an arg var.
Note that this is a very discouraged behavior, but if you really need to do it,
globals().update(args.__dict__)
will update your global namespace.

ArgumentParser: Optional argument with optional value

If I have an optional argument with optional argument value, is there a way to validate if the argument is set when the value is not given?
For instance:
parser = argparse.ArgumentParser()
parser.add_argument('--abc', nargs='?')
args = parser.parse_args()
Would correctly give me:
optional arguments:
--abc [ABC]
How do I distinguish between 1 and 2 below?
'' => args.abc is None
'--abc' => args.abc is still None
'--abc something' => args.abc is something
...
Update:
Found a trick to solve this problem: you can use "nargs='*'" instead of "nargs='?'". This way #1 would return None, and #2 would return an empty list. The downside is this will allow multiple values for the arguments to be accepted too; so you'd need to add a check for it if appropriate.
Alternatively you can also set a default value for the argument; see answer from chepner and Anand S Kumar.
With nargs='?', you can supply both a default and const.
In [791]: parser=argparse.ArgumentParser()
In [792]: parser.add_argument('--abc', nargs='?', default='default', const='const')
If the argument is not given it uses the default:
In [793]: parser.parse_args([])
Out[793]: Namespace(abc='default')
If given, but without an argument string, it uses the const:
In [794]: parser.parse_args(['--abc'])
Out[794]: Namespace(abc='const')
Otherwise it uses the argument string:
In [795]: parser.parse_args(['--abc','test'])
Out[795]: Namespace(abc='test')
In [796]: parser.print_help()
usage: ipython3 [-h] [--abc [ABC]]
optional arguments:
-h, --help show this help message and exit
--abc [ABC]
Use a different default value for the option. Compare
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--abc', nargs='?', default="default")
>>> parser.parse_args()
Namespace(abc='default')
>>> parser.parse_args(['--abc'])
Namespace(abc=None)
>>> parser.parse_args(['--abc', 'value'])
Namespace(abc='value')
I'm not sure how you would provide a different value for when --abc is used without an argument, short of using a custom action instead of the nargs argument.
Not sure if this is the standard way, but you can set default argument to something , and then that value would be used in case --abc is not in the argument list.
Example code -
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--abc', nargs='?', default="-1")
args = parser.parse_args()
print(args)
Result -
>python a.py
Namespace(abc='-1')
>python a.py --abc
Namespace(abc=None)
>python a.py --abc something
Namespace(abc='something')
I'm using this to have a command-line arg for multiprocessing. Specifying --multi uses all cores and given an arg specifies a number of cores, e.g., --multi 4 for four cores.
parser.add_argument("-mp", "--multi", type=int, nargs="*", help=multi_text)
Parsing logic is then:
if (args.multi == None):
num_cores = 1
elif (args.multi == []):
num_cores = multiprocessing.cpu_count()
elif (len(args.multi) == 1):
num_cores = args.multi[0]
else:
print("Invalid specification of core usage.")
sys.exit(1)

Is it possible to reconstruct a command line with Python's argparse?

I have a Python script that reads a file containing a command line invocation of some other tool. I'd like to modify the options of this invocation before calling the tool. For example, I might transform:
my_util --input file1.txt --option1 red --option2 blue
...to this:
my_util --input file1_001.txt --option1 red --option3 green
(More accurately, I'd be working on the arguments as lists.)
I figured that using the argparse module would be the easiest way to do this: I could parse the args, change, add or remove the options as I need to, and then reconstruct the command line.
But how do I do the last step? Given the Namespace object returned by parse_args(), can I easily reconstruct a list of command line options, such as could be passed to subprocess.Popen()?
A Namespace object is just a simple object subclass, so you can get the values out as a dict with vars:
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo')
>>> args = parser.parse_args(['--foo', 'BAR'])
>>> vars(args)
{'foo': 'BAR'}
Or you can assign to a class directly and get the arguments out as class variables:
>>> class C(object):
... pass
...
>>> c = C()
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo')
>>> parser.parse_args(args=['--foo', 'BAR'], namespace=c)
>>> c.foo
'BAR'
It would be fairly easy to use either of these structures to test/replace arguments and pass the results to Popen.
I know this is an old question, but I've just encountered the same problem. I realized that all I need is a way to iterate over the Action objects. Unfortunately, the internal list is not exposed by ArgParser itself. However, these objects are returned by add_argument(), so I can construct my own list. Well, putting actions.append() around each call looked like too much typing to me, so I store all options in a tuple:
def add_argument(*args, **kwargs):
return (args, kwargs)
parser = argparse.ArgumentParser()
options = (
add_argument('--verbose', action='store_true'),
add_argument('--author'),
add_argument('--subject', required=True),
add_argument('--cache', nargs='?'),
add_argument('files', nargs=''),
)
actions = []
for (args, kwargs) in options:
actions.append(parser.add_argument(*args, **kwargs))
args = parser.parse_args()
At this point, the options are parsed in args, and all argparse.Action objects are stored in the actions list. I can then iterate over this list and reconstruct the options like this:
cmdline = []
for action in actions:
value = getattr(args, action.dest)
if action.required or value != action.default:
if action.option_strings:
cmdline.append(action.option_strings[0])
if action.nargs is None:
cmdline.append(value)
elif action.nargs == '?':
if value != action.const:
cmdline.append(value)
elif action.nargs != 0:
cmdline += value
In my specific case, I also wanted to remove some options from the command line. To do that I simply added them separately with a call to parser.add_argument() and not through the options tuple.

Categories

Resources