在 Python argparse 中,是否可以配对 --no-something/--something 参数?

问题描述 投票:0回答:10

我正在编写一个程序,我希望在其中有这样的参数:

--[no-]foo   Do (or do not) foo. Default is do.

有没有办法让 argparse 为我做这件事?

python argparse
10个回答
23
投票

嗯,由于各种原因,到目前为止,没有一个答案是令人满意的。所以这是我自己的答案:

class ActionNoYes(argparse.Action):
    def __init__(self, opt_name, dest, default=True, required=False, help=None):
        super(ActionNoYes, self).__init__(['--' + opt_name, '--no-' + opt_name], dest, nargs=0, const=None, default=default, required=required, help=help)
    def __call__(self, parser, namespace, values, option_string=None):
        if option_string.starts_with('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

以及使用示例:

>>> p = argparse.ArgumentParser()
>>> p._add_action(ActionNoYes('foo', 'foo', help="Do (or do not) foo. (default do)"))
ActionNoYes(option_strings=['--foo', '--no-foo'], dest='foo', nargs=0, const=None, default=True, type=None, choices=None, help='Do (or do not) foo. (default do)', metavar=None)
>>> p.parse_args(['--no-foo', '--foo', '--no-foo'])
Namespace(foo=False)
>>> p.print_help()
usage: -c [-h] [--foo]

optional arguments:
  -h, --help       show this help message and exit
  --foo, --no-foo  Do (or do not) foo. (default do)

不幸的是,

_add_action
成员函数没有文档记录,因此就 API 支持而言这不是“官方”的。另外,
Action
主要是一个持有者类。它本身几乎没有行为。如果可以使用它来更多地自定义帮助消息,那就太好了。例如在开头说
--[no-]foo
。但这部分是由
Action
类之外的内容自动生成的。


15
投票

v3.9 添加了一个

action
类来执行此操作。来自文档(靠近
action
部分的末尾)

BooleanOptionalAction
argparse
中可用,并添加了对布尔操作的支持,例如
--foo
--no-foo

>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
>>> parser.parse_args(['--no-foo'])
Namespace(foo=False)

探索@wim 关于不互斥的评论。

In [37]: >>> parser = argparse.ArgumentParser()
    ...: >>> parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
Out[37]: BooleanOptionalAction(option_strings=['--foo', '--no-foo'], dest='foo', nargs=0, const=None, default=None, type=None, choices=None, help=None, metavar=None)

最后一行显示

add_argument
创建了一个
BooleanOptionalAction
Action 类。

具有各种输入:

In [38]: parser.parse_args('--foo'.split())
Out[38]: Namespace(foo=True)

In [39]: parser.parse_args('--no-foo'.split())
Out[39]: Namespace(foo=False)

In [40]: parser.parse_args([])
Out[40]: Namespace(foo=None)

In [41]: parser.parse_args('--no-foo --foo'.split())
Out[41]: Namespace(foo=True)

因此,您可以提供两个标志,最后一个标志生效,覆盖前一个标志生成的任何内容。就好像我们定义了两个

Actions
,具有相同的
dest
,但不同的
True/False
const。

关键是它定义了两个标志字符串:

option_strings=['--foo', '--no-foo']

这个新类的部分代码:

class BooleanOptionalAction(Action):
    def __init__(self,
                 option_strings,
                 dest,
                 ...):

        _option_strings = []
        for option_string in option_strings:
            _option_strings.append(option_string)

            if option_string.startswith('--'):
                option_string = '--no-' + option_string[2:]
                _option_strings.append(option_string)

     ...

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string in self.option_strings:
            setattr(namespace, self.dest, not option_string.startswith('--no-'))

因此,操作

__init__
定义了两个标志,而
__call__
检查
no
部分。


10
投票

add_mutually_exclusive_group()
argparse
有帮助吗?

parser = argparse.ArgumentParser()
exclusive_grp = parser.add_mutually_exclusive_group()
exclusive_grp.add_argument('--foo', action='store_true', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_true', help='do not do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'
print 'Starting program', 'with' if args.no_foo else 'without', 'no_foo'

这是运行时的样子:

./so.py --help
usage: so.py [-h] [--foo | --no-foo]

optional arguments:
  -h, --help  show this help message and exit
  --foo       do foo
  --no-foo    do not do foo

./so.py
Starting program without foo
Starting program without no_foo

./so.py --no-foo --foo
usage: so.py [-h] [--foo | --no-foo]
so.py: error: argument --foo: not allowed with argument --no-foo

这与以下互斥组中的内容不同,允许在程序中使用 neither 选项(并且我假设您需要 options 因为

--
语法)。这意味着其中之一:

parser.add_argument('--foo=', choices=('y', 'n'), default='y',
                    help="Do foo? (default y)")

如果这些是必需的(非可选),也许使用

add_subparsers()
就是您正在寻找的。

更新1

逻辑上不同,但也许更清晰:

...
exclusive_grp.add_argument('--foo', action='store_true', dest='foo', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_false', dest='foo', help='do not do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'

并运行它:

./so.py --foo
Starting program with foo
./so.py --no-foo
Starting program without foo
./so.py
Starting program without foo

10
投票

我修改了@Omnifarious的解决方案,使其更像标准操作:

import argparse

class ActionNoYes(argparse.Action):
    def __init__(self, option_strings, dest, default=None, required=False, help=None):

        if default is None:
            raise ValueError('You must provide a default with Yes/No action')
        if len(option_strings)!=1:
            raise ValueError('Only single argument is allowed with YesNo action')
        opt = option_strings[0]
        if not opt.startswith('--'):
            raise ValueError('Yes/No arguments must be prefixed with --')

        opt = opt[2:]
        opts = ['--' + opt, '--no-' + opt]
        super(ActionNoYes, self).__init__(opts, dest, nargs=0, const=None, 
                                          default=default, required=required, help=help)
    def __call__(self, parser, namespace, values, option_strings=None):
        if option_strings.startswith('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

您可以像添加任何标准选项一样添加是/否参数。您只需在

ActionNoYes
参数中传递
action
类即可:

parser = argparse.ArgumentParser()
parser.add_argument('--foo', action=ActionNoYes, default=False)

现在当你调用它时:

>> args = parser.parse_args(['--foo'])
Namespace(foo=True)
>> args = parser.parse_args(['--no-foo'])
Namespace(foo=False)
>> args = parser.parse_args([])
Namespace(foo=False)  

3
投票

编写你自己的子类。

class MyArgParse(argparse.ArgumentParser):
    def magical_add_paired_arguments( self, *args, **kw ):
        self.add_argument( *args, **kw )
        self.add_argument( '--no'+args[0][2:], *args[1:], **kw )

2
投票

为了好玩,这里是 S.Lott 答案的完整实现

import argparse

class MyArgParse(argparse.ArgumentParser):
    def magical_add_paired_arguments( self, *args, **kw ):
        exclusive_grp = self.add_mutually_exclusive_group()
        exclusive_grp.add_argument( *args, **kw )
        new_action = 'store_false' if kw['action'] == 'store_true' else 'store_true'
        del kw['action']
        new_help = 'not({})'.format(kw['help'])
        del kw['help']
        exclusive_grp.add_argument( '--no-'+args[0][2:], *args[1:], 
                           action=new_action,
                           help=new_help, **kw )

parser = MyArgParse()
parser.magical_add_paired_arguments('--foo', action='store_true',
                                    dest='foo', help='do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'

这是输出:

./so.py --help
usage: so.py [-h] [--foo | --no-foo]

optional arguments:
  -h, --help  show this help message and exit
  --foo       do foo
  --no-foo    not(do foo)

2
投票

扩展https://stackoverflow.com/a/9236426/1695680的答案

import argparse

class ActionFlagWithNo(argparse.Action):
    """
        Allows a 'no' prefix to disable store_true actions.
        For example, --debug will have an additional --no-debug to explicitly disable it.
    """
    def __init__(self, opt_name, dest=None, default=True, required=False, help=None):
        super(ActionFlagWithNo, self).__init__(
            [
                '--' + opt_name[0],
                '--no-' + opt_name[0],
            ] + opt_name[1:],
            dest=(opt_name[0].replace('-', '_') if dest is None else dest),
            nargs=0, const=None, default=default, required=required, help=help,
        )

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string.startswith('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

class ActionFlagWithNoFormatter(argparse.HelpFormatter):
    """
        This changes the --help output, what is originally this:

            --file, --no-file, -f

        Will be condensed like this:

            --[no-]file, -f
    """

    def _format_action_invocation(self, action):
        if action.option_strings[1].startswith('--no-'):
            return ', '.join(
                [action.option_strings[0][:2] + '[no-]' + action.option_strings[0][2:]]
                + action.option_strings[2:]
            )
        return super(ActionFlagWithNoFormatter, self)._format_action_invocation(action)


def main(argp=None):
    if argp is None:
        argp = argparse.ArgumentParser(
            formatter_class=ActionFlagWithNoFormatter,
        )
        argp._add_action(ActionFlagWithNo(['flaga', '-a'], default=False, help='...'))
        argp._add_action(ActionFlagWithNo(['flabb', '-b'], default=False, help='...'))

        argp = argp.parse_args()

这会产生如下帮助输出:

usage: myscript.py [-h] [--flaga] [--flabb]

optional arguments:
  -h, --help        show this help message and exit
  --[no-]flaga, -a  ...
  --[no-]flabb, -b  ...

要点版本在这里,欢迎请求:) https://gist.github.com/thorsummoner/9850b5d6cd5e6bb5a3b9b7792b69b0a5


2
投票

实际上我相信对此有更好的答案......

parser = argparse.ArgumentParser()
parser.add_argument('--foo',
                     action='store_true',
                     default=True,
                     help="Sets foo arg to True. If not included defaults to tru")

parser.add_argument('--no-foo',
                    action="store_const", 
                    const=False,
                    dest="foo",
                    help="negates --foo so if included then foo=False")
args = parser.parse_args()

0
投票

在看到这个问题和答案之前,我编写了自己的函数来处理这个问题:

def on_off(item):
    return 'on' if item else 'off'

def argparse_add_toggle(parser, name, **kwargs):
    """Given a basename of an argument, add --name and --no-name to parser

    All standard ArgumentParser.add_argument parameters are supported
    and fed through to add_argument as is with the following exceptions:
    name     is used to generate both an on and an off
             switch: --<name>/--no-<name>
    help     by default is a simple 'Switch on/off <name>' text for the
             two options. If you provide it make sure it fits english
             language wise into the template
               'Switch on <help>. Default: <default>'
             If you need more control, use help_on and help_off
    help_on  Literally used to provide the help text for  --<name>
    help_off Literally used to provide the help text for  --no-<name>
    """
    default = bool(kwargs.pop('default', 0))
    dest = kwargs.pop('dest', name)
    help = kwargs.pop('help', name)
    help_on  = kwargs.pop('help_on',  'Switch on {}. Default: {}'.format(help, on_off(defaults)))
    help_off = kwargs.pop('help_off', 'Switch off {}.'.format(help))

    parser.add_argument('--' + name,    action='store_true',  dest=dest, default=default, help=help_on)
    parser.add_argument('--no-' + name, action='store_false', dest=dest, help=help_off)

可以这样使用:

defaults = {
    'dry_run' : 0,
    }

parser = argparse.ArgumentParser(description="Fancy Script",
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
argparse_add_toggle(parser, 'dry_run', default=defaults['dry_run'],
                    help_on='No modifications on the filesystem. No jobs started.',
                    help_off='Normal operation')
parser.set_defaults(**defaults)

args = parser.parse_args()

帮助输出如下所示:

  --dry_run             No modifications on the filesystem. No jobs started.
  --no-dry_run          Normal operation

我更喜欢其他答案建议的子类化方法,而不是我的普通函数,因为它使使用它的代码更干净,更易于阅读。


此代码的优点是具有标准的默认帮助,而且还有

argparse.Action

help_on
来重新配置相当愚蠢的默认值。

也许有人可以整合。


0
投票

请参阅代码后的测试用例。

help_off

这是测试用例:

$ ./temp.py

不使用 foo 启动程序

$ ./temp.py --foo

用 foo 启动程序

$ ./temp.py --foo --no-foo

不使用 foo 启动程序

$ ./temp.py --foo --no-foo --foo

用 foo 启动程序

© www.soinside.com 2019 - 2024. All rights reserved.