What is Typer
Typer 是一个用于构建强大且易用的命令行应用程序的库,其设计简洁直观,具备出色的可读性。可以将其视为 CLI 版的 FastAPI。
Typer 的主要特性
- 直观易写:提供优秀的编辑器支持,代码补全随处可见,减少调试时间,使开发更加高效。同时,它的设计注重可读性,易于学习,无需大量查阅文档。
- 用户友好:自动生成命令行帮助信息,并支持终端自动补全,使最终用户的交互体验更加顺畅。
- 简洁高效:减少重复代码,在参数声明时支持多个功能点,降低出错概率,提高开发效率。
Typer 快速入门体验
在开始这个项目之前,让我们先简单体验一下 Typer 的基础用法。先使用pip安装typer:
pip install typer
创建main.py,先写一个简单的Hello体验一下:
import typer
def main(name: str, lastname: str = ""):
"""接受用户输入的姓名并打印问候语"""
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)
代码解析
- typer.run(main) 使 main() 函数成为一个 CLI 命令,自动解析参数。
- name: str是必填参数,需要在命令行提供。
- lastname: str = ""是可选参数, 命令没有提供 --lastname时会使用默认值。
在命令行中执行以下命令python main.py "Miles" --lastname "Shi"
,输出的结果如下
从这个简单的示例可以看到Typer的几个特点:简单、直观、无需额外处理参数解析
构建To-Do CLI 应用
1.创建项目
首先创建一个目录todo-cli, 在项目根目录下,创建以下文件和文件夹
├── main.py # 入口文件,启动 Typer CLI,处理全局选项(如 --version)
├── requirements.txt
└── todo/
├── __init__.py
├── cli.py # 核心 CLI 逻辑,定义 `typer.Typer()` 应用和命令(add, list, done, delete)
└── todo.json # 存储todo待办事项, 作为简单的数据库
设置项目环境
然后需要创建一个python虚拟环境,大家可以选择自己喜欢的虚拟环境工具,这边推荐使用venv。
在项目的根目录下执行以下命令
python -m venv todo-cli # create virtual env
激活虚拟环境, 因为之前创建的虚拟环境是在todo-cli项目目录中的,所以激活时也要在项目目录中执行如下命令或者加上绝对路径
todo-cli/Scripts/activate # activate on windwos
source todo-cli/bin/activate # activate on mac
安装依赖
激活虚拟环境后,使用命令pip install typer
安装typer
2.添加命令行入口
初始化Typer应用
首先在todo/cli.py中,创建一个Typer的实例,并添加命令行工具的名称APP_NAME和版本号VERSION
import typer
app = typer.Typer()
APP_NAME = "my_todo"
VERSION = "0.1.0"
添加命令和选项
在cli.py中添加main()方法,并使用typer.Option添加--version选项:
def _version_callback(value: bool) -> None:
"""显示应用版本并退出"""
if value:
typer.echo(f"{APP_NAME} v{VERSION}")
raise typer.Exit()
@app.callback()
def main(
version: bool = typer.Option(
False,
"--version",
"-v",
help="Show the application's version and exit.",
is_eager=True, # 立即执行,不要求输入子命令
callback=_version_callback, # 触发版本回调
)
):
"""To-Do CLI"""
return
最后在__main__.py
中,设置应用的入口点
from todo import cli
if __name__ == "__main__":
cli.app()
完成以上步骤后,我们使用python main.py --version
就能获取命令行应用的名称和版本号
3.处理数据存储
为了让任务数据管理模块化,我们将数据存储逻辑封装到database.py并使用todo.json作为任务存储(该方式适合小型项目或教程),所有任务以JSON格式存储,每个任务包含:
- task:任务描述(字符串)
- done:任务状态(布尔值,True 代表已完成,False 代表未完成)
以下是示例:
[
{
"task": "完成 Python 练习",
"done": false
},
{
"task": "阅读 Typer 官方文档",
"done": true
},
{
"task": "提交项目报告",
"done": false
}
]
import json
from pathlib import Path
import typer
DEFAULT_DB_FILE = Path(__file__).parent / "todo.json"
def initialize_db():
"""确保 JSON 数据库文件存在"""
if not DEFAULT_DB_FILE.exists():
try:
DEFAULT_DB_FILE.touch(exist_ok=True)
with DEFAULT_DB_FILE.open("w", encoding="utf-8") as f:
json.dump([], f)
except OSError as e:
typer.echo(f"⚠️ 无法初始化数据库文件: {e}", err=True)
def load_todos():
"""加载 JSON 文件中的待办事项"""
initialize_db() # 确保文件存在
try:
with DEFAULT_DB_FILE.open("r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
typer.echo("JSON 解析错误", err=True)
save_todos([]) # 重新初始化
except Exception as e:
typer.echo(f"加载任务时发生错误: {e}", err=True)
return []
def save_todos(todos):
"""保存待办事项到 JSON 文件"""
try:
with DEFAULT_DB_FILE.open("w", encoding="utf-8") as f:
json.dump(todos, f, indent=4, ensure_ascii=False)
except Exception as e:
typer.echo(f"无法保存数据: {e}", err=True)
- DEFAULT_DB_FILE:定义todo.json文件的存储路径,确保数据存放在todo/目录下
- initialize_db()函数:初始化todo.json文件,若不存在则创建一个空的JSON文件。
- load_todos()函数:加载todo.json文件中的数据,并返回Todo任务列表。
- save_todos(todos)函数:将新的任务列表保存到 todo.json 文件。
4.添加命令行指令
现在,我们将为CLI添加一些操作命令(add、list、done、delete)来管理任务。我们可以直接在cli.py定义这些命令,或者将它们拆分到单独的模块中,以便更好地组织代码。
接下来,我们将逐步实现这些命令,并展示它们的实际效果。
列出所有todo(list command)
# 列出任务
@app.command()
def list():
"""列出所有待办事项"""
todos = load_todos()
if not todos:
typer.echo("📌 当前没有待办事项。")
else:
for idx, todo in enumerate(todos, start=1):
status = "✔️" if todo["done"] else "❌"
typer.echo(f"{idx}. {status} {todo['task']}")
- 读取 todo.json 中的任务并按序号显示。
- 如果todos任务列表为空则显示”当前没有待办事项”
运行示例
可以看到list指令从todo.json中获取了所有todo任务
添加任务 (add command)
# 添加任务
@app.command()
def add(task: str):
"""添加一个新的待办事项"""
todos = load_todos()
todos.append({"task": task, "done": False})
save_todos(todos)
typer.echo(f"✅ 任务已添加: {task}")
- load_todos()读取已有任务,追加任务后使用save_todos()保存。
- typer.echo()显示任务添加成功
运行命令
python main.py add "完成 Python 练习"
输出内容
完成任务(done command)
@app.command()
def done(task_number: int):
"""标记指定任务为已完成"""
todos = load_todos()
if 0 < task_number <= len(todos):
todos[task_number - 1]["done"] = True
save_todos(todos)
typer.echo(f"🎉 任务 {task_number} 已完成!")
else:
typer.echo("⚠️ 任务编号无效。")
- 根据task_number任务号找到任务并标记为done。
- 如果编号无效,CLI会进行提示。
运行命令
python main.py done 1
输出结果
删除任务(delete command)
@app.command()
def delete(task_number: int):
"""删除指定任务"""
todos = load_todos()
if 0 < task_number <= len(todos):
removed = todos.pop(task_number - 1)
save_todos(todos)
typer.echo(f"🗑️ 任务已删除: {removed['task']}")
else:
typer.echo("⚠️ 任务编号无效。")
- 通过task_number找到任务并将其从 todo.json 中移除并保存
- 如果编号无效,CLI 会提示错误。
运行示例
python main.py delete 1
输出结果
5.安装命令行
完成所有CLI命令的开发后,我们希望可以能够像ls这样的命令一样能够直接在终端中运行,而不需要python main.py。
使用setuptools进行安装
在todo-cli项目根目录中创建setup.py,并添加以下代码
from setuptools import setup, find_packages
setup(
name="todo-cli",
version="0.1.0",
packages=find_packages(), #自动查找目录下的Python模块
install_requires=["typer"],
entry_points={
"console_scripts": [
"my_todo=todo.cli:app",
],
},
)
- install_requires定义项目依赖项,确保安装 typer,否则命令行工具不能运行。
- entry_points指定my_todo作为 CLI命令,执行todo.cli模块的app实例。
- packages查找目录下的Python模块
- name和version执行pip安装后的包名以及版本号
执行以下命令安装
pip install setuptools
pip install --editable .
安装完成后,在todo-cli的虚拟环境中可以直接运行my_todo —help