跳至内容

CLI 选项回调和上下文

在某些情况下,你可能希望为特定CLI 参数(对于CLI 选项CLI 参数)有一些自定义逻辑,该逻辑使用从终端接收的值执行。

在这些情况下,你可以使用CLI 参数回调函数。

验证CLI 参数

例如,你可以在执行其余代码之前进行一些验证。

from typing import Optional

import typer
from typing_extensions import Annotated


def name_callback(value: str):
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Annotated[Optional[str], typer.Option(callback=name_callback)] = None):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

提示

如果可能,最好使用Annotated版本。

from typing import Optional

import typer


def name_callback(value: str):
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Optional[str] = typer.Option(default=None, callback=name_callback)):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

此处,您使用关键字参数 callback 将函数传递给 typer.Option()typer.Argument()

该函数接收来自命令行中的值。它可以对该值执行任何操作,然后返回值。

在此情况下,如果 --name 不为 Camila,我们将引发 typer.BadParameter() 异常。

BadParameter 异常是特殊的,它会显示生成该异常的参数的错误。

检查它

$ python main.py --name Camila

Hello Camila

$ python main.py --name Rick

Usage: main.py [OPTIONS]

// We get the error from the callback
Error: Invalid value for '--name': Only Camila is allowed

处理自动补全

对于回调和自动补全,需要一些小的特殊处理,这一点需要注意。

但首先,让我们在您的 shell(Bash、Zsh、Fish 或 PowerShell)中使用自动补全。

在为自己的 Python 包安装自动补全后,当您使用 CLI 程序并开始使用 -- 添加一个 CLI 选项,然后按 TAB 时,您的 shell 将向您显示可用的 CLI 选项(对于 CLI 参数 等也是如此)。

要使用上一个脚本快速检查它,请使用 typer 命令

// Hit the TAB key in your keyboard below where you see the: [TAB]
$ typer ./main.py [TAB][TAB]

// Depending on your terminal/shell you will get some completion like this ✨
run    -- Run the provided Typer app.
utils  -- Extra utility commands for Typer apps.

// Then try with "run" and --help
$ typer ./main.py run --help

// You get a help text with your CLI options as you normally would
Usage: typer run [OPTIONS]

  Run the provided Typer app.

Options:
  --name TEXT  [required]
  --help       Show this message and exit.

// Then try completion with your program
$ typer ./main.py run --[TAB][TAB]

// You get completion for CLI options
--help  -- Show this message and exit.
--name

// And you can run it as if it was with Python directly
$ typer ./main.py run --name Camila

Hello Camila

shell 自动补全的工作原理

其内部工作原理是,shell/终端将使用一些特殊环境变量(保存当前 CLI 参数 等)调用您的 CLI 程序,并且您的 CLI 程序将打印一些特殊值,shell 将使用这些值来显示自动补全。所有这些都由 Typer 在后台为您处理。

但主要的重点是,它全部基于 shell 读取的由您的程序打印的值。

在回调中中断自动补全

假设在回调运行时,我们希望显示一条消息,说明正在验证名称

from typing import Optional

import typer
from typing_extensions import Annotated


def name_callback(value: str):
    print("Validating name")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Annotated[Optional[str], typer.Option(callback=name_callback)] = None):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

提示

如果可能,最好使用Annotated版本。

from typing import Optional

import typer


def name_callback(value: str):
    print("Validating name")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Optional[str] = typer.Option(default=None, callback=name_callback)):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

并且由于在 shell 调用您的程序请求自动补全时将调用回调,因此该消息 "Validating name" 将被打印,并且它将中断自动补全。

它看起来像

// Run it normally
$ typer ./main.py run --name Camila

// See the extra message "Validating name"
Validating name
Hello Camila

$ typer ./main.py run --[TAB][TAB]

// Some weird broken error message ⛔️
(eval):1: command not found: Validating
rutyper ./main.pyed Typer app.

修复补全 - 使用 Context

当你创建一个 Typer 应用程序时,它在内部使用 Click。

每个 Click 应用程序都有一个特殊对象,称为 "上下文",它通常是隐藏的。

但是,你可以通过声明类型为 typer.Context 的函数参数来访问上下文。

"上下文" 有一些关于当前程序执行的附加数据

from typing import Optional

import typer
from typing_extensions import Annotated


def name_callback(ctx: typer.Context, value: str):
    if ctx.resilient_parsing:
        return
    print("Validating name")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Annotated[Optional[str], typer.Option(callback=name_callback)] = None):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

提示

如果可能,最好使用Annotated版本。

from typing import Optional

import typer


def name_callback(ctx: typer.Context, value: str):
    if ctx.resilient_parsing:
        return
    print("Validating name")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Optional[str] = typer.Option(default=None, callback=name_callback)):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

在处理补全时,ctx.resilient_parsing 将为 True,因此你可以直接返回而不打印任何其他内容。

但在正常调用程序时,它将为 False。因此,你可以继续执行之前的代码。

修复补全只需要这些。🚀

检查它

$ typer ./main.py run --[TAB][TAB]

// Now it works correctly 🎉
--help  -- Show this message and exit.
--name

// And you can call it normally
$ typer ./main.py run --name Camila

Validating name
Hello Camila

使用 CallbackParam 对象

通过声明具有其值函数参数,你可以访问 typer.Context 的方式相同,你可以声明另一个类型为 typer.CallbackParam 的函数参数以获取特定的 Click Parameter 对象。

from typing import Optional

import typer
from typing_extensions import Annotated


def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
    if ctx.resilient_parsing:
        return
    print(f"Validating param: {param.name}")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Annotated[Optional[str], typer.Option(callback=name_callback)] = None):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

提示

如果可能,最好使用Annotated版本。

from typing import Optional

import typer


def name_callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
    if ctx.resilient_parsing:
        return
    print(f"Validating param: {param.name}")
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value


def main(name: Optional[str] = typer.Option(default=None, callback=name_callback)):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

这可能不太常见,但如果你需要,可以这样做。

例如,如果你有一个可以被多个CLI 参数使用的回调,这样回调就可以知道每次是哪个参数。

检查它

$ python main.py --name Camila

Validating param: name
Hello Camila

技术细节

因为你根据标准 Python 类型注释在回调函数中获取相关数据,所以你可以免费在编辑器中获得类型检查和自动补全。

Typer 将确保你获得所需的函数参数。

你不必担心它们的名字、顺序等。

因为它基于标准 Python 类型,所以它 "只需工作"。✨

Click 的 Parameter

typer.CallbackParam 实际上只是 Click 的 Parameter 的一个子类,因此你可以在编辑器中获得所有正确的补全。

带有类型注释的回调

只需声明每个类型的函数参数,即可获取 typer.Contexttyper.CallbackParam

顺序无关紧要,函数参数的名称无关紧要。

您还可以仅获取 typer.CallbackParam 而不获取 typer.Context,反之亦然,它仍然有效。

value 函数参数

回调中的 value 函数参数也可以具有任何名称(例如 lastname)和任何类型,但它应该具有与主函数中相同的类型注释,因为这是它将接收的内容。

也可以不声明其类型。它仍然有效。

并且完全可以不声明 value 参数,例如,仅获取 typer.Context。这也将有效。