跳到主要内容

Commands

Typer 支持在单文件中定义多个子命令(Subcommand)

A command looks the same as a CLI argument, it's just some name without a preceding --. But commands have a predefined name, and are used to group different sets of functionalities into the same CLI application.

Here I'll use CLI application or program to refer to the program you are building in Python with Typer, and command to refer to one of these "subcommands" of your program.

单命令

之前写得都是简化的模式:

import typer


def main(name: str):
print(f"Hello {name}")


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

实际上,单命令式的完整写法是这样的:

import typer

app = typer.Typer()


@app.command()
def main(name: str):
print(f"Hello {name}")


if __name__ == "__main__":
app()

多命令

import typer

app = typer.Typer()


@app.command()
def create():
print("Creating user: Hiro Hamada")


@app.command()
def delete():
print("Deleting user: Hiro Hamada")


if __name__ == "__main__":
app()
image-20240710180525044

关于只定义了一个 @app.command() 时 Typer 的行为,详见:https://typer.tiangolo.com/tutorial/commands/one-or-multiple/

help 说明配置

使用 no_args_is_help=True 来让命令的默认输出为 helping page:

app = typer.Typer(no_args_is_help=True)

这样,运行 python main.py 的结果也会是帮助说明页了


使用 help="Awesome CLI user manager." 来给整个 CLI application 添加说明

app = typer.Typer(help="Awesome CLI user manager.")

@app.command 中添加 help 参数,可以给子命令添加说明

@app.command(help="Create a new user with USERNAME.")
image-20240710181232441

但是这会覆盖 docstring 的说明


将 command 设为废弃:

@app.command(deprecated=True)
image-20240710181522566

Rich Markup

欲获取 rich 的能力:

app = typer.Typer(rich_markup_mode="rich")

这样就能在各种注释说明的地方使用 rich 的标记语法啦

import typer
from typing_extensions import Annotated

app = typer.Typer(rich_markup_mode="rich")


@app.command()
def create(
username: Annotated[
str, typer.Argument(help="The username to be [green]created[/green]")
],
):
"""
[bold green]Create[/bold green] a new [italic]shinny[/italic] user. :sparkles:

This requires a [underline]username[/underline].
"""
print(f"Creating user: {username}")


@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].")
def delete(
username: Annotated[
str, typer.Argument(help="The username to be [red]deleted[/red]")
],
force: Annotated[
bool, typer.Option(help="Force the [bold red]deletion[/bold red] :boom:")
] = False,
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")


if __name__ == "__main__":
app()
image-20240710181844403 image-20240710181948813

Rich Markdown

想获得 markdown 能力:

app = typer.Typer(rich_markup_mode="markdown")

例如

import typer
from typing_extensions import Annotated

app = typer.Typer(rich_markup_mode="markdown")


@app.command()
def create(
username: Annotated[str, typer.Argument(help="The username to be **created**")],
):
"""
**Create** a new *shinny* user. :sparkles:

* Create a username

* Show that the username is created

---

Learn more at the [Typer docs website](https://typer.tiangolo.com)
"""
print(f"Creating user: {username}")


@app.command(help="**Delete** a user with *USERNAME*.")
def delete(
username: Annotated[str, typer.Argument(help="The username to be **deleted**")],
force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False,
):
"""
Some internal utility function to delete.
"""
print(f"Deleting user: {username}")


if __name__ == "__main__":
app()
image-20240710182105394 image-20240710182555517

Notice that in Markdown you cannot define colors. For colors you might prefer to use Rich markup.

Help Panels

当然 typer 支持了子命令的 help panels:

@app.command(rich_help_panel="Utils and Configs")

没有 panel 的命令将展示在默认 panel 区,而有 panel 的命令将展示在各自的 panel 区

image-20240711100418424

同样的,CLI Parameters 也支持 panel

@app.command()
def create(
username: Annotated[str, typer.Argument(help="The username to create")],
lastname: Annotated[
str,
typer.Argument(
help="The last name of the new user", rich_help_panel="Secondary Arguments"
),
] = "",
force: Annotated[bool, typer.Option(help="Force the creation of the user")] = False,
age: Annotated[
Union[int, None],
typer.Option(help="The age of the new user", rich_help_panel="Additional Data"),
] = None,
favorite_color: Annotated[
Union[str, None],
typer.Option(
help="The favorite color of the new user",
rich_help_panel="Additional Data",
),
] = None,
):
"""
[green]Create[/green] a new user. :sparkles:
"""
print(f"Creating user: {username}")
image-20240711100657797

后记 Epilog

使用 epilog 参数为 CLI application 添加后记

@app.command(epilog="Made with :heart: in [blue]Venus[/blue]")
image-20240711100920068

自定义命令名

默认情况下,命令名即 @app.command 修饰的函数名,不过 _ 会被转换为 -

  • create_user -> create-user

当然可以自定义啦~

@app.command("create")  # the command name will be "create"
def cli_create_user(username: str):
print(f"Creating user: {username}")

callback

可以为定义一个通用的 callback,作用于所有的子命令

import typer

app = typer.Typer()
state = {"verbose": False}


@app.command()
def create(username: str):
if state["verbose"]:
print("About to create a user")
print(f"Creating user: {username}")
if state["verbose"]:
print("Just created a user")


@app.command()
def delete(username: str):
if state["verbose"]:
print("About to delete a user")
print(f"Deleting user: {username}")
if state["verbose"]:
print("Just deleted a user")


@app.callback()
def main(verbose: bool = False):
"""
Manage users in the awesome CLI app.
"""
if verbose:
print("Will write verbose output")
state["verbose"] = True


if __name__ == "__main__":
app()
image-20240711101634028

相当于一个,装饰器的作用?

也可以仅仅使用 callback 来为总程序添加注释说明

@app.callback()
def callback():
"""
Manage users CLI app.

Use it with the create command.

A new user with the given NAME will be created.
"""

在初始化的时候添加 callback

def callback():
print("Running a command")


app = typer.Typer(callback=callback)

@app.callback() 的作用差不多,但是 @app.callback() 可以覆盖 app 的 callback

改变 callback 的运行行为

默认情况下,当没有明确给出子命令时,@app.callback() 只会负责展示帮助信息,而不会运行里面的逻辑。当然我们可以改变这一点:

@app.callback(invoke_without_command=True)
def main():
"""
Manage users in the awesome CLI app.
"""
print("Initializing database")

image-20240711102831269

context

使用 typer.Context 来获取上下文

import typer

app = typer.Typer()


@app.command()
def create(username: str):
print(f"Creating user: {username}")


@app.command()
def delete(username: str):
print(f"Deleting user: {username}")


@app.callback()
def main(ctx: typer.Context):
"""
Manage users in the awesome CLI app.
"""
print(f"About to execute command: {ctx.invoked_subcommand}") # the command that was invoked


if __name__ == "__main__":
app()

image-20240711102609554

可以通过 ctx.invoked_subcommand is None 来判断现在到底调用的是主程序还是子命令:

@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
"""
Manage users in the awesome CLI app.
"""
if ctx.invoked_subcommand is None:
print("Initializing database")

配置上下文

在创建子命令或 callback 时,你可以传入对 context 的相关配置,以控制某些行为

以下的例子中,我们将 allow_extra_argsignore_unknown_options 都设置为 True,这样就可以接收额外的参数了

@app.command(
context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
)
def main(ctx: typer.Context):
for extra_arg in ctx.args:
print(f"Got extra arg: {extra_arg}")
image-20240711103535032

Notice that it saves all the extra CLI parameters as a raw list of str, including the CLI option names and values, everything together.