如何使用 Python 的 Click 包 return 来自装饰器的参数值?

How do I return a parameter value from a decorator using Python's Click package?

在我的 CLI 应用程序中,我有一个配置文件,其中包含有关如何连接到 API 的信息。可以为多个环境设置此配置:(test/prod/etc)。

我想确保传递的环境存在于配置文件中,所以我创建了一个装饰器并将其建模为 confirmation_option

这是问题的一个小例子:

# -*- coding: utf-8 -*-

import sys
import click

TEST_DICT = {
    'production': 'ProdKey'
}


def environment_option(*param_decls, **attrs):
    """
    Check that the passed environment exists
    """
    def decorator(f):
        def callback(ctx, param, value):
            if not value:
                ctx.abort()
            try:
                TEST_DICT[value]
            except KeyError:
                click.secho("Bad environment provided: {}".format(value), fg='red')
                sys.exit(1)

        attrs.setdefault('callback', callback)
        return click.option(*(param_decls or ('--environment',)), **attrs)(f)
    return decorator


@click.group()
@click.pass_context
def cli(ctx):
    """My CLI"""

@cli.command()
@environment_option('-e', '--environment', help="Environment to associate this key with", required=True)
@click.pass_context
def show_keys(ctx, environment):
    """
    List the available keys for the selected environment
    """
    click.echo("Environment:", type(environment))


if __name__ == "__main__":
    sys.exit(cli())  # pragma: no cover

如果我传入无效环境,它会按预期工作。

$ python cli show-keys -e notreal
Bad environment provided: notreal

我遇到的问题是,一个有效的环境——不会在装饰器中触发错误的环境——在返回到 show_keys 函数时没有值。是 NoneType:

$ python cli show-keys -e production

Traceback (most recent call last):
  File "/home/devuser/.virtualenvs/mycli/bin/mycli", line 11, in <module>
    load_entry_point('mycli', 'console_scripts', 'mycli')()
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/home/devuser/repositories/mycli/mycli/mycli/commands/config_cmds.py", line 60, in show_keys
    click.echo("Environment:", type(environment))
  File "/home/devuser/.virtualenvs/mycli/lib/python3.6/site-packages/click/utils.py", line 260, in echo
    file.write(message)
AttributeError: type object 'NoneType' has no attribute 'write'

如何从这个装饰器而不是 NoneType 取回我的 environment 的值?

Python 函数没有 return 语句,return None。所以在回调中,需要return的值为TEST_DICT[value]。类似于:

def callback(ctx, param, value):
    if not value:
        ctx.abort()
    try:
        return TEST_DICT[value]
    except KeyError:
        ....

测试代码:

import sys
import click

TEST_DICT = {
    'production': 'ProdKey'
}


def environment_option(*param_decls, **attrs):
    """
    Check that the passed environment exists
    """

    def decorator(f):
        def callback(ctx, param, value):
            if not value:
                ctx.abort()
            try:
                return TEST_DICT[value]
            except KeyError:
                click.secho("Bad environment provided: {}".format(value),
                            fg='red')
                sys.exit(1)

        attrs.setdefault('callback', callback)
        return click.option(*(param_decls or ('--environment',)), **attrs)(f)

    return decorator


@click.group()
@click.pass_context
def cli(ctx):
    """My CLI"""


@cli.command('show-keys')
@environment_option('-e', '--environment',
                    help="Environment to associate this key with",
                    required=True)
@click.pass_context
def show_keys(ctx, environment):
    """
    List the available keys for the selected environment
    """
    click.echo("Environment: {}".format(environment))


if __name__ == "__main__":
    commands = (
        'show-keys -e production',
        'show-keys -e notreal',
        'show-keys --help',
        '--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: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> show-keys -e production
Environment: ProdKey
-----------
> show-keys -e notreal
Bad environment provided: notreal
-----------
> show-keys --help
Usage: test.py show-keys [OPTIONS]

  List the available keys for the selected environment

Options:
  -e, --environment TEXT  Environment to associate this key with  [required]
  --help                  Show this message and exit.
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  My CLI

Options:
  --help  Show this message and exit.

Commands:
  show-keys  List the available keys for the selected...
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  My CLI

Options:
  --help  Show this message and exit.

Commands:
  show-keys  List the available keys for the selected...