书承上文,在Python中使用自带的 argparse 来解析命令行参数并构建CLI应用其实不太现实,除非你的应用极其简单。因此我在一段时间的开发后就将命令行解析工具从 argparse 换到了 Typer,这是一个非常简单且易用的命令行解析工具,允许你创建两层子命令rich 的美观命令行输出。我们直接来看看如何使用 Typer 构建命令行程序。
如果你有什么不清楚的地方,可以看看 Typer 的官方文档, 或者国内镜像,里面内容也很清楚,但由于 Typer 的配置过于灵活,我在这篇文章给出了一个基础的构建范例。
创建主程序入口
与 argparse 不同,Typer 虽然也可以使用显式方法创建命令行解析,但一般来说都使用装饰器来创建命令行解析的,基本代码如下:
import typerfrom 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()上面演示的程序拥有两个子命令,你可以按如下所示运行这两个不同的功能。
$ program-name hello lancer_soulHello lancer_soul.
$ program-name goodbye lancer_soulGoodbye 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 就不会为你的程序创建任何子命令,你可以直接运行主程序。
$ program-name lancer_soul Hello lancer_soul.将子命令函数与主函数分开
当我们的子命令比较多的时候,我们可能希望将子命令的创建与主程序分开来分模块编写。这个时候我们不再用 @app.command() 装饰器来创建命令,而是使用显式方法来创建。
import typerfrom program_name.command.hello import hellofrom 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()在另一个文件中我们定义真正的要用到的子命令入口。
import typer # 在定义子命令入口的时候,其实隐性使用了typer,不会提示无效引用from typing_extensions import Annotated
def hello(name: Annotated[str, typer.Arguement()]): print(f"Hello {name}.")参数与选项
在应用程序中,传参指的是按一定顺序传入的参数,选项则是按特定名称传入的参数。相比于 argparse Typer 的语法非常简单,只需在创建命令时填入所需参数和选项即可。除此之外,我们也展示了 Typer 在创建参数时的一大优势:被创建的命令行参数是一个变量,而非 args 的 atribute ,可以被直接使用。
import typerfrom typing_extensions import Annotatedfrom 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 更建议直接使用 rich。rich 的进度条由一个叫做 track 的函数提供,如果只需要简单使用的话,导入该函数就行。进度条显示的是迭代对象的进度,所以我们只需要使用 track 将迭代对象包裹起来就可以了。
import timefrom rich.progress import trackfor i in track(range(20), description="Processing..."): time.sleep(0.1)print("Process done.")除了这种进度条,你应该还见过那种旋转的进度,它一般用于表示无法计算进度的操作,只是为了告诉用户这项操作正在进行。
import timefrom 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 来测试,我们需要将真正负责业务的函数与函数命令行入口分离,将业务函数拆分成独立的模块。就像这样:
import typer # 在定义子命令入口的时候,其实隐性使用了typer,不会提示无效引用from typing_extensions import Annotatedfrom 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}.")def hi(firstname, lastname) return firstname + " " + lastname这样,我们就能单独测试 hi 这个函数,在 test 目录下来创建一个测试脚本。
from program_name.fullname import fullname
def test_fullname(): assert fullname("John", "Smith") == "John Smith"如果要测试你的函数是否如期抛出异常,那么可以这么写:
from program_name.fullname import fullname
def test_fullname(): with pytest.raises(TyperError): fullname("John", 250)结语
对于一个大型项目或多人合作项目来说,使用 pytest 进行自动化测试是非常有必要的,加上多人合作编辑的需要,项目的模块化变得必须了起来。如果你在编写项目时能够按照 Typer 和 pytest 的规范进行编写,在后续真正上手工作项目时就能提前适应。