7

I want to use argparse to build a tool with subcommand. The possible syntax could be

/tool.py download --from 1234 --interval 60

/tool.py download --build 1432

/tool.py clean --numbers 10

So I want to use argparse to implement:

  1. ensure '--from' and '--interval' are always together used
  2. ensure '--build' is never used with other arguments

But I didn't find a way to pair '--from' and '--internal' to a group, then make the group is mutual exclusive with '--build'.

Below is my current code, and it only make the '--from' and '--build' are mutual exclusive. Neither ensure '--from' and '--interval' come together, nor ensure '--interval' and '--build' are mutual exclusive.

parser = argparse.ArgumentParser(description='A Tool')
subparsers = parser.add_subparsers(help='sub-command help')

#create the parser for the 'download' command
download_parser = subparsers.add_parser('download', help='download help')
download_parser.add_argument('--interval', dest='interval', type=int,help='interval help')
group = download_parser.add_mutually_exclusive_group()
group.add_argument('--from',type=int, help='from help')
group.add_argument('--build', type=int, help='interval help')

For example,

/tool.py download --from 1234

should not be allowed because '--from' must work with '--interval'. But my code accepts it silently.

And

/tool.py download --interval 1234 --build 5678

should not be allowed because '--build' can't be used with other argument. But my code accepts it too.

Any suggestion will be highly appreciated. Thanks.

Community
  • 1
  • 1
Landy
  • 379
  • 1
  • 4
  • 10

1 Answers1

6

You could use custom actions for this:

import argparse
import sys


class VerifyNoBuild(argparse.Action):
    def __call__(self, parser, args, values, option_string=None):
        # print 'No: {n} {v} {o}'.format(n=args, v=values, o=option_string)
        if args.build is not None:
            parser.error(
                '--build should not be used with --from or --interval')
        setattr(args, self.dest, values)


class VerifyOnlyBuild(argparse.Action):
    def __call__(self, parser, args, values, option_string=None):
        # print 'Only: {n} {v} {o}'.format(n=args, v=values, o=option_string)
        if getattr(args, 'from') is not None:
            parser.error('--from should not be used with --build')
        if getattr(args, 'interval') is not None:
            parser.error('--interval should not be used with --build')
        setattr(args, self.dest, values)

parser = argparse.ArgumentParser(description='A Tool')
subparsers = parser.add_subparsers(help='sub-command help')

# create the parser for the 'download' command
download_parser = subparsers.add_parser('download', help='download help')

download_parser.add_argument('--interval',
                             type=int, help='interval help',
                             action=VerifyNoBuild)
download_parser.add_argument('--from',
                             type=int, action=VerifyNoBuild)
download_parser.add_argument('--build',
                             type=int, action=VerifyOnlyBuild)

args = parser.parse_args('download --from 1234 --interval 60'.split())
print(args)
# Namespace(build=None, from=1234, interval=60)

args = parser.parse_args('download --build 1432'.split())
print(args)
# Namespace(build=1432, from=None, interval=None)

args = parser.parse_args('download --build 1432 --from 1234'.split())
print(args)
# usage: test.py download [-h] [--interval INTERVAL] [--from FROM] [--build BUILD]
# test.py download: error: --build should not be used with --from or --interval

args = parser.parse_args('download --build 1432 --interval 60'.split())
print(args)
# usage: test.py download [-h] [--interval INTERVAL] [--from FROM] [--build BUILD]
# test.py download: error: --build should not be used with --from or --interval

But really, I think this is shorter and simpler:

def parse_options():
    parser = argparse.ArgumentParser(description='A Tool')
    subparsers = parser.add_subparsers(help='sub-command help')

    #create the parser for the 'download' command
    download_parser = subparsers.add_parser('download', help='download help')
    download_parser.add_argument('--interval', type=int, help='interval help')
    download_parser.add_argument('--from', type=int)
    download_parser.add_argument('--build', type=int)

    opt=parser.parse_args()
    from_interval=[getattr(opt,key) is not None for key in ('from','interval')]
    if opt.build is not None:
        if any(from_interval):
            sys.exit('error!')
    elif not all(from_interval):
        sys.exit('error!')
    return opt
hans_meine
  • 1,881
  • 1
  • 17
  • 29
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thanks. Custom actions is very helpful. I modified your code to ensure --from and --interval are always used together. It works well. Thanks! :) – Landy Mar 03 '11 at 08:10
  • @Landy: Ah yes, I forgot about that condition. Did you manage to verify that condition from within the `VerifyNoBuild` class? If so, would you please post your solution? I'd like to see how that's done. I've modified the alternative solution to handle that condition. – unutbu Mar 03 '11 at 08:25
  • Sorry for the late response. Actually my code is quite simple. As you said, I modified the **VerifyNoBuild** class, added the following lines before **setattr()** is called: `if (args.begin is None) or (args.interval is None): sys.exit('--from and--interval must be used together')` – Landy Mar 07 '11 at 02:36
  • @Landy: Are you sure that works? The `VerifyNoBuild` action is typically called twice (when both `--from` and `--interval` are supplied). The first time `VerifyNoBuild.__call__` is called, either `getattr(args,'from')` or `getattr(args,'interval')` is going to be `None`. This would lead to `sys.exit` being called even though both arguments are being supplied. – unutbu Mar 07 '11 at 03:23
  • You're right. I only just tested the unacceptable arguments, didn't try the acceptable arguments. :((( – Landy Mar 07 '11 at 05:38