命令和组

Click应用程序的结构定义为 Command ,它定义了单个命名的命令,以及 Group ,它定义了一个名称下的命令(或多个组)的嵌套集合。

回调调用

对于常规命令,每当命令运行时都会执行回调。如果脚本是唯一的命令,它将始终启动(除非参数回调阻止它)。例如,如果有人通过 --help 剧本)

对于团体来说,情况看起来有所不同。在这种情况下,只要引发子命令,就会触发回调。这实际上意味着当内部命令运行时,外部命令也会运行:

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    click.echo(f"Debug mode is {'on' if debug else 'off'}")

@cli.command()  # @cli, not @click!
def sync():
    click.echo('Syncing')

这里是这样的:

$ tool.py
Usage: tool.py [OPTIONS] COMMAND [ARGS]...

Options:
  --debug / --no-debug
  --help                Show this message and exit.

Commands:
  sync

$ tool.py --debug sync
Debug mode is on
Syncing

传递参数

Click“严格分隔命令和子命令之间的参数”。这意味着必须指定特定命令的选项和参数 之后 命令名本身,但是 之前 任何其他命令名。

这种行为已经可以通过预先定义的 --help 选择权。假设我们有一个叫做 tool.py ,包含名为 sub .

  • tool.py --help 将返回整个程序的帮助(列出子命令)。

  • tool.py sub --help 将返回的帮助 sub 子命令。

  • 但是 tool.py --help sub 会治疗 --help 作为主程序的参数。Click然后调用回调 --help ,这将打印帮助并在Click“可以处理子命令”之前中止程序。

嵌套处理和上下文

从前面的示例中可以看到,基本命令组接受一个调试参数,该参数将传递给它的回调,但不会传递给同步命令本身。sync命令只接受自己的参数。

这使得工具可以完全独立地执行操作,但是一个命令如何与嵌套的命令进行通信?答案是 Context .

每次调用命令时,都会创建一个新的上下文并与父上下文链接。通常,你看不到这些上下文,但它们就在那里。上下文与值一起自动传递给参数回调。命令还可以通过使用 pass_context() 装饰者。在这种情况下,上下文作为第一个参数传递。

上下文还可以携带可用于程序目的的程序指定对象。这意味着您可以构建这样的脚本:

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below)
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug

@cli.command()
@click.pass_context
def sync(ctx):
    click.echo(f"Debug is {'on' if ctx.obj['DEBUG'] else 'off'}")

if __name__ == '__main__':
    cli(obj={})

如果提供了对象,则每个上下文都会将对象传递给其子级,但在任何级别上,都可以覆盖上下文的对象。要联系父母, context.parent 可以使用。

除此之外,没有什么可以阻止应用程序修改全局状态,而不是向下传递对象。例如,您可以翻转一个全局 DEBUG 变量并用它来完成。

修饰命令

正如您在前面的示例中看到的,装饰器可以更改调用命令的方式。在后台实际发生的情况是,回调总是通过 Context.invoke() 方法自动正确调用命令(通过传递上下文或不传递上下文)。

当您想编写自定义装饰器时,这非常有用。例如,一个常见的模式是配置一个表示状态的对象,然后将其存储在上下文中,然后使用一个自定义装饰器来查找这种类型的最新对象,并将其作为第一个参数传递。

例如, pass_obj() decorator可以这样实现:

from functools import update_wrapper

def pass_obj(f):
    @click.pass_context
    def new_func(ctx, *args, **kwargs):
        return ctx.invoke(f, ctx.obj, *args, **kwargs)
    return update_wrapper(new_func, f)

这个 Context.invoke() 命令将以正确的方式自动调用函数,因此将使用 f(ctx, obj)f(obj) 取决于它本身是否用 pass_context() .

这是一个非常强大的概念,可用于构建非常复杂的嵌套应用程序;请参见 复杂的应用 更多信息。

不带命令的组调用

默认情况下,除非传送子命令,否则不会调用组。事实上,不提供命令会自动通过 --help 默认情况下。此行为可以通过传递 invoke_without_command=True 给一群人。在这种情况下,总是调用回调,而不是显示帮助页面。上下文对象还包括关于调用是否会转到子命令的信息。

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('I was invoked without subcommand')
    else:
        click.echo(f"I am about to invoke {ctx.invoked_subcommand}")

@cli.command()
def sync():
    click.echo('The subcommand')
$ tool
I was invoked without subcommand
$ tool sync
I am about to invoke sync
The subcommand

自定义组

您可以通过子类化来自定义组的行为,使其超越其接受的参数 click.Group

要覆盖的最常见方法是 get_command()list_commands()

下面的示例实现了一个基本的插件系统,该系统从文件夹中的Python文件加载命令。该命令是延迟加载的,以避免缓慢启动。

import importlib.util
import os
import click

class PluginGroup(click.Group):
    def __init__(self, name=None, plugin_folder="commands", **kwargs):
        super().__init__(name=name, **kwargs)
        self.plugin_folder = plugin_folder

    def list_commands(self, ctx):
        rv = []

        for filename in os.listdir(self.plugin_folder):
            if filename.endswith(".py"):
                rv.append(filename[:-3])

        rv.sort()
        return rv

    def get_command(self, ctx, name):
        path = os.path.join(self.plugin_folder, f"{name}.py")
        spec = importlib.util.spec_from_file_location(name, path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module.cli

cli = PluginGroup(
    plugin_folder=os.path.join(os.path.dirname(__file__), "commands")
)

if __name__ == "__main__":
    cli()

自定义类还可以与装饰符一起使用:

@click.group(
    cls=PluginGroup,
    plugin_folder=os.path.join(os.path.dirname(__file__), "commands")
)
def cli():
    pass

命令链

在一次调用中调用多个子命令非常有用。例如, my-app validate build upload 会调用 validate ,那么 build ,那么 upload 。要实现这一点,请传递 chain=True 在创建组时。

@click.group(chain=True)
def cli():
    pass

@cli.command('validate')
def validate():
    click.echo('validate')

@cli.command('build')
def build():
    click.echo('build')

您可以这样调用它:

$ my-app validate build
validate
build

使用链接时,有几个限制:

  • 只有最后一个命令可以使用 nargs=-1 否则解析器将无法找到进一步的命令。

  • 不能将组嵌套在链组下面。

  • 在命令行上,必须在链中每个命令的参数之前指定选项。

  • 这个 Context.invoked_subcommand 属性将是 '*' 因为解析器还不知道将运行的命令的完整列表。

命令管道

使用链接时,一种常见的模式是让每个命令处理上一个命令的结果。

要做到这一点,直接的方法是使用 make_pass_decorator() 将上下文对象传递给每个命令,并存储和读取该对象上的数据。

pass_ns = click.make_pass_decorator(dict, ensure=True)

@click.group(chain=True)
@click.argument("name")
@pass_ns
def cli(ns, name):
    ns["name"] = name

@cli.command
@pass_ns
def lower(ns):
    ns["name"] = ns["name"].lower()

@cli.command
@pass_ns
def show(ns):
    click.echo(ns["name"])
$ process Click show lower show
Click
click

另一种方法是收集每个命令返回的数据,然后在链的末尾对其进行处理。使用群组的 result_callback() 修饰符来注册在链完成后调用的函数。它被传递给返回值列表以及在组上注册的任何参数。

命令可以返回任何内容,包括函数。这里有一个这样的例子,其中每个子命令创建一个处理输入的函数,然后结果回调调用每个函数。该命令获取一个文件,处理每一行,然后输出它。如果没有给出子命令,则输出文件的内容不变。

@click.group(chain=True, invoke_without_command=True)
@click.argument("fin", type=click.File("r"))
def cli(fin):
    pass

@cli.result_callback()
def process_pipeline(processors, fin):
    iterator = (x.rstrip("\r\n") for x in input)

    for processor in processors:
        iterator = processor(iterator)

    for item in iterator:
        click.echo(item)

@cli.command("upper")
def make_uppercase():
    def processor(iterator):
        for line in iterator:
            yield line.upper()
    return processor

@cli.command("lower")
def make_lowercase():
    def processor(iterator):
        for line in iterator:
            yield line.lower()
    return processor

@cli.command("strip")
def make_strip():
    def processor(iterator):
        for line in iterator:
            yield line.strip()
    return processor

这是一步一步完成的,所以我们一步一步地完成它。

  1. 首先要做的是 group() 那是可以锁链的。此外,我们还指示Click以调用,即使没有定义子命令。如果不这样做,那么调用空管道将生成帮助页,而不是运行结果回调。

  2. 接下来我们要做的就是在我们的组中注册一个结果回调。此回调将使用一个参数调用,该参数是所有子命令的所有返回值的列表,然后是与组本身相同的关键字参数。这意味着我们可以轻松地访问输入文件,而不必使用上下文对象。

  3. 在这个结果回调中,我们创建一个输入文件中所有行的迭代器,然后通过所有子命令返回的所有回调传递这个迭代器,最后我们将所有行打印到stdout。

在这之后,我们可以注册尽可能多的子命令,并且每个子命令都可以返回一个处理器函数来修改行流。

需要注意的一点是,每次运行回调之后,Click都会关闭上下文。这意味着,例如,在 processor 因为文件将在那里关闭。这种限制不太可能改变,因为它会使资源处理更加复杂。为此,建议不要使用文件类型并通过手动打开文件 open_file() .

有关在管道处理方面也有所改进的更复杂的示例,请参见 imagepipe example 在Click存储库中。它实现了一个基于流水线的图像编辑工具,具有良好的内部结构。

覆盖默认值

默认情况下,参数的默认值从 default 定义默认值时提供的标志,但这不是唯一可以从中加载默认值的位置。另一个地方是 Context.default_map (词典)上下文。这允许从配置文件加载默认值以覆盖常规默认值。

如果您从另一个包中插入了一些命令,但对默认值不满意,那么这将非常有用。

可以为每个子命令任意嵌套默认映射:

default_map = {
    "debug": True,  # default for a top level option
    "runserver": {"port": 5000}  # default for a subcommand
}

当调用脚本时,可以提供默认映射,或者在任何时候由命令重写。例如,顶级命令可以从配置文件加载默认值。

示例用法:

import click

@click.group()
def cli():
    pass

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

if __name__ == '__main__':
    cli(default_map={
        'runserver': {
            'port': 5000
        }
    })

行动中:

$ cli runserver
Serving on http://127.0.0.1:5000/

上下文默认值

Changelog

在 2.0 版本加入.

从click 2.0开始,您不仅可以在调用脚本时,还可以在声明命令的装饰器中覆盖上下文的默认值。例如,前面的示例定义了一个自定义 default_map 这也可以在装饰器中完成。

此示例与前一个示例相同:

import click

CONTEXT_SETTINGS = dict(
    default_map={'runserver': {'port': 5000}}
)

@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
    pass

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

if __name__ == '__main__':
    cli()

同样的例子是:

$ cli runserver
Serving on http://127.0.0.1:5000/

命令返回值

Changelog

在 3.0 版本加入.

click 3.0中的一个新介绍是完全支持命令回调的返回值。这使得以前难以实现的一系列功能得以实现。

本质上,任何命令回调现在都可以返回值。该返回值被冒泡到某些接收器。这方面的一个用例已经在以下示例中展示 命令链 其中已经演示了链接组可以具有处理所有返回值的回调。

在click中使用命令返回值时,您需要知道:

  • 命令回调的返回值通常从 Command.invoke() 方法。这条规则的例外情况与 Group S:

    • 在组中,返回值通常是所调用子命令的返回值。此规则的唯一例外是,如果在没有参数和 invoke_without_command 启用。

    • 如果为链接设置了组,则返回值是所有子命令结果的列表。

    • 组的返回值可以通过 Group.result_callback 。这是通过链模式下所有返回值的列表调用的,或者在非链式命令的情况下调用单个返回值。

  • 返回值从 Context.invoke()Context.forward() 方法。这在内部希望调用另一个命令的情况下很有用。

  • Click对返回值没有任何硬要求,本身也不使用这些返回值。这允许返回值用于自定义修饰符或工作流(如命令链接示例中所示)。

  • 当Click脚本作为命令行应用程序调用时(通过 Command.main() )该返回值被忽略,除非 standalone_mode 被禁用,在这种情况下,它会冒泡通过。