【问题标题】:How to stop processing arguments when using argparse's CustomAction使用 argparse 的 CustomAction 时如何停止处理参数
【发布时间】:2016-01-30 16:21:53
【问题描述】:

我在 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/ 参数。如何在不消耗 module/ 参数的情况下适当地结束 __call__ 方法并允许 runner.py 处理它?

PS:我尝试退出不是 name=value 的最后一个参数,但这不起作用,因为模块已经被消耗,我不知道如何将它放回堆栈。

【问题讨论】:

  • 解析器根据其nargs 和任何后续操作的nargs 分配操作获取的values 列表。与optparse 相比,此操作不会从某些主列表中剥离值。玩转位置、可选和nargs 的不同组合,看看每个有多少值。
  • 另请参阅stackoverflow.com/questions/33301000/…。它从多个--flag=key:value 字符串构建字典。

标签: python argparse pytest


【解决方案1】:

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

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)

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

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_argsunrecognized 值从命名空间中取出,并将它们放入 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。问题是'module/'仍然被分配给--props,没有给foo留下任何东西。 --props 有一个 * nargs,这意味着它获取所有符合条件的参数(没有 -)。将“module/”作为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”可用于标记“--props”参数的结束:

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 默认值加载到命名空间中。您可以使用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 问题,涉及一个常规位置,后跟两个“?”定位。

正如我在评论中指出的,argparseoptparse 不同。我相信optparse 每个动作(或等效的)都会消耗尽可能多的字符串,并将其余字符串留给后续动作。在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 对。这些可以根据需要在解析后重新分配。

【讨论】:

  • “我会试试你最新的自定义操作” - 指的是@tsps 还是我? Here's mine on PB.
  • 我正在尝试 tsps 的第二次尝试,将值放入“无法识别”属性中。它没有帮助。
【解决方案2】:

没有办法* 阻止“module/”被使用,因为它没有关联的name or flags 表明它是一个单独的参数,不会被--props 使用。

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

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

这样会消耗尽可能多的参数。您需要提供 -m--module 选项以让 argparse 分别存储“模块/”。

否则,您可以将模块作为位置参数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)

* 如果你真的不能把它作为一个单独的参数,那么你必须在 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,但考虑到在您执行此操作时会消耗这些值,可能会产生意想不到的副作用,类似于您在迭代时不应从列表中插入/删除的原因它。

【讨论】:

  • 感谢详细的解释!该程序的早期行为没有--props,并且在没有参数的情况下指定了模块。 --props 的引入稍微扰乱了平静,因此需要更改一大堆依赖脚本。为了保持向后兼容性,我不想使用额外的标志来解析module 参数。另一个麻烦是module 是一个可选参数,使得NameValueAction 难以处理。在您指出在 NameValueAction 中处理此问题后,我想我找到了一种方法。见下文。
  • @tsps 然后让 module 成为具有默认值的位置参数,并在 module 之后指定 --props。所以添加了新的东西,你可以处理指定的模块和缺失的模块,因为那将是唯一的位置参数。
  • 确实 - 这也是一个合理的解决方案!
【解决方案3】:

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

来自argparse.py#parse_known_args(..)

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) 模块进行处理。类因此改变:

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):])

所以 cmd 行的工作方式如下:

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

如果在--props 之后指定了其他选项,argparse 将停止处理当前的Action 并向前迭代。所以以下也将起作用

$ python runner.py --env=target_env --props name1=value1 name2=value2 --timeout=300 module/

【讨论】:

  • _UNRECOGNIZED_ARGS_ATTR 可能是一个很好的用法。如果您使用parse_known_args,我希望在extras 的列表中看到“module/”。使用此属性的唯一已定义 Action 类是 subparsers
  • 我认为您对解析方法的概述需要一些改进。稍后我会尝试添加一些内容。
  • 我一直在寻找类似于'_unrecognized_args' 的东西,或者基本上是一种将某些东西添加到“解析”队列/列表的机制。太糟糕了,它没有记录在案,或者没有公开的方法来访问和修改它。
  • _unrecognized_args 只是子解析器将extras 传递回主解析器的一种方式。它不会回到队列中。
【解决方案4】:

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

正如上面提到的@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 的多次出现。

【讨论】:

    猜你喜欢
    • 2020-03-04
    • 2017-11-26
    • 1970-01-01
    • 1970-01-01
    • 2022-06-11
    • 2017-01-14
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多