复杂的应用

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

迟缓加载子命令

延迟子命令的加载可能会使大型CLI和导入速度较慢的CLI受益。支持此使用模式的接口包括 Group.list_commands()Group.get_command() 。一种习俗 Group 子类可以通过存储额外数据来实现延迟加载器 Group.get_command() 负责运行导入。

因为这种情况的主要情况是 Group 它懒惰地加载子命令,下面的示例显示了一个懒惰组实现。

警告

延迟加载Python代码可能导致难以跟踪错误、依赖于顺序的代码库中的循环导入以及其他令人惊讶的行为。建议仅将此技术与测试配合使用,该测试至少会运行 --help 在每个子命令上。这将保证每个子命令都可以成功加载。

定义懒惰组

以下是 Group 子类添加一个属性, lazy_subcommands ,它存储从子命令名到用于导入它们的信息的映射。

# in lazy_group.py
import importlib
import click

class LazyGroup(click.Group):
    def __init__(self, *args, lazy_subcommands=None, **kwargs):
        super().__init__(*args, **kwargs)
        # lazy_subcommands is a map of the form:
        #
        #   {command-name} -> {module-name}.{command-object-name}
        #
        self.lazy_subcommands = lazy_subcommands or {}

    def list_commands(self, ctx):
        base = super().list_commands(ctx)
        lazy = sorted(self.lazy_subcommands.keys())
        return base + lazy

    def get_command(self, ctx, cmd_name):
        if cmd_name in self.lazy_subcommands:
            return self._lazy_load(cmd_name)
        return super().get_command(ctx, cmd_name)

    def _lazy_load(self, cmd_name):
        # lazily loading a command, first get the module name and attribute name
        import_path = self.lazy_subcommands[cmd_name]
        modname, cmd_object_name = import_path.rsplit(".", 1)
        # do the import
        mod = importlib.import_module(modname)
        # get the Command object from that module
        cmd_object = getattr(mod, cmd_object_name)
        # check the result to make debugging easier
        if not isinstance(cmd_object, click.Command):
            raise ValueError(
                f"Lazy loading of {import_path} failed by returning "
                "a non-command object"
            )
        return cmd_object

使用LazyGroup定义CLI

使用 LazyGroup 定义好后,现在可以编写一个组来延迟加载其子命令,如下所示:

# in main.py
import click
from lazy_group import LazyGroup

@click.group(
    cls=LazyGroup,
    lazy_subcommands={"foo": "foo.cli", "bar": "bar.cli"},
    help="main CLI command for lazy example",
)
def cli():
    pass
# in foo.py
import click

@click.group(help="foo command for lazy example")
def cli():
    pass
# in bar.py
import click
from lazy_group import LazyGroup

@click.group(
    cls=LazyGroup,
    lazy_subcommands={"baz": "baz.cli"},
    help="bar command for lazy example",
)
def cli():
    pass
# in baz.py
import click

@click.group(help="baz command for lazy example")
def cli():
    pass

是什么触发了懒惰加载?

有几个事件可以通过运行 Group.get_command() 功能。有些是直觉的,有些则不是。

所有情况都是参照上面的示例描述的,假设主程序名为 cli

  1. 命令解析。如果用户运行 cli bar baz ,这必须首先解决 bar ,然后解决 baz 。每个子命令解析步骤都会执行延迟加载。

  2. 帮助文本呈现。为了获得子命令的简短帮助描述, cli --help 将加载 foobar 。请注意,它仍然不会加载 baz

  3. Shell补全。为了获得懒惰命令的子命令, cli <TAB> 将需要解析的子命令 cli 。此过程将触发延迟负载。

进一步推迟进口

有可能让这一过程变得更懒惰,但通常情况下,你越想推迟工作,这就越困难。

例如,子命令可以表示为自定义 Command 子类,该子类将导入命令延迟到调用它,但它提供 Command.get_short_help_str() 以便支持补全和帮助文本。更简单地,可以构造其回调函数将任何实际工作推迟到导入之后的命令。

此命令定义提供 foo ,但是与导入“真正的”回调函数相关联的任何工作都被推迟到调用时:

@click.command()
@click.option("-n", type=int)
@click.option("-w", type=str)
def foo(n, w):
    from mylibrary import foo_concrete

    foo_concrete(n, w)

因为Click从选项、参数和命令属性构建帮助文本和使用信息,所以它不知道底层函数正在以任何方式处理延迟导入。因此,所有Click提供的实用程序和功能都将在这样的命令上正常工作。