I'm working on something where I need to use argparse.
Here's the code I got a problem with:
parser = argparse.ArgumentParser()
parser.add_argument('--create n file', dest='create',
nargs=2, default=(1, 'world1.json'),
help='Create a party of n player with mission parameters in file')
I'm trying to find a way to either set both n and file to another value, or set only one of them. n is an int and file a str.
Here's what I would like to get, using the following command:
Command
Expected result
python mission.py --create 2
create = [2, 'world1.json']
python mission.py --create world2.json
create = [1, 'world2.json']
python mission.py --create 3 world2.json
create = [3, 'world2.json']
When --create is used (with or without specifying n/file), I'll need to start a function using the list as arguments.
I've tried multiple things and read argparse documentation more than once but can't find a way to do it.
The code below returns the expected results for the listed usecases. I decided to use an extra function to handle the argument, as the program must accept either an int or a string for the first argument passed.
I use a "try" block to see whether the single argument can be parsed as an int before proceeding.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--create n file', dest='create', nargs='+', default=(1,'world1.json'),
help='Create a party of n player with mission parameters in file')
args = parser.parse_args()
def get_n_file(arg):
if len(arg)==1:
try:
i = int(arg[0])
result = int(arg[0]), 'world'+str(arg[0])+'.json'
except:
s = arg[0]
result = 1, s
return result
elif len(arg)==2:
return int(arg[0]), arg[1]
print(args.create)
n, f = get_n_file(args.create)
print(n, f)
Related
I have a small Python tracking script with a couple of possible modes (e.g. "add" and "show") that correspond to methods that take different numbers of arguments.
I want to check the mode argument is valid, and if so, then call the appropriate method with all remaining command line arguments passed as individual parameters.
The code I have looks like this:
"""python tracking.py add <person> <date> <score>
python tracking.py show <person> <date>
"""
if __name__ == "__main__":
tracking = TrackingData()
modes = {'add': tracking.add_data, 'show': tracking.print_data }
mode = sys.argv[1]
# If mode is valid
modes[mode](sys.argv[2:])
# Original code looks a bit like this:
#
# person = sys.argv[2]
# date = sys.argv[3]
# score = sys.argv[4]
# tracking.add_data(person, date, score)
# tracking.print_data(person, date)
However, the code gives a TypeError: print_data() missing 1 required positional argument: 'date' - so presumably the array slice is being passed as a single parameter, whereas I need it to be flattened into its component entries.
What's the best way to do this? Are there any better patterns for this type of dispatch table / modal action approach?
Using * will unpack values from a list into positional arguments. So you would just need to change your dispatch line to look like this:
modes[mode](*sys.argv[2:])
For example:
def add3(a, b, c):
return a + b + c
nums = [1, 2, 3]
add3(*nums) # 6
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
I'm using python 3.74 and I'm trying to use the argparse module to read a pair of numbers into the variable nums. I would like to check if certain conditions hold between the two numbers and then sort them.
I have the code below.
import argparse
def f(x):
assert x[0]!=x[1],"err"
return sorted([int(y) for y in x])
parser = argparse.ArgumentParser()
parser.add_argument("--nums", required=True, type=f,nargs=2)
args = parser.parse_args()
I want the snippet above to return [1, 2] when the input is 2 1. I get an IndexError instead.
This led me to investigate with the code below:
parser = argparse.ArgumentParser()
parser.add_argument("--nums", required=True, type=lambda x:x*2 ,nargs=2)
args = parser.parse_args()
now the output for 2 1 is ['22', '11']
which leads me to believe that type is applied te each of the values separately. Is there any way to perform type validation on --nums as a whole? I'd preferably like to do it within add_argument.
As the documentation states
type= can take any callable that takes a single string argument and returns the converted value
And we can confirm this by looking at the ArgParse implementation, nargs arguments are evaluated individually. So your f function is being called for each argument.
If you really want it to be done as part of the type checking, you could require a string input of 1,2 and parse that yourself like so:
def f(x):
x = x.split(',')
assert x[0]!=x[1],'err'
return sorted([int(y) for y in x])
Where an input of 1,2 results in [1, 2]
But that defeats the purpose of argparse, so you're better off simply calling your validator after the arguments have been parsed, the experience for the end-user is no different.
parser.add_argument("--nums", required=True, type=int, nargs=2)
args = parser.parse_args()
print(f(args.nums))
An input of 2 1 returns [1, 2]
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.
I need to let the end user of my python script types something like:
script.py -sizes <2,2> <3,3> <6,6>
where each element of the -sizes option is a pair of two positive integers. How can I achieve this with argparse ?
Define a custom type:
def pair(arg):
# For simplity, assume arg is a pair of integers
# separated by a comma. If you want to do more
# validation, raise argparse.ArgumentError if you
# encounter a problem.
return [int(x) for x in arg.split(',')]
then use this as the type for a regular argument:
p.add_argument('--sizes', type=pair, nargs='+')
Then
>>> p.parse_args('--sizes 1,3 4,6'.split())
Namespace(sizes=[[1, 3], [4, 6]])
Argparse doesn't try to cover all possible formats of input data. You can always get sizes as string and parse them with few lines of code:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--sizes', nargs='+')
args = parser.parse_args()
try:
sizes = [tuple(map(int, s.split(',', maxsplit=1))) for s in args.sizes]
except Exception:
print('sizes cannot be parsed')
print(sizes)
"Special cases aren't special enough to break the rules."