Python argparse - make arguments required or optional based on another argument - python

How can a program accept/validate a set of parameters, depending on a previous parameter/option?
e.g:
params:
<action1> -p <path> -name <name> -t <type>
<action2> -v <value> -name <name>
<action3> -p <path> -t <type>
<action4> -m <mode1 | mode2>
--verbose
--test
--..
So if one of the actionX parameters is used (only one can be used), additional parameters might be required.
For instance for action2 the -v and -name are required.
valid input:
python myparser.py action2 -v 11 -name something --test --verbose
python myparser.py action4 -m mode1
python myparser.py --test
invalid input:
python myparser.py action2 -v 11
python myparser.py action4 -n name1
Can the argparse validate this or is it better to add all of them as optional and validate them later on?

You can use subparsers
Simple example for your case
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("--test", action="store_true")
subparsers = parser.add_subparsers()
parser_action2 = subparsers.add_parser("action2")
parser_action2.add_argument("-v", required=True)
parser_action2.add_argument("-name", type=str, required=True)
parser_action4 = subparsers.add_parser("action4")
parser_action4.add_argument("-m", type=str, required=True)
valid case
parser.parse_args(["action2", "-v", "11", "-name", "something"])
parser.parse_args(["action4", "-m", "mode1"])
parser.parse_args(["--test"])
# Namespace(v='11', name='something')
# Namespace(m='mode1')
# Namespace(test=True)
Invalid case
parser.parse_args(["action2", "-v", "11"])
# action2: error: the following arguments are required: -name
parser.parse_args(["action4", "-n", "name1"])
# action4: error: the following arguments are required: -m
EDIT: And in case you want to use shared argument which is required in one and optional in others, it is better to make them optional and validate them later as you said.

Related

Put subparser command at the beginning of arguments

I wanted to create an interface with argparse for my script with subcommands; so, if my script is script.py, I want to call it like python script.py command --foo bar, where command is one of the N possible custom commands.
The problem is, I already tried looking for a solution here on StackOverflow, but it seems like everything I tried is useless.
What I have currently is this:
parser = argparse.ArgumentParser()
parser.add_argument("-x", required=True)
parser.add_argument("-y", required=True)
parser.add_argument("-f", "--files", nargs="+", required=True)
# subparsers for commands
subparsers = parser.add_subparsers(title="Commands", dest="command")
subparsers.required = True
summary_parser = subparsers.add_parser("summary", help="help of summary command")
If I try to run it with:
args = parser.parse_args("-x 1 -y 2 -f a/path another/path".split())
I got this error, as it should be: script.py: error: the following arguments are required: command.
If, however, I run this command:
args = parser.parse_args("summary -x 1 -y 2 -f a/path another/path".split())
I got this error, that I shouldn't have: script.py: error: the following arguments are required: -x, -y, -f/--files.
If I put the command at the end, changing also the order of arguments because of -f, it works.
args = parser.parse_args("-x 1 -f a/path another/path -y 2 summary".split())
If I add the parents keyword in subparser, so substitute the summary_parser line with summary_parser = subparsers.add_parser("summary", help=HELP_CMD_SUMMARY, parents=[parser], add_help=False), then I got:
script.py summary: error: the following arguments are required: command when summary is in front of every other argument;
script.py summary: error: the following arguments are required: -x, -y, -f/--files, command when summary is at the end of the args.
My question is, how I have to setup the parsers to have the behaviour script.py <command> <args>? Every command shares the same args, because they are needed to create certain objects, but at the same time every command can needs other arguments too.
Creating another parser helped me getting what I wanted.
The root parser should add all the optional arguments - and also have add_help=False, to avoid an help message conflict -, then another parser - parser2, with a lot of fantasy - will be created.
The second parser will have subparsers, and they all needs to specify as parents the root parser.
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument() ...
parser2 = argparse.ArgumentParser()
subparsers = parser2.add_subparsers(title="Commands", dest="command")
subparsers.required = True
summary_parser = subparsers.add_parser("summary", parents=[parser])
summary_parser.add_argument("v", "--verbose", action="store_true")
# parse args
args = parser2.parse_args()
Now the output will be this:
usage: script.py [-h] {summary} ...
optional arguments:
-h, --help show this help message and exit
Commands:
{summary}
summary For each report print its summary, then exit
usage: script.py summary [-h] -x X -y Y -f FILES [FILES ...] [-v]
optional arguments:
-h, --help show this help message and exit
-x X
-y Y
-f FILES [FILES ...], --files FILES [FILES ...]
-v, --verbose

How to configure argparse to be like a comment central kind of cli

I'd like to do something like:
my-cli.py todo1 --todo1-option1 --todo1-option2 ...
my-cli.py todo2 --todo2-option1 --todo2-option2 ...
In addition, I'm hoping to declare --todo1-option1 is required if the command is todo1 and it is invalid parameter if I specify parameters for different command (i.e. todo2)
I'm also looking to have different -h for different command (i.e. todo1 or todo2).
I'm sorry if my English is a little broken. I hope you get what I mean.
The way I understand some tutorials, the main python app is the action itself like for example:
cp -f file1 file2
At first I thought there's some clever trick using the add_mutually_exclusive_group but the answer to my question is in fact the subparsers
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='commands')
# A todo1 command
todo1_parser = subparsers.add_parser(
'todo1', help='List contents')
todo1_parser.add_argument(
'--todo1-option1', action='store', required=True,
help='Todo1 Option 1')
todo1_parser.add_argument(
'--todo1-option2', action='store',
help='Todo1 Option 2')
# A todo2 command
todo2_parser = subparsers.add_parser(
'todo2', help='List contents')
todo2_parser.add_argument(
'--todo2-option1', action='store', required=True,
help='Todo2 Option 1')
todo2_parser.add_argument(
'--todo2-option2', action='store',
help='Todo2 Option 2')
print(parser.parse_args())
Test for list of commands:
$ python test_argparse.py
python test_argparse.py usage: test_argparse.py [-h] {todo1,todo2} ...
test_argparse.py: error: too few arguments
Test for Todo1 required parameters:
$ python test_argparse.py todo1
usage: test_argparse.py todo1 [-h] --todo1-option1 TODO1_OPTION1 [--todo1-option2 TODO1_OPTION2]
test_argparse.py todo1: error: the following arguments are required: --todo1-option1
Test for Todo1 required parameters:
$python test_argparse.py todo2
usage: test_argparse.py todo2 [-h] --todo2-option1 TODO2_OPTION1 [--todo2-option2 TODO2_OPTION2]
test_argparse.py todo2: error: the following arguments are required: --todo2-option1
Test for Todo2 shouldn't be availble to Todo1:
$python test_argparse.py todo1 --todo2-option1 aaa
usage: test_argparse.py [-h] {todo1,todo2} ... test_argparse.py: error:
unrecognized arguments: --todo2-option1 aaa

argparse with 2 date arguments

I want to create a program which selects users from a database between 2 dates given on the command line. I have:
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--date1","-d1",help="Show users between dates",type=str)
group.add_argument("--date2","-d2",help="Show users between dates",type=str)
if args.date1 and args.date2:
DataCalculation.show_users_between_date(args.date1,args.date2)
And in my DataCalculation I have query to get users between 2 dates.
Unfortunately this solution doesnt work and I get error: argument --date2/-d2: not allowed with argument --date1/d1
I was running program like: py main.py -d1 1994-01-01 -d2 1995-12-31
I was thinking that I can split these 2 dates to list in function and give only 1 argument like: py main.py -d 1994-01-01 1995-12-31, but this idea doesn't work too. Is there an easy way to use 2 arguments which have to be given together?
You're looking for inclusivity, not exclusivity. You can accomplish that by using nargs=2 with one option, like your second case.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"--date",
"-d",
nargs=2,
metavar=('start', 'end'), # Describes each argument
help="Show users between start and end dates",
)
args = parser.parse_args()
print(args)
Usage:
$ ./tmp.py -d 1994-01-01 1995-12-31
Namespace(date=['1994-01-01', '1995-12-31'])
$ ./tmp.py -d 1994-01-01
usage: tmp.py [-h] [--date start end]
tmp.py: error: argument --date/-d: expected 2 arguments
$ ./tmp.py -d 1994-01-01 1995-12-31 1998
usage: tmp.py [-h] [--date start end]
tmp.py: error: unrecognized arguments: 1998
$ ./tmp.py -h
usage: tmp.py [-h] [--date start end]
optional arguments:
-h, --help show this help message and exit
--date start end, -d start end
Show users between start and end dates
You could use
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--daterange","-dr",help="Show users between dates",type=str)
args = parser.parse_args()
date1, date2 = args.daterange.split()
print(date1)
And then quotes around your arguments as in
python test.py -dr "1994-01-01 1995-12-31"
Which yields with the above snippet:
1994-01-01

argparse - Reproduce behavior of "--help" argument with a specific argument

I have some trouble while working with the argparse module for Python v2.7.
Basically, what I have is a script that works with 5 mandatory arguments :
url
method
login
password
output
An example of the syntax would look like this :
script.py -w/--url [URL] -m/--method [METHOD] -l/--login [LOGIN] -p/--password [PASSWORD] -o/--output [OUTPUT]
What I'd like to do is this :
add an optional argument -t/--test
its behavior would be that, based on the url used with the -w/--url argument, it would bypass completely the -m/--method, -l/--login and -p/--password arguments but, for it to work, I need to tell argparse to stop processing arguments if -t/--test is provided (but only with -w/--url).
Is this behavior even possible? I tried to play with argparse sub-commands but it seems to be (at least to my small knowledge) a bit overkill.
NB: Here is my original code :
# Description : parses script arguments
# Argument(s) : all
# Return value : all arguments values
def testArgs():
parser = argparse.ArgumentParser(description='Foo', formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-w','--url', help='URL', required=True)
parser.add_argument('-t','--test', help='Test command', action='store_true')
parser.add_argument('-m','--method', help='METHOD', required=True)
parser.add_argument('-u','--login_name', help='LOGIN', required=True)
parser.add_argument('-p','--login_password', help='PASSWORD', required=True)
parser.add_argument('-o','--output_format', help='OUTPUT', required=True, choices=['json', 'yaml', 'python'], default='json')
args = parser.parse_args()
return args
EDIT : After a lot of testing, I have managed the following :
def testArgs():
parser = argparse.ArgumentParser(description='DESCRIPTION')
subparsers = parser.add_subparsers()
p_list = subparsers.add_parser('test', help='List all available methods')
p_list.add_argument('-w', help='URL', required=True)
p_list.add_argument('-t', help='Test', action='store_true', required=True)
p_cmd = subparsers.add_parser('cmd', help='Executes command')
p_cmd.add_argument('-w', help='URL', required=True)
p_cmd.add_argument('-m', help='Method', required=True)
p_cmd.add_argument('-l', help='Login', required=True)
p_cmd.add_argument('-p', help='Password', required=True)
p_cmd.add_argument('-o', help='Output', required=True)
args = parser.parse_args()
return args
Which exhibits the following behavior :
$ python testArgparse.py -h
usage: testArgeparse.py [-h] {test,cmd} ...
DESCRIPTION
positional arguments:
{test,cmd}
test Lists all available methods
cmd Executes command
optional arguments:
-h, --help show this help message and exit
But to access help on the others arguments, I need to do the following :
$ python testArgparse.py test -h
usage: testArgparse.py test [-h] -w W -t
optional arguments:
-h, --help show this help message and exit
-w W URL
-t Test
$ python testArgparse.py cmd -h
usage: testArgparse.py cmd [-h] -w W -m M -l L -p P -o O
optional arguments:
-h, --help show this help message and exit
-w W URL
-m M Method
-l L Login
-p P Password
-o O Output
I'd like to be able to, at least, display help about all arguments without having to use --help for both test and cmd arguments.
Ideally, what I'd like is this behavior :
$ python testArgparse.py [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
required=True with store_true does not make sense. The default is False, but if it is required the returned value will always be True.
Since only -w is required unconditionally, I would drop the required parameter on everything else. Then test for the required values after parse_args. I can still issue an argparse error with usage at that time. In other words, do my own testing rather than try something fancy in argparse.
def testArgs():
usage = '[-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]'
parser = argparse.ArgumentParser(description='DESCRIPTION',usage=usage)
parser.add_argument('-w', help='URL', required=True)
parser.add_argument('-t', help='Test', action='store_true')
parser.add_argument('-m', help='Method')
parser.add_argument('-l', help='Login')
parser.add_argument('-p', help='Password')
parser.add_argument('-o', help='Output')
args = parser.parse_args()
# sample test, streamline and refine to suit your needs
# this assumes the default for all these args is None
if not args.t: # '-t' in in argv
if any([args.m is None, args.l is None, args.p is None, args.o is None]):
parser.error('m,l,p,o are all required')
return args
1216:~/mypy$ python2.7 stack21070971.py
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: argument -w is required
1213:~/mypy$ python2.7 stack21070971.py -w url
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: m,l,p,o are all required
1213:~/mypy$ python2.7 stack21070971.py -w url -t
Namespace(l=None, m=None, o=None, p=None, t=True, w='url')
1214:~/mypy$ python2.7 stack21070971.py -w url -m mmm
usage: [-w URL -t] | [-w URL -m METHOD -u LOGIN -p PASS -o OUTPUT]
stack21070971.py: error: m,l,p,o are all required
...
1215:~/mypy$ python2.7 stack21070971.py -w url -m mmm -l uuu -p ppp -o ooo
Namespace(l='uuu', m='mmm', o='ooo', p='ppp', t=False, w='url')

How to let argparse check mutually exclusive groups of arguments

I have a test code as follows, that shall take EITHER the positional argument file OR all the optional arguments time, expression and name:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-t","--time")
parser.add_argument("-x","--expression")
parser.add_argument("-n","--name")
parser.add_argument("file")
print parser.parse_args()
The following combination should work
test.py filename
test.py -t 5 -x foo -n test
but NOT these:
test.py filename -t 5 # should raise error because the positional and the optional -t argument cannot be used together
test.py -t 5 -x foo # should raise an error because all three of the optional arguments are required
Any simple solution to that problem?
The first issue is that you have specified that file is positional which will make it required. You will probably need to convert it to a optional argument.
Here is a simple way to check that the correct arguments have been provided:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-t","--time")
parser.add_argument("-x","--expression")
parser.add_argument("-n","--name")
parser.add_argument("-f", "--file")
args = parser.parse_args()
has_file = args.file is not None
has_txn = None not in frozenset([args.time, args.expression, args.name])
assert (has_file ^ has_txn), "File or time, expression and name must be provided"
You might find the following approach useful.
import argparse, sys
print(sys.argv)
if len(sys.argv) == 2:
sys.argv += ['-t', 'time', '-x', 'expression', '-n', 'name']
else:
sys.argv.append('FILE')
parser = argparse.ArgumentParser()
group = parser.add_argument_group('Required Group', 'All 3 are required else provide "file" argument.')
group.add_argument("-t","--time", required=True)
group.add_argument("-x","--expression", required=True)
group.add_argument("-n","--name", required=True)
parser.add_argument("file", help='file name')
print(parser.parse_args())
Here is some example output..
$ ./t.py -t 2 -x "a + b" -n George
['./t.py', '-t', '2', '-x', 'a + b', '-n', 'George']
Namespace(expression='a + b', file='FILE', name='George', time='2')
$ ./t.py FILE
['./t.py', 'FILE']
Namespace(expression='expression', file='FILE', name='name', time='time')
$ ./t.py -h
usage: t.py [-h] -t TIME -x EXPRESSION -n NAME file
positional arguments:
file file name
optional arguments:
-h, --help show this help message and exit
Required Group:
All 3 are required else provide "file" argument.
-t TIME, --time TIME
-x EXPRESSION, --expression EXPRESSION
-n NAME, --name NAME

Categories