Python argparse 子命令的依赖与冲突

7 投票
1 回答
5580 浏览
提问于 2025-04-16 12:48

我想用 argparse 来创建一个带有子命令的工具。可能的用法示例如下:

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

/tool.py download --build 1432

/tool.py clean --numbers 10

所以我想用 argparse 来实现以下功能:

  1. 确保 '--from' 和 '--interval' 总是一起使用
  2. 确保 '--build' 不能和其他参数一起使用

但是我找不到方法把 '--from' 和 '--interval' 组合成一组,然后让这一组和 '--build' 互斥。

下面是我现在的代码,它只让 '--from' 和 '--build' 互斥。既没有确保 '--from' 和 '--interval' 一起出现,也没有确保 '--interval' 和 '--build' 互斥。

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')

举个例子,

/tool.py download --from 1234

这个用法是不允许的,因为 '--from' 必须和 '--interval' 一起使用。但我的代码却默默接受了这个用法。

还有,

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

这个用法也不允许,因为 '--build' 不能和其他参数一起使用。但我的代码也接受了这个。

任何建议都非常感谢。谢谢。

1 个回答

6

你可以使用自定义动作来实现这个功能:

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

不过我觉得其实有更简单、更短的方法:

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

撰写回答