Python 根据参数数量点击不同的命名参数

Python click different named arguments based on number of arguments

如何使用 Python click 库实现以下概要?

Usage: app CMD [OPTIONS] [FOO] [BAR]
       app CMD [OPTIONS] [FOOBAR]

我不知道我是否能够根据给定参数的数量为同一命令传递两组不同的命名参数。也就是说,如果只传递了一个参数,则为 foobar,但如果传递了两个参数,则为 foobar.

此类实现的代码表示如下所示(前提是您可以使用函数重载,但您不能)

@click.command()
@click.argument('foo', required=False)
@click.argument('bar', required=False)
def cmd(foo, bar):
    # ...

@click.command()
@click.argument('foobar', required=False)
def cmd(foobar):
    # ...

您可以通过创建自定义 click.Command class 添加多个命令处理程序,每个命令处理程序具有不同数量的参数。如果参数不是严格必需的,那么最好调用哪个命令处理程序存在一些歧义,但这主要可以通过使用适合传递的命令行的第一个签名来处理。

自定义Class

class AlternateArgListCmd(click.Command):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.alternate_arglist_handlers = [(self, super())]
        self.alternate_self = self

    def alternate_arglist(self, *args, **kwargs):
        from click.decorators import command as cmd_decorator

        def decorator(f):
            command = cmd_decorator(*args, **kwargs)(f)
            self.alternate_arglist_handlers.append((command, command))

            # verify we have no options defined and then copy options from base command
            options = [o for o in command.params if isinstance(o, click.Option)]
            if options:
                raise click.ClickException(
                    f'Options not allowed on {type(self).__name__}: {[o.name for o in options]}')
            command.params.extend(o for o in self.params if isinstance(o, click.Option))
            return command

        return decorator

    def make_context(self, info_name, args, parent=None, **extra):
        """Attempt to build a context for each variant, use the first that succeeds"""
        orig_args = list(args)
        for handler, handler_super in self.alternate_arglist_handlers:
            args[:] = list(orig_args)
            self.alternate_self = handler
            try:
                return handler_super.make_context(info_name, args, parent, **extra)
            except click.UsageError:
                pass
            except:
                raise

        # if all alternates fail, return the error message for the first command defined
        args[:] = orig_args
        return super().make_context(info_name, args, parent, **extra)

    def invoke(self, ctx):
        """Use the callback for the appropriate variant"""
        if self.alternate_self.callback is not None:
            return ctx.invoke(self.alternate_self.callback, **ctx.params)
        return super().invoke(ctx)

    def format_usage(self, ctx, formatter):
        """Build a Usage for each variant"""
        prefix = "Usage: "
        for _, handler_super in self.alternate_arglist_handlers:
            pieces = handler_super.collect_usage_pieces(ctx)
            formatter.write_usage(ctx.command_path, " ".join(pieces), prefix=prefix)
            prefix = " " * len(prefix)

使用自定义 Class:

要使用自定义 class,请将其作为 cls 参数传递给 click.command 装饰器,例如:

@click.command(cls=AlternateArgListCmd)
@click.argument('foo')
@click.argument('bar')
def cli(foo, bar):
    ...

然后在命令上使用 alternate_arglist() 装饰器添加另一个 具有不同参数的命令处理程序。

@cli.alternate_arglist()
@click.argument('foobar')
def cli_one_param(foobar):
    ...

这是如何工作的?

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

在这种情况下我们添加一个新的装饰器方法:alternate_arglist(),并覆盖三个方法:make_context(), invoke() & format_usage()。覆盖的 make_context() 方法检查哪个命令处理程序变体与传递的参数数量匹配,覆盖的 invoke() 方法用于调用适当的命令处理程序变体,覆盖的 format_usage() 是用于创建显示各种用法的帮助消息。

测试代码:

import click


@click.command(cls=AlternateArgListCmd)
@click.argument('foo')
@click.argument('bar')
@click.argument('baz')
@click.argument('bing', required=False)
@click.option('--an-option', default='empty')
def cli(foo, bar, baz, bing, an_option):
    """Best Command Ever!"""
    if bing is not None:
        click.echo(f'foo bar baz bing an-option: {foo} {bar} {baz} {bing} {an_option}')
    else:
        click.echo(f'foo bar baz an-option: {foo} {bar} {baz} {an_option}')


@cli.alternate_arglist()
@click.argument('foo')
@click.argument('bar')
def cli_two_param(foo, bar, an_option):
    click.echo(f'foo bar an-option: {foo} {bar} {an_option}')


@cli.alternate_arglist()
@click.argument('foobar', required=False)
def cli_one_param(foobar, an_option):
    click.echo(f'foobar an-option: {foobar} {an_option}')


if __name__ == "__main__":
    commands = (
        '',
        'p1',
        'p1 p2 --an-option=optional',
        'p1 p2 p3',
        'p1 p2 p3 p4 --an-option=optional',
        'p1 p2 p3 p4 p5',
        '--help',
    )

    import sys, time
    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            cli(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

测试结果:

Click Version: 7.1.2
Python Version: 3.8.5 (tags/v3.8.5:580fbb0, Jul 20 2020, 15:57:54) [MSC v.1924 64 bit (AMD64)]
-----------
>
foobar an-option: None empty
-----------
> p1
foobar an-option: p1 empty
-----------
> p1 p2 --an-option=optional
foo bar an-option: p1 p2 optional
-----------
> p1 p2 p3
foo bar baz an-option: p1 p2 p3 empty
-----------
> p1 p2 p3 p4 --an-option=optional
foo bar baz bing an-option: p1 p2 p3 p4 optional
-----------
> p1 p2 p3 p4 p5
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
       test_code.py [OPTIONS] FOO BAR
       test_code.py [OPTIONS] [FOOBAR]
Try 'test_code.py --help' for help.

Error: Got unexpected extra argument (p5)
-----------
> --help
Usage: test_code.py [OPTIONS] FOO BAR BAZ [BING]
       test_code.py [OPTIONS] FOO BAR
       test_code.py [OPTIONS] [FOOBAR]

  Best Command Ever!

Options:
  --an-option TEXT
  --help            Show this message and exit.