Python Click - 从配置文件中提供参数和选项

Cal*_*vin 10 python command-line-interface python-click

鉴于以下计划:

#!/usr/bin/env python
import click

@click.command()
@click.argument("arg")
@click.option("--opt")
@click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
    print("arg: {}".format(arg))
    print("opt: {}".format(opt))
    print("config_file: {}".format(config_file))
    return

if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)

我可以使用命令行提供的参数和选项来运行它.

$ ./click_test.py my_arg --config_file my_config_file
arg: my_arg
opt: None
config_file: my_config_file
Run Code Online (Sandbox Code Playgroud)

如何提供一个配置文件(iniyamlpyjson?)到--config_file并接受内容作为参数和期权的价值?

例如,我想要my_config_file包含

opt: my_opt
Run Code Online (Sandbox Code Playgroud)

并有程序的输出显示:

$ ./click_test.py my_arg --config_file my_config_file
arg: my_arg
opt: my_opt
config_file: my_config_file
Run Code Online (Sandbox Code Playgroud)

我找到了callback看起来很有用的函数,但我找不到修改同一函数的兄弟参数/选项的方法.

Ste*_*uch 25

这可以通过以下click.Command.invoke()方法来完成:

自定义类:

def CommandWithConfigFile(config_file_param_name):

    class CustomCommandClass(click.Command):

        def invoke(self, ctx):
            config_file = ctx.params[config_file_param_name]
            if config_file is not None:
                with open(config_file) as f:
                    config_data = yaml.safe_load(f)
                    for param, value in ctx.params.items():
                        if value is None and param in config_data:
                            ctx.params[param] = config_data[param]

            return super(CustomCommandClass, self).invoke(ctx)

    return CustomCommandClass
Run Code Online (Sandbox Code Playgroud)

使用自定义类:

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

@click.command(cls=CommandWithConfigFile('config_file'))
@click.argument("arg")
@click.option("--opt")
@click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
Run Code Online (Sandbox Code Playgroud)

测试代码:

# !/usr/bin/env python
import click
import yaml

@click.command(cls=CommandWithConfigFile('config_file'))
@click.argument("arg")
@click.option("--opt")
@click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
    print("arg: {}".format(arg))
    print("opt: {}".format(opt))
    print("config_file: {}".format(config_file))


main('my_arg --config_file config_file'.split())
Run Code Online (Sandbox Code Playgroud)

检测结果:

arg: my_arg
opt: my_opt
config_file: config_file
Run Code Online (Sandbox Code Playgroud)


Aar*_*ron 7

我意识到这已经很老了,但是自从 Click 2.0 以来,有一个更简单的解决方案。以下是对文档中的示例的轻微修改。

此示例采用显式--port参数,它将采用环境变量或配置文件(具有该优先级)。

命令组

我们的代码:

import os
import click
from yaml import load
try:
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader


@click.group(context_settings={'auto_envvar_prefix': 'FOOP'})  # this allows for environment variables
@click.option('--config', default='~/config.yml', type=click.Path())  # this allows us to change config path
@click.pass_context
def foop(ctx, config):
    if os.path.exists(config):
        with open(config, 'r') as f:
            config = load(f.read(), Loader=Loader)
        ctx.default_map = config


@foop.command()
@click.option('--port', default=8000)
def runserver(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")


if __name__ == '__main__':
    foop()
Run Code Online (Sandbox Code Playgroud)

假设我们的配置文件 ( ~/config.yml) 如下所示:

runserver:
    port: 5000
Run Code Online (Sandbox Code Playgroud)

我们有第二个配置文件(位于~/config2.yml),如下所示:

runserver:
    port: 9000
Run Code Online (Sandbox Code Playgroud)

然后如果我们从 bash 调用它:

$ foop runserver
# ==> Serving on http://127.0.0.1:5000/
$ FOOP_RUNSERVER_PORT=23 foop runserver
# ==> Serving on http://127.0.0.1:23/
$ FOOP_RUNSERVER_PORT=23 foop runserver --port 34
# ==> Serving on http://127.0.0.1:34/
$ foop --config ~/config2.yml runserver
# ==> Serving on http://127.0.0.1:9000/
Run Code Online (Sandbox Code Playgroud)

单一命令

如果您不想使用命令组并希望拥有单个命令的配置:

import os
import click
from yaml import load
try:
    from yaml import CLoader as Loader
except ImportError:
    from yaml import Loader


def set_default(ctx, param, value):
    if os.path.exists(value):
        with open(value, 'r') as f:
            config = load(f.read(), Loader=Loader)
        ctx.default_map = config
    return value


@click.command(context_settings={'auto_envvar_prefix': 'FOOP'})
@click.option('--config', default='config.yml', type=click.Path(),
              callback=set_default, is_eager=True, expose_value=False)
@click.option('--port')
def foop(port):
    click.echo(f"Serving on http://127.0.0.1:{port}/")
Run Code Online (Sandbox Code Playgroud)

会给出类似的行为。