2

I have test scenario where I need to take action based on arguments passed to python script. The requirement is as follows:

test.py -a a1 -b b1 [[[-c c1] [-d d1]] | [-e e1 -f f1]]

  1. The user has to pass mandatory arguments '-a' and '-b'.
  2. The arguments '-c' and '-d' are optional arguments which could be passed individually.
  3. The arguments '-e' and '-f' are optional arguments that has to be passed together along with mandatory arguments.
  4. If any of '-c' or '-d' are passed along with '-e' and '-f', then script should give error for arguments '-c' or '-d'.
  5. If '-e' and '-f' are passed along with '-c' or '-d', then script should give error for arguments '-e' and '-d'.

The error could be generic error for all the optional arguments.

Till now, I'm trying to do it using parents= option of argparse.ArgumentParser but it is giving error when I try to read the passed arguments.

import argparse


parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('-a', type=str, required=True)
parent_parser.add_argument('-b', type=str, required=True)

one_parser = argparse.ArgumentParser(parents=[parent_parser])
one_parser.add_argument('-c', type=str, default="test")
one_parser.add_argument('-d', type=str, default="Default")

multi_parser = argparse.ArgumentParser(parents=[parent_parser])
multi_parser.add_argument('-e', type=str)
multi_parser.add_argument('-f', type=str)

if str(one_parser.parse_args().c):
    print(one_parser.parse_args())
elif str(multi_parser.parse_args().e):
    print(multi_parser.parse_args())

When I run the script as :

test.py -a a1 -b b1 -e e1 -f f1

I'm getting the error :

usage: test.py [-h] -a A -b B [-c C] [-d D]
test.py: error: unrecognized arguments: -e e1 -f f1

Process finished with exit code 2

Any pointers are highly appreciated. Thank you.

Answer: The solution is hack but it worked for me.

import argparse


parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-a', type=str, required=True)
parser.add_argument('-b', type=str, required=True)
parser.add_argument('-c', type=str)
parser.add_argument('-d', type=str)
parser.add_argument('-e', type=str)
parser.add_argument('-f', type=str)
args = parser.parse_args()

a = str(args.a)
b = str(args.b)
c = str(args.c)
d = str(args.d)
e = str(args.e)
f = str(args.f)

if (e != "None" and f == "None") or (e == "None" and f != "None"):
    if c == "None" and d == "None":
        raise argparse.ArgumentTypeError("Both e and f arguments are required")
    elif c != "None" or d != "None":
        raise argparse.ArgumentTypeError("e and f are not supported with c or d")
if (c != "None" or d != "None") and (e != "None" or f != "None"):
    raise argparse.ArgumentTypeError("c and d are not supported with e and f")
if e == "None" and f == "None":
    if c == "None":
        c = "test"
    if d == "None":
        d = "Default"

IMO, argparser should handle argument dependency more efficiently. Maybe someone will contribute and help us all.. :)

Nilesh Bhave
  • 301
  • 1
  • 12
  • Rather than use multiple parsers, you should look at arguments groups https://docs.python.org/3.6/library/argparse.html#argument-groups – Valentin M. Jan 09 '20 at 10:26
  • 1. You have to use only one parser, because if one fails, it exits. Just like that. `if str(one_parser.parse_args().c):` doesn't even check the `c` because it cannot recognise `-e e1`, so it prints help and exits. 2. You can throw errors manually for more complex checking - `raise argparse.ArgumentTypeError("your error message here")` (if used in `except`, remember that you can `raise ... from` for informative error messages!) 3. Argument groups are your friend. They can't do everything, but you'll probably get the `[[[-c c1] [-d d1]] | [-e e1 -f f1]]` part if you do them right. – h4z3 Jan 09 '20 at 10:29
  • @PySaad It means that -c and -d are independent optional arguments. Also, -e and -f needs to be used together and should not be used with -c or -d. – Nilesh Bhave Jan 09 '20 at 10:36
  • @ValentinMarle Thanks for the pointer. I'll try that out. – Nilesh Bhave Jan 09 '20 at 10:38
  • @h4z3 Thanks for the explanation. I'll try your suggestion. – Nilesh Bhave Jan 09 '20 at 10:41
  • 2
    @NileshBhave Additionally, from my personal experience trying to do something similar, argparse does not easily (if ever) handle this level of argument complexity. It will probably be easier to parse your arguments simply saying "required" or "optional", then check for compliance to your conditions in your code, raising `argparse.ArgumentTypeError` when needed (as h4z3 pointed out) – Valentin M. Jan 09 '20 at 10:47
  • 2
    Just a note: there's also just `ArgumentError` - and it's the one that should be used here. `ArgumentTypeError` is about converting the string you get into real argument value. (I just looked into one of my scripts where I parse date, so I needed the type version of the error there, oops.) – h4z3 Jan 09 '20 at 10:53
  • @ValentinMarle You are right. argparse does not handle argument complexity in easy way. – Nilesh Bhave Jan 09 '20 at 13:50
  • If `-a` and `-b` are required, why make them options? – chepner Jan 09 '20 at 14:15
  • @chepner Arguments flags can help when users are trying use your script. You can keep the arguments require with the following parameter `parser.add_argument("-a", required=True)` – Valentin M. Jan 09 '20 at 14:30
  • I realize that, but I disagree that `script -a foo` is any easier than `script foo`, unless you are converting a configuration file into a command line. – chepner Jan 09 '20 at 14:35
  • You can clean up your code by dropping all the `str` type stuff. `sys.argv` is a list of strings, and `argparse` does not change that (unless you use some other `type` parameter). The default default is `None`, which is easy to test with `if args.foo is None:...`. – hpaulj Jan 09 '20 at 18:19
  • I explored generalizing the `mutually_exclusive_group` mechanism to include nested groups and other logic rules (https://bugs.python.org/issue11588). Generalizing groups isn't hard; the logic a bit harder, but generating a meaningful usage much harder. Even your custom usage is hard to read. – hpaulj Jan 09 '20 at 18:25
  • I don't see how doing the testing in `argparse` could be more efficient. The required amount of processing will be just as much if not more (general purpose tools are more complex and usually slower). – hpaulj Jan 09 '20 at 18:29
  • Rules 4 &5 are confusing. Is it just a difference in order, `c` before `f` or the reverse? Post parsing checking will loose that distinction. Why are `e` and `f` separate? Why not a combined `--ef` with `nargs=2? From a user stand point, is this the clearest, easiest design? – hpaulj Jan 09 '20 at 18:39

1 Answers1

1

Your issue is using multiple parser, arguments groups might help you.

You then have to check yourself that arguments comply with your conditions, and raise argparse.ArgumentError when necessary.

Here is a code sample that could do the trick. The arguments_group serve only to document the -h parameter (argparser help)

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-a", required=True)
parser.add_argument("-b", required=True)
first = parser.add_argument_group("group 1", "None, one or both")
first.add_argument("-c")
first.add_argument("-d")
second = parser.add_argument_group("group 2", "None or both")
second.add_argument("-e")
second.add_argument("-f")

args = parser.parse_args()

a = args.a
b = args.b
c = args.c
d = args.d
e = args.e
f = args.f

if (e is not None and f is None) or (e is None and f is not None):
    if c is None and d is None:
        raise argparse.ArgumentError("Both e and f arguments are required")
    elif c is None or d is not None:
        raise argparse.ArgumentError("e and f are not supported with c or d")
if (c is not None or d is not None) and (e is not None or f is not None):
    raise argparse.ArgumentError("c and d are not supported with e and f")
Valentin M.
  • 520
  • 5
  • 19
  • First I tried to use the same method. But that does not work. The link that you mentioned earlier, shows following : ```The add_argument_group() method returns an argument group object which has an add_argument() method just like a regular ArgumentParser. When an argument is added to the group, the parser treats it just like a normal argument, but displays the argument in a separate group for help messages.``` So, in short, its just a group to display but it does not solve the dependency problem. – Nilesh Bhave Jan 09 '20 at 16:05
  • 1
    Yes, the argument_group serve only for documentation. You still have to perform the arguments checking after the parsing. I edited my answer to add the checking part. – Valentin M. Jan 09 '20 at 16:13