【问题标题】:Preserving the order of user-provided parameters with Python Click使用 Python Click 保留用户提供的参数的顺序
【发布时间】:2022-02-13 02:50:52
【问题描述】:

我正在尝试将 argparse 命令行界面 (CLI) 移植到 click。此 CLI 必须保持用户提供参数的顺序。在argparse 版本中,我使用this StackOverflow answer 来维护秩序。对于click,我不知道该怎么做。

我尝试创建一个自定义回调来按顺序存储参数和值,但是如果多次使用一个参数,回调会在它第一次看到匹配的参数时触发。

import click
import typing

class OrderParams:
    _options: typing.List[typing.Tuple[click.Parameter, typing.Any]] = []

    @classmethod
    def append(cls, ctx: click.Context, param: click.Parameter, value: typing.Any):
        cls._options.append((param, value))

@click.command()
@click.option("--animal", required=True, multiple=True, callback=OrderParams.append)
@click.option("--thing", required=True, multiple=True, callback=OrderParams.append)
def cli(*, animal, thing):
    click.echo("Got this order of parameters:")
    for param, value in OrderParams._options:
        print("  ", param.name, value)

if __name__ == "__main__":
    cli()

电流输出:

$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
   animal ('cat', 'dog')
   thing ('rock',)

期望的输出:

$ python cli.py --animal cat --thing rock --animal dog
Got this order of parameters:
   animal 'cat'
   thing 'rock'
   animal 'dog'

click documentation describes the behavior 表示第一次遇到参数时调用一次回调,即使该参数被多次使用。

如果一个选项或参数在命令行上被拆分为多个位置,因为它是重复的 [...] 回调将根据第一个选项的位置触发。

【问题讨论】:

    标签: python command-line-interface python-click


    【解决方案1】:

    这可以通过使用自定义类覆盖click.Command 参数解析器调用来完成:

    自定义类:

    class OrderedParamsCommand(click.Command):
        _options = []
    
        def parse_args(self, ctx, args):
            # run the parser for ourselves to preserve the passed order
            parser = self.make_parser(ctx)
            opts, _, param_order = parser.parse_args(args=list(args))
            for param in param_order:
                type(self)._options.append((param, opts[param.name].pop(0)))
    
            # return "normal" parse results
            return super().parse_args(ctx, args)
    

    使用自定义类:

    然后要使用自定义命令,请将其作为 cls 参数传递给 command 装饰器,例如:

    @click.command(cls=OrderedParamsCommand)
    @click.option("--animal", required=True, multiple=True)
    @click.option("--thing", required=True, multiple=True)
    def cli(*, animal, thing):
        ....
    

    这是如何工作的?

    这是因为click 是一个设计良好的OO 框架。 @click.command() 装饰器 通常实例化一个click.Command 对象,但允许这种行为被覆盖 cls 参数。所以从我们的click.Command继承是一件相对容易的事情 自己的类并覆盖所需的方法。

    在这种情况下,我们覆盖 click.Command.parse_args() 并自己运行解析器以保存 传入的订单。

    测试代码:

    import click
    
    class OrderedParamsCommand(click.Command):
        _options = []
    
        def parse_args(self, ctx, args):
            parser = self.make_parser(ctx)
            opts, _, param_order = parser.parse_args(args=list(args))
            for param in param_order:
                type(self)._options.append((param, opts[param.name].pop(0)))
    
            return super().parse_args(ctx, args)
    
    
    @click.command(cls=OrderedParamsCommand)
    @click.option("--animal", required=True, multiple=True)
    @click.option("--thing", required=True, multiple=True)
    def cli(*, animal, thing):
        click.echo("Got this order of parameters:")
        for param, value in OrderedParamsCommand._options:
            print("  ", param.name, value)
    
    
    if __name__ == "__main__":
        cli('--animal cat --thing rock --animal dog'.split())
    

    结果:

    Got this order of parameters:
       animal cat
       thing rock
       animal dog
    

    【讨论】:

    • 感谢您的出色回答。我通过在OrderedParamsCommand.parse_args() 中设置self._options = [] 稍微修改了这一点,以避免在多个命令中使用相同的命令类时可能发生的冲突。然后,在cli 中,我使用ctx.command.options 从实例中获取值。 之所以有效,是因为 click 是一个设计良好的 OO 框架——我在你的其他答案中看到了这一点;)
    • @jakub 如果您认为该类有可能在同一上下文中被使用两次(单击通常不会出现这种情况),请将列表放在self 上,然后以@ 身份访问987654340@.
    • 我用 pytest 参数化了我的 cli 测试,并且该类在同一上下文中被多次使用
    • 这在不带参数调用脚本时不起作用,应该对opts[param.name] 进行类型检查。我提出了修改建议。编辑:我不能:(
    【解决方案2】:

    @StephenRauch 的回答是正确的,但它遗漏了一个小细节。调用 cli 函数,即不带任何参数的脚本将导致错误:

    type(self)._options.append((param, opts[param.name].pop(0)))
    AttributeError: 'NoneType' object has no attribute 'pop'
    

    解决此问题的方法是检查 opts[param.name] 是否不是 None。修改后的OrderedParamsCommand 看起来像:

    class OrderedParamsCommand(click.Command):
        _options = []
    
        def parse_args(self, ctx, args):
            # run the parser for ourselves to preserve the passed order
            parser = self.make_parser(ctx)
            opts, _, param_order = parser.parse_args(args=list(args))
            for param in param_order:
                # Type check
                option = opts[param.name]
                if option != None:
                    type(self)._options.append((param, option.pop(0)))
    
            # return "normal" parse results
            return super().parse_args(ctx, args)
    

    编辑:我发布了一个答案,因为我无法提出编辑建议

    编辑 2:呃,这不应该是解决方案。我刚刚用这个尝试了--help,它因为这个错误而崩溃了:

    type(self)._options.append((param, option.pop(0)))
    AttributeError: 'bool' object has no attribute 'pop'
    

    太糟糕了,我希望有一个合适的解决方案

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-02
      • 2020-09-19
      • 2013-01-12
      • 2023-01-23
      • 1970-01-01
      • 2015-08-26
      相关资源
      最近更新 更多