【问题标题】:Python Cement CLI - print help for subparser when invalid argument givenPython Cement CLI - 在给出无效参数时打印子解析器的帮助
【发布时间】:2020-07-01 21:44:38
【问题描述】:

我正在使用 Python 3.7 和 Cement 3.0.4。

我有一个带有控制器的基本应用程序,并且控制器有一个带有单个可选参数的命令。我看到的是,如果我向命令传递一个无效参数,我会得到一个无效参数错误,但我得到的是应用程序本身的“使用”输出,而不是控制器上的命令。这是一个例子:

from cement import App, Controller, ex

class Base(Controller):
    class Meta:
        label = 'base'


    @ex(help='example sub-command')
    def cmd1(self):
        print('Inside Base.cmd1()')

    @ex(
        arguments=[
            (['-n', '--name'],
             {'help': ''': The name you want printed out''',
              'dest': 'name',
              'required': False}),
            ],
        help=' the help for cmd2.')
    def cmd2(self):
        print(self.app.pargs.name)

此应用程序有一个名为 cmd2 的命令,它接受一个可选参数 -n,如帮助所述,它将被打印出来。所以如果我这样做:

with MyApp(argv=['cmd2', '-n', 'bob']) as app:
    app.run()

我将输出: bob 正如预期的那样。但是,如果我将无效参数传递给 cmd2:

with MyApp(argv=['cmd2', '-a', 'bob']) as app:
    app.run()

我明白了:

usage: myapp [-h] [-d] [-q] {cmd1,cmd2} ...
myapp: error: unrecognized arguments: -a bob

我希望看到的不是 myapp 的用法,而是 cmd2 命令的用法,类似于我对命令执行-h

with MyApp(argv=['cmd2', '-h']) as app:
    app.run()

输出

usage: myapp cmd2 [-h] [-n NAME]

optional arguments:
  -h, --help            show this help message and exit
  -n NAME, --name NAME  : The name you want printed out

我意识到这大部分都委托给 Argparse,而不是由 Cement 处理。我已经进行了一些调试,我看到嵌套了多个 ArgparseArgumentHandler 类。所以在上面的例子中,myapp 有一个ArgparseArgumentHandler,它的动作中有一个SubParsersAction,它有一个choices 字段,其中有一个包含我在控制器上的两个命令的映射,cmd1 和@987654336 @映射到他们自己的ArgparseArgumentHandler

当检测到无效参数时,它位于 myapp 的 ArgparseArgumentHandler 中,因此它在 myapp 上调用 print_usage(),而不是在 ArgparseArgumentHandler 上调用命令 cmd2

我对 Argparse 的了解有限,而且我确实觉得导航有点复杂。我现在能想到的唯一解决方法是子类化ArgparseArgumentHandler,并覆盖error(),并尝试确定错误是否是由于已识别的参数引起的,如果是,请尝试为它找到解析器.. 类似这样的伪代码:

class ArgparseArgumentOverride(ext_argparse.ArgparseArgumentHandler):

    def error(self, message):
         # determine if there are unknown args
        args, argv = self.parse_known_args(self.original_arguments, self.original_namespace)
        # we are in an error state and have unrecognized args
        if argv:
            controller_namespace = args.__controller_namespace__


            for action in self._actions:
                if action.choices is not None:
                    # we found an choice with our namespace
                    if action.choices[controller_namespace]:
                        command_parser= action.choices[controller_namespace]
                        # this should be the show_usage for the command
                        complete_command.print_usage(sys.stderr)

上面还是伪代码,实际上做这样的事情会感觉非常脆弱、容易出错和不可预测。我知道必须有更好的方法来做到这一点,我只是没有找到它。我已经研究了几个小时的文档和源代码,但仍然没有找到我要找的东西。谁能告诉我我错过了什么?任何关于如何在此处进行的建议将不胜感激。非常感谢!

【问题讨论】:

    标签: python python-3.x argparse cement


    【解决方案1】:

    我不熟悉cement,但正如您推断的那样,usage 是由argparse 生成的:

    In [235]: parser = argparse.ArgumentParser(prog='myapp')                                                             
    In [236]: parser.add_argument('-d');                                                                                 
    In [237]: sp = parser.add_subparsers(dest='cmd')                                                                     
    In [238]: sp1 = sp.add_parser('cmd1')                                                                                
    In [239]: sp2 = sp.add_parser('cmd2')                                                                                
    In [240]: sp2.add_argument('-n','--name');                                                                           
    
    In [241]: parser.parse_args('cmd2 -a'.split())                                                                       
    usage: myapp [-h] [-d D] {cmd1,cmd2} ...
    myapp: error: unrecognized arguments: -a
    

    如果错误与sp2 参数相关,则用法反映了这一点:

    In [242]: parser.parse_args('cmd2 -n'.split())                                                                       
    usage: myapp cmd2 [-h] [-n NAME]
    myapp cmd2: error: argument -n/--name: expected one argument
    

    但是unknown args 由主解析器处理。例如,如果我们改用parse_known_args

    In [245]: parser.parse_known_args('cmd2 foobar'.split())                                                             
    Out[245]: (Namespace(cmd='cmd2', d=None, name=None), ['foobar'])
    In [246]: parser.parse_known_args('cmd2 -a'.split())                                                                 
    Out[246]: (Namespace(cmd='cmd2', d=None, name=None), ['-a'])
    

    未知参数作为extras 列表返回。 parse_args 返回错误而不是 extras

    _SubParsers_Action.__call__中,相关代码为:

        # parse all the remaining options into the namespace
        # store any unrecognized options on the object, so that the top
        # level parser can decide what to do with them
    
        # In case this subparser defines new defaults, we parse them
        # in a new namespace object and then update the original
        # namespace for the relevant parts.
        subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
        for key, value in vars(subnamespace).items():
            setattr(namespace, key, value)
    
        if arg_strings:
            vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
            getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
    

    理论上,您可以构造一个备用_SubParsersAction 类(或子类),以不同方式处理arg_strings。将 parse_known_args 调用更改为 parse_args 可能就足够了:

        subnamespace = parser.parse_args(arg_strings, None)
    

    注意parse_args 调用parse_known_args,如果有未知数会引发错误:

    def parse_args(self, args=None, namespace=None):
        args, argv = self.parse_known_args(args, namespace)
        if argv:
            msg = _('unrecognized arguments: %s')
            self.error(msg % ' '.join(argv))
        return args
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-11
      • 2013-12-04
      • 1970-01-01
      • 2017-05-31
      • 2018-03-27
      • 1970-01-01
      相关资源
      最近更新 更多