复杂的应用

Click旨在帮助创建复杂和简单的CLI工具。然而,其设计的威力在于能够任意地将系统嵌套在一起。例如,如果您曾经使用过django,那么您将认识到它提供了一个命令行实用程序,但芹菜也是如此。当将芹菜与django一起使用时,有两个工具需要相互交互并交叉配置。

在两个单独的Click命令行实用程序的理论世界中,它们可以通过将一个嵌套在另一个实用程序中来解决这个问题。例如,Web框架还可以加载消息队列框架的命令。

基本概念

要理解这是如何工作的,您需要理解两个概念:上下文和调用约定。

语境

每当执行click命令时, Context 对象被创建,该对象保持此特定调用的状态。它记住解析的参数、创建它的命令、在函数末尾需要清理哪些资源等等。它还可以选择保存应用程序定义的对象。

上下文对象构建一个链接列表,直到到达顶部。每个上下文都链接到父上下文。这允许一个命令在另一个命令下工作,并在其中存储自己的信息,而不必担心更改父命令的状态。

但是,因为父数据是可用的,所以可以根据需要导航到它。

大多数情况下,您看不到上下文对象,但在编写更复杂的应用程序时,它很有用。这使我们进入下一个阶段。

呼叫约定

当执行click命令回调时,它将所有非隐藏参数作为关键字参数传递。明显缺乏的是上下文。但是,回调可以选择通过用标记自身来传递给上下文对象。 pass_context() .

那么,如果不知道命令回调是否应该接收上下文,如何调用它呢?答案是上下文本身提供了一个助手函数 (Context.invoke() )这可以帮你。它接受回调作为第一个参数,然后正确地调用函数。

构建Git克隆

在本例中,我们希望构建一个类似于版本控制系统的命令行工具。像git这样的系统通常提供一个over arching命令,该命令已经接受了一些参数和配置,然后具有执行其他操作的额外子命令。

根命令

在顶层,我们需要一个能够容纳我们所有命令的组。在这种情况下,我们使用 click.group() 它允许我们在它下面注册其他的Click命令。

对于此命令,我们还希望接受一些配置工具状态的参数:

import os
import click


class Repo(object):
    def __init__(self, home=None, debug=False):
        self.home = os.path.abspath(home or '.')
        self.debug = debug


@click.group()
@click.option('--repo-home', envvar='REPO_HOME', default='.repo')
@click.option('--debug/--no-debug', default=False,
              envvar='REPO_DEBUG')
@click.pass_context
def cli(ctx, repo_home, debug):
    ctx.obj = Repo(repo_home, debug)

让我们理解这是怎么回事。我们创建了一个可以有子命令的group命令。当它被调用时,它将创建 Repo 类。这保存了我们的命令行工具的状态。在这种情况下,它只记住一些参数,但此时它还可以开始加载配置文件等。

然后,上下文将此状态对象记为 obj . 这是一个特殊的属性,在这个属性中,命令应该记住需要传递给孩子的内容。

为了使其有效,我们需要用 pass_context() 否则,上下文对象将完全对我们隐藏。

第一个子命令

让我们向其中添加第一个子命令clone命令:

@cli.command()
@click.argument('src')
@click.argument('dest', required=False)
def clone(src, dest):
    pass

现在我们有了一个克隆命令,但是如何访问repo呢?正如您所能想象的,一种方法是使用 pass_context() 函数,它将再次使我们的回调得到我们记忆回购的上下文传递。但是,这个装饰器的第二个版本称为 pass_obj() 它只传递存储的对象(在我们的例子中是repo):

@cli.command()
@click.argument('src')
@click.argument('dest', required=False)
@click.pass_obj
def clone(repo, src, dest):
    pass

交错命令

虽然与我们想要构建的特定程序无关,但对交错系统也有相当好的支持。例如,假设我们的版本控制系统有一个超级酷的插件,它需要大量的配置,并希望将自己的配置存储为 obj . 如果我们在下面附加另一个命令,我们会突然得到插件配置,而不是我们的repo对象。

解决这一问题的一个明显方法是在插件中存储对repo的引用,但是命令需要知道它附加在此类插件的下面。

有一个更好的系统可以利用上下文的关联性来构建。我们知道插件上下文链接到创建repo的上下文。因此,我们可以开始搜索上下文存储的对象是repo的最后一个级别。

为此提供的内置支持由 make_pass_decorator() 工厂,它将为我们创建查找对象(内部调用 Context.find_object() )在我们的例子中,我们知道我们想要找到最近的 Repo 对象,因此让我们为此制作一个装饰器:

pass_repo = click.make_pass_decorator(Repo)

如果我们现在使用 pass_repo 而不是 pass_obj 我们总是会得到回购而不是其他东西:

@cli.command()
@click.argument('src')
@click.argument('dest', required=False)
@pass_repo
def clone(repo, src, dest):
    pass

确保对象创建

上面的示例仅在外部命令创建了 Repo 对象并将其存储在上下文中。对于一些更高级的用例,这可能会成为一个问题。默认行为 make_pass_decorator() 是调用来的 Context.find_object() 找到目标。如果找不到目标, make_pass_decorator() 将引发错误。另一种行为是使用 Context.ensure_object() 它将找到对象,如果找不到,将创建一个对象并将其存储在最内部的上下文中。此行为也可以用于 make_pass_decorator() 旁路 ensure=True

pass_repo = click.make_pass_decorator(Repo, ensure=True)

在本例中,如果缺少对象,则最内部的上下文将获取创建的对象。这可能会替换先前放置的对象。在这种情况下,即使外部命令不运行,该命令仍保持可执行状态。要使其工作,对象类型需要有一个不接受参数的构造函数。

因此,它独立运行:

@click.command()
@pass_repo
def cp(repo):
    click.echo(isinstance(repo, Repo))

正如你所看到的:

$ cp
True