1775 字
9 分钟
使用Typer开发CLI应用程序

书承上文,在Python中使用自带的 argparse 来解析命令行参数并构建CLI应用其实不太现实,除非你的应用极其简单。因此我在一段时间的开发后就将命令行解析工具从 argparse 换到了 Typer,这是一个非常简单且易用的命令行解析工具,允许你创建两层子命令(对于大部分项目来说,一层子命令就已经够用了)、简明的书写命令行参数与选项以及提供了基于 rich 的美观命令行输出。我们直接来看看如何使用 Typer 构建命令行程序。

如果你有什么不清楚的地方,可以看看 Typer官方文档, 或者国内镜像,里面内容也很清楚,但由于 Typer 的配置过于灵活,我在这篇文章给出了一个基础的构建范例。

创建主程序入口#

argparse 不同,Typer 虽然也可以使用显式方法创建命令行解析,但一般来说都使用装饰器来创建命令行解析的,基本代码如下:

import typer
from typing_extensions import Annotated
app = typer.Typer(
no_args_is_help=True, # 这个flag用来设置当没有程序没有任何输入时,返回help。
help="your program help" # 这里填入你的主程序帮助。
)
@app.command(help="your sub command help") # 这个help填入你的子命令帮助,下面的help则是参数和选项的帮助。
def hello(name: Annotated[str, typer.Arguement(help="your name")]): # Typer建议采用类型标注来规定输入参数,不用也可以。
print(f"Hello {name}.")
@app.command(help="your another command help")
def goodbye(name: str = typer.Arguement(..., help="your name")): # 不用类型标注则是这种形式。
print(f"Goodbye {name}.")
def main():
app()
if __name__ == "__main__":
main()

上面演示的程序拥有两个子命令,你可以按如下所示运行这两个不同的功能。

Terminal window
$ program-name hello lancer_soul
Hello lancer_soul.
$ program-name goodbye lancer_soul
Goodbye lancer_soul.

如果你的程序不需要任何子命令,Typer 也可以自动识别,你只需要像上面一样创建命令就行了。

import typer
app = typer.Typer(
no_args_is_help=True, # 这个flag用来设置当没有程序没有任何输入时,返回help。
help="your program help" # 这里填入你的主程序简介。
)
@app.command(help="your sub command help") # 当程序只有一个子命令时,这里不需要填帮助。
def hello(name: Annotated[str, typer.Arguement()]):
print(f"Hello {name}.")
def main():
app()
if __name__ == "__main__":
main()

当你在程序入口只创建了一个子命令的时候,Typer 就不会为你的程序创建任何子命令,你可以直接运行主程序。

Terminal window
$ program-name lancer_soul
Hello lancer_soul.

将子命令函数与主函数分开#

当我们的子命令比较多的时候,我们可能希望将子命令的创建与主程序分开来分模块编写。这个时候我们不再用 @app.command() 装饰器来创建命令,而是使用显式方法来创建。

__main__.py
import typer
from program_name.command.hello import hello
from program_name.command.goodbye import goodbye
app = typer.Typer()
app.command(help="your sub command help")(hello)
app.command(help="your sub command help")(goodbye)
def main():
app()
if __name__ == "__main__":
main()

在另一个文件中我们定义真正的要用到的子命令入口。

command/hello.py
import typer # 在定义子命令入口的时候,其实隐性使用了typer,不会提示无效引用
from typing_extensions import Annotated
def hello(name: Annotated[str, typer.Arguement()]):
print(f"Hello {name}.")

参数与选项#

在应用程序中,传参指的是按一定顺序传入的参数,选项则是按特定名称传入的参数。相比于 argparse Typer 的语法非常简单,只需在创建命令时填入所需参数和选项即可。除此之外,我们也展示了 Typer 在创建参数时的一大优势:被创建的命令行参数是一个变量,而非 argsatribute ,可以被直接使用。

import typer
from typing_extensions import Annotated
from typing import Literal
def app(
name: Annotated[str, typer.Arguement(help="your name")],
fromal: Annotated[bool, typer.Option("--fromal", "-f", help="Say hi formally.")] = False,
sex: Annotated[Literal["male", "female"], typer.Option("--sex", "-s", help="male or female")] = "male"
):
if fromal:
if sex == "male":
print(f"Good day Mr. {name}.")
else:
print(f"Good day Ms. {name}.")
else:
print(f"Hello {name}.")

从上面的例子我们可以发现,相比 argparse 的显式定义各种参数,Typer 使用声明式定义各类参数,只是在定义具有规定参数的选项时需要用到 Literal 类。与 argparse 类似,Typer 也是通过参数/选项是否具有默认值来确定其是否必需。

进度条#

当你使用 uv 的时候可能已经发现它具有非常好看的进度条,这实际上是 Typer 的依赖:rich 的功能,在使用 Typer 的时候不妨试试它。Typer 本身也提供了一个 typer.progressbar 的接口,但 Typer 更建议直接使用 richrich 的进度条由一个叫做 track 的函数提供,如果只需要简单使用的话,导入该函数就行。进度条显示的是迭代对象的进度,所以我们只需要使用 track 将迭代对象包裹起来就可以了。

import time
from rich.progress import track
for i in track(range(20), description="Processing..."):
time.sleep(0.1)
print("Process done.")

除了这种进度条,你应该还见过那种旋转的进度,它一般用于表示无法计算进度的操作,只是为了告诉用户这项操作正在进行。

import time
from rich.progress import Progress, SpinnerColumn, TextColumn
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=Ture,
) as progress:
progress.add_task(description="Processing...", total=None)
time.sleep(3)
print("Process done.")

上述内容已经用到了进度显示的高级用法,这里的 Progress 类是自定义的,如果你不喜欢这个款式,可以在 rich用户手册中查看具体的自定义选项。

测试#

测试是一个非常重要的步骤。在 argparse 的文章中,我们是直接使用命令行来测试的,这不是不可以,但我们有更好的办法。pytest 是一个广泛使用的 Python 测试工具,你只需要运行一个函数,并断言其应当出现的结果。运行 pytest 后,你就能知道该函数有没有通过测试。测试文件就放在之前的 test 目录下。为了让我们的项目能够使用 pytest 来测试,我们需要将真正负责业务的函数与函数命令行入口分离,将业务函数拆分成独立的模块。就像这样:

command/hello.py
import typer # 在定义子命令入口的时候,其实隐性使用了typer,不会提示无效引用
from typing_extensions import Annotated
from program_name.fullname import fullname
def hello(
firstname: Annotated[str, typer.Arguement()],
lastname: Annotated[str, typer.Arguement()],
):
name = fullname(firstname, lastname)
print(f"Hello {name}.")
fullname.py
def hi(firstname, lastname)
return firstname + " " + lastname

这样,我们就能单独测试 hi 这个函数,在 test 目录下来创建一个测试脚本。

test/test_fullname.py
from program_name.fullname import fullname
def test_fullname():
assert fullname("John", "Smith") == "John Smith"

如果要测试你的函数是否如期抛出异常,那么可以这么写:

test/test_fullname.py
from program_name.fullname import fullname
def test_fullname():
with pytest.raises(TyperError):
fullname("John", 250)

结语#

对于一个大型项目或多人合作项目来说,使用 pytest 进行自动化测试是非常有必要的,加上多人合作编辑的需要,项目的模块化变得必须了起来。如果你在编写项目时能够按照 Typerpytest 的规范进行编写,在后续真正上手工作项目时就能提前适应。

使用Typer开发CLI应用程序
https://blog.lancersoul.top/posts/typer-project-dev/
作者
Lancer Soul
发布于
2025-10-29
许可协议
CC BY-NC-SA 4.0