如何在使用argparse的CustomAction时停止处理参数

时间:2015-10-30 09:28:51

标签: python argparse pytest

我在argparse中为我的python项目实现了一个CustomAction。 CustomAction用于在命令行上指定任意数量的name=value对样式参数,即nargs='*'

class NameValueAction(argparse.Action):
    """ CustomAction for argparse to be able to process name,value \
       pairs  specified as command line arguments. Specified as

        $ python runner.py --env=target_env --props name1=value1 name2=value2 module/
    """
    def __call__(self, parser, namespace, values, option_string=None):
        for value in values:
            n, v = value.split('=')
            setattr(namespace, n, v)

问题是无法阻止__call__处理命令行上的module/参数。如何在不消耗__call__参数的情况下正确结束module/方法并允许它由runner.py处理?

PS:我已经尝试退出最后一个不是name=value的论点,但是这不起作用,因为模块已经被消耗了,我不知道如何重新使用它堆栈。

4 个答案:

答案 0 :(得分:2)

我会尝试您最新的自定义操作:

In [34]: parser=argparse.ArgumentParser()

In [35]: parser.add_argument('--env')
In [36]: parser.add_argument('--props',nargs='*',action=NameValueAction)
Out[36]: NameValueAction(option_strings=['--props'], dest='props', nargs='*', const=None, default=None, type=None, choices=None, help=None, metavar=None)

unrecognized arguments出现parse_args错误。您的操作正确定义为未知:

In [37]: args=parser.parse_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
usage: ipython2.7 [-h] [--env ENV] [--props [PROPS [PROPS ...]]]
ipython2.7: error: unrecognized arguments: module/
...

使用parse_known_args,我可以看到没有错误消息的args和extras:

In [38]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
Out[38]: 
(Namespace(env='target_env', name1='value1', name2='value2', props=None),
 ['module/'])

因此--props之后的所有字符串都作为values传递给该Action。它将值分配给命名空间,然后返回。 parse_known_args从名称空间中取出unrecognized值,并将它们放在extras列表中。

现在我将添加一个位置,希望它将采用module/字符串:

In [39]: parser.add_argument('foo')

In [40]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 module/'.split())
usage: ipython2.7 [-h] [--env ENV] [--props [PROPS [PROPS ...]]] foo
ipython2.7: error: too few arguments
...

糟糕,即使使用parse_known_args,也会出现其他错误。问题在于'模块/'仍然被--props给予,foo没有留下任何内容。 --props有一个*的标志,这意味着它会获得所有符合条件的参数(无-)。放置'模块/'在命名空间中unknown没有帮助。解析器不会重新评估此列表中的字符串。

我可以使用' - '表示后面的所有字符串都是位置。现在--props未收到或处理'模块\'。相反,下次处理位置时,foo会消耗它。

In [41]: parser.parse_known_args('--env=target_env --props name1=value1 name2=value2 -- module/'.split())
Out[41]: 
(Namespace(env='target_env', foo='module/', name1='value1', name2='value2', props=None),
 [])

另一个可选项,例如' - env'可以用来标记“道具”的结尾。参数:

In [42]: parser.parse_known_args('--props name1=value1 name2=value2 --env=target_env module/'.split())
Out[42]: 
(Namespace(env='target_env', foo='module/', name1='value1', name2='value2', props=None),
 [])

请注意,progs=None出现在命名空间中。这是因为解析器在解析开始时将所有Action默认值加载到Namespace中。您可以使用default=argparse.SUPPRESS来阻止这种情况。

请参阅此错误/问题,了解如何将参数分配给' *'可选,以及如何为后续定位保留一些:

http://bugs.python.org/issue9338 argparse optionals with nargs='?', '*' or '+' can't be followed by positionals

https://stackoverflow.com/a/33405960/901925是另一个最近的SO问题,涉及定期的位置,然后是两个'?' positionals。

正如我在评论中指出的那样,argparseoptparse不同。我相信optparse每个Action(或等效的)消耗尽可能多的字符串,剩下的就是后续的Action。在argparse个别操作中,无法访问主列表(arg_strings)。它是决定Action获取多少字符串的解析器。

argparse.py文件中的更多详细信息。它是parse_args相关部分的摘要。

 _parse_known_args(self, arg_strings, namespace):
     # arg_strings - master list of strings from sys.argv
     start_index = 0
     while start_index<amax:
         # step through arg_strings processing postionals and optionals
         consume_positionals()
         start_index = next_option_string_index
         start_index = consume_optional(start_index)

consume_optional(start_index): # function local to _parse_known_args
     ...
     start = start_index + 1
     arg_count = <fn of available arguments and nargs>
     stop = start + arg_count
     args = arg_strings[start:stop]
     <action = CustomAction.__call__>
     take_action(action, args, option_string)
     return stop

take_action(action, argument_strings, ...): # another local function
     # argument_strings is a slice of arg_strings
     argument_values = self._get_values(action, argument_strings)
     # _get_values passes strings through the action.type function
     action(self, namespace, argument_values, option_string)
     # no return

最终结果是,您的CustomAction.__call__获取了values列表,这些列表来自主arg_strings列表的一部分。它无法访问arg_strings,也无法访问该切片的startstop。因此,它无法改变字符串对自身或任何后续操作的分配。

另一个想法是将您无法解析的值放入self.dest

class NameValueAction(argparse.Action):
    def __call__(self, parser, namespace, values,    option_string=None):
        extras = []
        for value in values:
            try:
                n, v = value.split('=')
                setattr(namespace, n, v)
            except ValueError:
                extras.append(value)
        if len(extras):         
            setattr(namespace, self.dest, extras)

然后解析(没有foo位置)会产生:

In [56]: parser.parse_args('--props name1=value1 p1 name2=value2 module/'.split())
Out[56]: Namespace(env=None, name1='value1', name2='value2', props=['p1', 'module/'])

args.props现在包含['p1','module/']--props得到的字符串,但无法解析为n=v对。根据需要解析后可以对它们进行重新定位。

答案 1 :(得分:1)

没有办法*阻止&#39;模块/&#39;被消费,因为没有关联name or flags来表示它是一个单独的参数而不被--props消费。

我认为您已将--props设置为:

parser.add_argument('--props', nargs='*', action=NameValueAction)

这样会消耗尽可能多的args。您需要提供-m--module选项才能让argparse存储&#39;模块/&#39;分开。

否则,您可以将模块作为位置arg parser.add_argument('module')并在命令行上的--props之前指定它:

parser.add_argument('--env')
parser.add_argument('--props', nargs='*', action=NameValueAction)
parser.add_argument('module')

""" Usage:
$ python runner.py --env=target_env module/ --props name1=value1 name2=value2
or
$ python runner.py module/ --env=target_env --props name1=value1 name2=value2
"""

该过程为:

>>> parser.parse_args('--env=target_env module/ --props name1=value1 name2=value2'.split())
Namespace(env='target_env', module='module/', name1='value1', name2='value2', props=None)

顺便说一句,使用您现有的代码并且没有上面建议的更改,您只需在命令行中指定module=module,它就会像name=value对一样处理:

>>> parser.parse_args('--env=target_env --props name1=value1 name2=value2 module=module/'.split())
Namespace(env='target_env', module='module/', name1='value1', name2='value2', props=None)

*如果确实无法将其作为单独的arg使用,那么您必须在NameValueAction中处理它。我将您的__call__修改为:

def __call__(self, parser, namespace, values, option_string=None):
    for value in values:
        try:
            n, v = value.split('=')
            setattr(namespace, n, v)  # better to put this in the else clause actually
        except ValueError:  # "need more than 1 value to unpack"
                            # raised when there's no '=' sign
            setattr(namespace, 'module', value)

>>> parser.parse_args('--env=target_env --props name1=value1 name2=value2 MOARmodules/'.split())
Namespace(env='target_env', module='MOARmodules/', name1='value1', name2='value2', props=None)

当然,其缺点是剩下的行动有多复杂。上面实现的行为与action=store类似,只会将其应用于'module'

您也可以尝试尝试将值附加到sys.argv,但考虑到在您执行此操作时正在使用这些值,可能会产生意想不到的副作用,类似于您不应该这样做的原因。在迭代时插入/删除列表。

答案 2 :(得分:1)

在@ aneroid的线索中查看NameValueAction内的处理后,我通读了argparse模块以找到可行的方法。 Actionsargparse中执行命令行解析。 Action下的argparse在程序的命令行的一部分上被触发。 argparse维护一个由用户定义的默认Actions(例如:store, store_true, const等)和CustomAction对象的列表。然后将这些循环并在命令行的一部分上顺序处理以查找匹配并构建与每个Namespace对应的Action。在每次迭代中,argparse.Action可能会发现命令行的某些部分与Action处理的任何内容都不匹配并返回它们(在_UNRECOGNIZED_ARGS_ATTR字段中,该字段由属性{{1}标识} '_unrecognized_args')回到来电者

来自argparse.py#parse_known_args(..)

Namespace

如上所示,如果找到任何无法识别的参数,它们将返回try: namespace, args = self._parse_known_args(args, namespace) if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR)) delattr(namespace, _UNRECOGNIZED_ARGS_ATTR) return namespace, args except ArgumentError: err = _sys.exc_info()[1] self.error(str(err)) 中的调用者。 args类可以利用它来让它们由后面的任何其他NameValueAction或项目的(Actions)模块进行处理。因此,班级发生了变化:

runner.py

因此cmd行的工作原理如下:

class NameValueAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): for value in values: try: n, v = value.split('=') setattr(namespace, n, v) except ValueError: # when input has ended without an option, probably at module name setattr(namespace, '_unrecognized_args', values[values.index(value):])

如果在$ python runner.py --env=target_env --props name1=value1 name2=value2 module/之后指定了其他选项,--props将停止处理当前argparse并向前迭代。所以以下内容也可以使用

Action

答案 3 :(得分:1)

(回答是因为我还需要事先从列表中“吃掉”一些未知的参数,下面的解决方案相当通用。)

正如上面提到的@hpaulj,如果没有子类化ArgumentParser,使用位置参数将无法工作,因为解析器只是将所有内容传递给Action,但是如果你只想要解析选项并获得非选项参数返回列表(即将它们传递给不同的解析器),以下工作(至少在Python 3.4上):

#!/usr/bin/env python3

import argparse
import itertools

class EatUnknown(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, *args, **kwargs):
        nargs = argparse.REMAINDER
        super().__init__(option_strings, dest, nargs, *args, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        def all_opt_strings(parser):
            nested = (x.option_strings for x in parser._actions
                      if x.option_strings)
            return itertools.chain.from_iterable(nested)

        all_opts = list(all_opt_strings(parser))

        eaten = []
        while len(values) > 0:
            if values[0] in all_opts:
                break
            eaten.append(values.pop(0))
        setattr(namespace, self.dest, eaten)

        _, extras = parser._parse_known_args(values, namespace)
        try:
            getattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR).extend(extras)
        except AttributeError:
            setattr(namespace, argparse._UNRECOGNIZED_ARGS_ATTR, extras)

parser = argparse.ArgumentParser()
parser.add_argument("--foo", action="append")
parser.add_argument('--eatme', action=EatUnknown)
parser.add_argument('--eater', action=EatUnknown)

print(parser.parse_known_args())

可生产

$ ./argparse_eater.py --foo 1 AAA --eater 2 --unk-opt 3 --foo 4 BBB --eatme 5 --another-unk --foo 6 CCC
(Namespace(eater=['2', '--unk-opt', '3'], eatme=['5', '--another-unk'], foo=['1', '4', '6']), ['AAA', 'CCC', 'BBB'])

此示例“吃掉”任何非选项以及未知选项参数(其中nargs='*'无法使用,证明示例正确),但不是allow_abbrev兼容。

这个想法是使用一个简单的递归,这显然适用于代码是可重入的。可能不是最好的想法,但使用_unrecognized_args并不是更好。

鉴于OP,这适用于--props的多次出现。