复杂的应用程序

Click 旨在帮助创建复杂和简单的 CLI 工具。然而,它设计初衷是将任意系统嵌套在一起。例如,如果你曾经使用 Django,你会意识到它提供了一个命令行实用程序,但是 Celery 也有此功能。在 Django 中使用 Celery 时,这两个工具需要互相交互并进行交叉配置。

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

基本概念

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

上下文(Contexts)

无论何时执行 Click 命令,Context 都会创建一个对象,用于保存此特定调用的状态。它会记住解析的参数,创建它的命令,需要在函数结束时清理哪些资源,等等。它也可以选择性地保存应用程序定义的对象。

上下文对象构建一个链表,直到它们碰到最上面的一个。每个上下文链接到父上下文。这允许命令在另一个命令之下工作,并在那里存储它自己的信息,而不必担心改变父命令的状态。

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

大多数情况下,你没有看到上下文对象,但是当编写更复杂的应用程序时,它会派上用场。下面我们来看下一个概念。

调用约定(Calling Convention)

当执行 Click 命令回调时,它将所有非隐藏参数作为关键字参数传递。Contexts 显然不存在。但是,回调可以选择通过标记自己来传递给上下文对象 pass_context()

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

建立一个 Git 克隆

在这个例子中,我们想要构建一个类似于版本控制系统的命令行工具。像 Git 这样的系统通常会提供一个重载的命令,它已经接受了一些参数和配置,然后有额外的子命令来做其他事情。

根命令

在顶层,我们需要一个可以容纳所有命令的组。在这种情况下,我们使用 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)

让我们来看看其中含义,我们创建一个可以有子命令的组命令。当它被调用时,它将创建一个 Repo 类的实例。它将保持我们的命令行工具的状态。在这种情况下,它只是记住一些参数,但在这一点上它也可以加载配置文件等等。

这个状态对象接着被上下文(contexts)作为 obj 所记住。 这是一个特殊的属性,命令应该记住他们需要传递给他们的孩子。

同时,我们需要标记我们的功能 pass_context(), 否则,上下文对象将会把我们完全隐藏掉。

第一个子命令

让我们添加我们的第一个子命令,克隆命令:

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

所以现在我们有一个克隆命令,但是我们如何获得 repo? 一种方法是使用 pass_context() 函数,这个函数也会让我们的回调函数也获得我们之前 repo 掉的上下文(contexts)。 另一种方法,我可以直接使用 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,我们将总是得到一个 repo,而不是别的:

@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() (帮助定位对象)。如果找不到对象,则会引发错误。另一种行为是使用 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(repo)

你将看到:

$ cp
<Repo object at 0x7f18f0ddca90>