复杂的应用¶
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
。
命令解析。如果用户运行
cli bar baz
,这必须首先解决bar
,然后解决baz
。每个子命令解析步骤都会执行延迟加载。帮助文本呈现。为了获得子命令的简短帮助描述,
cli --help
将加载foo
和bar
。请注意,它仍然不会加载baz
。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提供的实用程序和功能都将在这样的命令上正常工作。