首页 > 解决方案 > Python根据参数数量单击不同的命名参数

问题描述

如何使用 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):
    # ...

标签: pythoncommand-line-interfacepython-click

解决方案


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

自定义类

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)

使用自定义类:

要使用自定义类,请将其作为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参数覆盖此行为。因此,从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.

推荐阅读