高级模式

除了常见的功能外,Click还提供了一些高级功能。

回调和渴望期权

有时,您希望参数来完全更改执行流程。例如,当您想要拥有一个 --version 参数,用于打印版本,然后退出应用程序。

注:a的实际实现 --version 单击作为中提供了可重复使用的参数 click.version_option() . 这里的代码只是如何实现此类标志的示例。

在这种情况下,您需要两个概念:渴望参数和回调。 渴望参数是在其他参数之前处理的参数,回调是在参数处理后执行的参数。 这种渴望是必要的,以便早期所需的参数不会产生错误消息。 例如,如果 --version 并不渴望,也是一个参数 --foo 是之前需要并定义的,您需要指定它 --version 上班了 详细信息请参见 回调评估命令 .

回调是用三个参数调用的函数:当前 Context ,当前 Parameter ,以及价值。上下文提供了一些有用的功能,例如退出应用程序并访问其他已处理的参数。

这是一个例子 --version 旗帜:

def print_version(ctx, param, value):
    if not value or ctx.resilient_parsing:
        return
    click.echo('Version 1.0')
    ctx.exit()

@click.command()
@click.option('--version', is_flag=True, callback=print_version,
              expose_value=False, is_eager=True)
def hello():
    click.echo('Hello World!')

expose_value 参数防止了毫无意义的 version 参数被传递给回调。 如果未指定,则将布尔值传递给 hello 剧本 的 resilient_parsing 如果Click想要在没有任何可能改变执行流的破坏性行为的情况下解析命令行,则将标志应用于上下文。 在这种情况下,因为我们会退出该程序,所以我们什么也不做。

它看起来是什么样子:

$ hello
Hello World!
$ hello --version
Version 1.0

验证回调

Changelog

在 2.0 版本发生变更.

如果您想应用自定义验证逻辑,可以在参数回调中执行此操作。如果验证不起作用,这些回调既可以修改值,也可以引发错误。类型转换后运行回调。它适用于所有来源,包括提示。

在Click 1.0中,您只能提高 UsageError 但从Click 2.0开始,您还可以提高 BadParameter 错误,它的额外优点是它将自动格式化错误消息,使其还包含参数名称。

def validate_rolls(ctx, param, value):
    if isinstance(value, tuple):
        return value

    try:
        rolls, _, dice = value.partition("d")
        return int(dice), int(rolls)
    except ValueError:
        raise click.BadParameter("format must be 'NdM'")

@click.command()
@click.option(
    "--rolls", type=click.UNPROCESSED, callback=validate_rolls,
    default="1d6", prompt=True,
)
def roll(rolls):
    sides, times = rolls
    click.echo(f"Rolling a {sides}-sided dice {times} time(s)")
$ roll --rolls=42
Usage: roll [OPTIONS]
Try 'roll --help' for help.

Error: Invalid value for '--rolls': format must be 'NdM'

$ roll --rolls=2d12
Rolling a 12-sided dice 2 time(s)

$ roll
Rolls [1d6]: 42
Error: format must be 'NdM'
Rolls [1d6]: 2d12
Rolling a 12-sided dice 2 time(s)

参数修改

如您所见,参数(选项和参数)被转发到命令回调。防止将参数传递给回调的一种常见方法是 expose_value 参数的参数,它完全隐藏参数。它的工作方式是 Context 对象具有 params 属性,它是所有参数的字典。字典中的任何内容都将被传递给回调函数。

这可以用来补充其他参数。通常,不推荐使用此模式,但在某些情况下,它可能会很有用。至少,知道系统以这种方式工作是件好事。

import urllib

def open_url(ctx, param, value):
    if value is not None:
        ctx.params['fp'] = urllib.urlopen(value)
        return value

@click.command()
@click.option('--url', callback=open_url)
def cli(url, fp=None):
    if fp is not None:
        click.echo(f"{url}: {fp.code}")

在这种情况下,回调会返回未更改的URL,但也会传递一秒钟 fp 回调的值。更推荐的是在包装器中传递信息,但是:

import urllib

class URL(object):

    def __init__(self, url, fp):
        self.url = url
        self.fp = fp

def open_url(ctx, param, value):
    if value is not None:
        return URL(value, urllib.urlopen(value))

@click.command()
@click.option('--url', callback=open_url)
def cli(url):
    if url is not None:
        click.echo(f"{url.url}: {url.fp.code}")

令牌规范化

Changelog

Added in version 2.0.

从click 2.0开始,可以提供用于规范化令牌的函数。标记是选项名、选项值或命令值。例如,这可以用于实现不区分大小写的选项。

为了使用这个特性,需要向上下文传递一个执行令牌规范化的函数。例如,您可以有一个将令牌转换为小写的函数:

CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())

@click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--name', default='Pete')
def cli(name):
    click.echo(f"Name: {name}")

以及它在命令行上的工作方式:

$ cli --NAME=Pete
Name: Pete

调用其他命令

有时,从另一个命令调用一个命令可能很有趣。这是一种通常不鼓励单击的模式,但仍然可能。为此,您可以使用 Context.invoke()Context.forward() 方法。

它们的工作原理相似,但不同之处在于 Context.invoke() 只使用作为调用者提供的参数调用另一个命令,而 Context.forward() 从当前命令填充参数。两者都接受命令作为第一个参数,其他的一切都会如您所期望的那样向前传递。

例子:

cli = click.Group()

@cli.command()
@click.option('--count', default=1)
def test(count):
    click.echo(f'Count: {count}')

@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
    ctx.forward(test)
    ctx.invoke(test, count=42)

看起来像是:

$ cli dist
Count: 1
Count: 42

转发未知选项

在某些情况下,能够接受所有未知选项进行进一步的手动处理是很有趣的。从click 4.0开始,click通常可以做到这一点,但它有一些局限性,这些局限性在于问题的性质。通过一个名为 ignore_unknown_options 它将指示解析器收集所有未知选项,并将它们放入剩余参数,而不是触发解析错误。

这通常可以通过两种不同的方式激活:

  1. 它可以在自定义上启用 Command 子类通过更改 ignore_unknown_options 属性。

  2. 它可以通过更改上下文类上相同名称的属性来启用 (Context.ignore_unknown_options )这是最好的改变通过 context_settings 命令上的字典。

对于大多数情况,最简单的解决方案是第二个。一旦行为改变了,就需要一些东西来选择那些剩余的选项(此时,这些选项被认为是参数)。对于这一点,您有两个选择:

  1. 你可以使用 pass_context() 以传递上下文。除此之外,只有当 ignore_unknown_options 你也设置 allow_extra_args 否则,该命令将中止,并返回一个错误,即存在剩余参数。如果使用此解决方案,将在 Context.args .

  2. 您可以附加一个 argument() 使用 nargs 设置为 -1 这将吞噬掉所有剩余的争论。在这种情况下,建议将 typeUNPROCESSED 为了避免对这些参数进行任何字符串处理,否则它们会被强制自动转换为Unicode字符串,这通常不是您想要的。

最后你会得到这样的结果:

import sys
from subprocess import call

@click.command(context_settings=dict(
    ignore_unknown_options=True,
))
@click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode')
@click.argument('timeit_args', nargs=-1, type=click.UNPROCESSED)
def cli(verbose, timeit_args):
    """A fake wrapper around Python's timeit."""
    cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
    if verbose:
        click.echo(f"Invoking: {' '.join(cmdline)}")
    call(cmdline)

看起来像是:

$ cli --help
Usage: cli [OPTIONS] [TIMEIT_ARGS]...

  A fake wrapper around Python's timeit.

Options:
  -v, --verbose  Enables verbose mode
  --help         Show this message and exit.

$ cli -n 100 'a = 1; b = 2; a * b'
python -mtimeit -n 100 a = 1; b = 2; a * b

$ cli -v 'a = 1; b = 2; a * b'
Invoking: echo python -mtimeit a = 1; b = 2; a * b
python -mtimeit a = 1; b = 2; a * b

正如您所看到的,冗长的标志是通过单击处理的,其他的一切都将结束在 timeit_args 用于进一步处理的变量,例如,它允许调用子进程。对于如何忽略未处理的标志,有一些重要的事情需要了解:

  • 未知的长选项通常被忽略,根本不进行处理。例如,如果 --foo=bar--foo bar 他们通常都是这样结束的。注意,由于解析器不知道选项是否接受参数,所以 bar 部分可以作为参数处理。

  • 如果需要,可能会部分处理和重新组装未知的短选项。例如,在上面的示例中,有一个名为 -v 这将启用详细模式。如果命令将被忽略 -va 然后 -v 部件将通过单击(如所知)处理,并且 -a 最终会在剩余参数中进行进一步处理。

  • 根据你的计划,你可以通过禁用分散的论点来获得一些成功。 (allow_interspersed_args )它指示解析器不允许混合参数和选项。根据您的情况,这可能会改善您的结果。

尽管通常不鼓励组合处理来自您自己的命令和来自另一个应用程序的命令的选项和参数,但如果您可以避免这种情况,您应该这样做。与其自己处理一些参数,不如将子命令下的所有内容都转发给另一个应用程序。

管理资源

打开组中的资源可供子命令使用,这很有用。许多类型的资源在使用后需要关闭或以其他方式清理。在Python中实现这一点的标准方法是将上下文管理器与 with 语句。

例如, Repo 类从 复杂的应用 实际上可能定义为上下文管理器:

class Repo:
    def __init__(self, home=None):
        self.home = os.path.abspath(home or ".")
        self.db = None

    def __enter__(self):
        path = os.path.join(self.home, "repo.db")
        self.db = open_database(path)
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.db.close()

通常,它将与 with 声明:

with Repo() as repo:
    repo.db.query(...)

但是,一个 with 组中的块将退出并关闭数据库,然后才能被子命令使用。

相反,使用上下文的 with_resource() 方法进入上下文管理器并返回资源。当组和任何子命令完成时,上下文的资源将被清理。

@click.group()
@click.option("--repo-home", default=".repo")
@click.pass_context
def cli(ctx, repo_home):
    ctx.obj = ctx.with_resource(Repo(repo_home))

@cli.command()
@click.pass_obj
def log(obj):
    # obj is the repo opened in the cli group
    for entry in obj.db.query(...):
        click.echo(entry)

如果资源不是上下文管理器,通常可以使用 contextlib . 如果不可能,使用上下文 call_on_close() 方法注册清理函数。

@click.group()
@click.option("--name", default="repo.db")
@click.pass_context
def cli(ctx, repo_home):
    ctx.obj = db = open_db(repo_home)

    @ctx.call_on_close
    def close_db():
        db.record_use()
        db.save()
        db.close()

在 8.2 版本发生变更: Context.call_on_close 和通过注册的上下文管理器 Context.with_resource 当CLI退出时将关闭。这些以前在退出时不会被召唤。