اگر می‌خواهید در کوتاه‌ترین زمان یک ابزار خط فرمان قابل اتکا بسازید، آموزش Typer در پایتون دقیقا همان چیزی است که نیاز دارید: Typer با تایپ‌هینت‌ها (type hints) کار می‌کند، سینتکس ساده‌ای دارد و به شکل طبیعی گزینه‌ها و آرگومان‌ها، راهنما، اعتبارسنجی و تکمیل خودکار را فراهم می‌کند. در این راهنما با یک مثال عملی از نصب تا ساخت چندفرمانی، تست، بسته‌بندی و نصب به صورت دستور سیستمی پیش می‌رویم.

پیش نیازها و نصب

برای شروع فقط به پایتون و pip نیاز دارید. پیشنهاد می‌شود در یک محیط مجازی کار کنید.

python -m venv .venv
source .venv/bin/activate  # در ویندوز: .venv\Scripts\activate
pip install "typer[all]"

بسته "[all]" امکان تکمیل خودکار و امکانات تعاملی را فعال می‌کند.

اولین CLI در 5 دقیقه

هدف: یک دستور ساده برای سلام کردن بسازیم که نام را بگیرد و بنا بر گزینه‌ها خروجی را تنظیم کند.

import typer

app = typer.Typer(help="ابزار نمونه برای نمایش Typer")

@app.command()
def hello(
    name: str = typer.Argument(..., help="نام مخاطب"),
    formal: bool = typer.Option(False, "--formal", "-f", help="سلام رسمی"),
):
    """نمایش پیام خوشامدگویی."""
    if formal:
        typer.echo(f"درود بر شما، {name}. خوش آمدید.")
    else:
        typer.echo(f"سلام {name}!")

if __name__ == "__main__":
    app()

اجرا و بررسی راهنما:

python app.py --help
python app.py hello --help
python app.py hello سارا
python app.py hello سارا --formal

آرگومان‌ها، گزینه‌ها و تایپ‌ها

Typer تایپ‌ها را از annotation های پایتون می‌خواند و اعتبارسنجی می‌کند. مثال زیر متریک‌ها را جمع می‌کند و خروجی را به شکل مناسب نمایش می‌دهد.

from typing import Optional
import typer

app = typer.Typer()

@app.command()
def add(
    a: float = typer.Argument(..., help="شماره اول"),
    b: float = typer.Argument(..., help="شماره دوم"),
    round_: Optional[int] = typer.Option(None, "--round", "-r", min=0, help="گرد کردن تا n رقم اعشار"),
):
    result = a + b
    if round_ is not None:
        result = round(result, round_)
    typer.secho(f"نتیجه: {result}", fg=typer.colors.GREEN)

if __name__ == "__main__":
    app()

نکات مهم:

  • typer.Argument برای مقادیر ضروری موقعیت‌دار استفاده می‌شود.
  • typer.Option برای گزینه‌های اختیاری با نام‌های بلند/کوتاه استفاده می‌شود.
  • با تعیین نوع (float، int، bool و…) تبدیل و پیام خطای مناسب به طور خودکار انجام می‌شود.
  • برای گزینه‌های بولی می‌توانید از flag استفاده کنید: debug: bool = typer.Option(False, "--debug/--no-debug")

ساخت چند فرمانی و گروه‌بندی

برای پروژه‌های واقعی، بهتر است فرمان‌ها را گروه‌بندی کنید. در Typer می‌توانید چند Typer بسازید و آنها را به برنامه اصلی اضافه کنید.

import typer

app = typer.Typer(help="مدیریت کارها (Todo)")

todo_app = typer.Typer(help="فرمان‌های مربوط به کارها")
user_app = typer.Typer(help="فرمان‌های مربوط به کاربر")

@todo_app.command("add")
def add_task(title: str = typer.Argument(..., help="عنوان کار"), priority: int = typer.Option(3, "--priority", "-p", min=1, max=5)):
    typer.echo(f"کار '{title}' با اولویت {priority} افزوده شد.")

@todo_app.command("list")
def list_tasks(done: bool = typer.Option(False, "--done/--pending", help="فقط کارهای انجام‌شده یا انجام‌نشده")):
    status = "انجام‌شده" if done else "باز"
    typer.echo(f"فهرست کارهای {status}... (اینجا از دیتابیس/فایل می‌خوانید)")

@user_app.command("login")
def login(username: str = typer.Option(..., prompt=True), password: str = typer.Option(..., prompt=True, hide_input=True)):
    typer.echo(f"ورود کاربر {username} موفق بود.")

app.add_typer(todo_app, name="todo")
app.add_typer(user_app, name="user")

if __name__ == "__main__":
    app()

نمونه اجرا:

python cli.py todo add "مطالعه Typer" --priority 4
python cli.py todo list --done
python cli.py user login

ورودی تعاملی، محیط و مقادیر پیش‌فرض

برای دریافت ورودی در زمان اجرا، از prompt ها استفاده کنید. برای خواندن از متغیر محیطی، envvar را تعیین کنید.

import os
import typer

app = typer.Typer()

@app.command()
def config(
    api_key: str = typer.Option(
        ...,
        "--api-key",
        prompt=True,
        hide_input=True,
        help="کلید API",
        envvar="MYAPP_API_KEY",
    ),
):
    # اگر کاربر تعیین نکند، از MYAPP_API_KEY خوانده می‌شود.
    typer.echo(f"کلید تنظیم شد: {'*' * len(api_key)}")

if __name__ == "__main__":
    app()

مدیریت خطا و خروجی مناسب

برای خطاهای قابل پیش‌بینی از BadParameter، Abort یا Exit استفاده کنید تا پیام دوستانه و کد خروج درست برگردانده شود.

import typer

app = typer.Typer()

def ensure_even(value: int):
    if value % 2 != 0:
        raise typer.BadParameter("عدد باید زوج باشد.")
    return value

@app.command()
def process(n: int = typer.Argument(..., callback=ensure_even)):
    try:
        typer.echo(f"در حال پردازش مقدار زوج: {n}")
        # منطق پردازش...
    except Exception as e:
        typer.secho(f"خطای غیرمنتظره: {e}", fg=typer.colors.RED)
        raise typer.Exit(code=1)

if __name__ == "__main__":
    app()

تست کردن CLI با Typer

برای اطمینان از رفتار درست، از تستر داخلی Typer (بر پایه Click) استفاده کنید.

# test_cli.py
from typer.testing import CliRunner
import app  # ماژولی که در آن app = typer.Typer() تعریف شده

runner = CliRunner()

def test_hello():
    result = runner.invoke(app.app, ["hello", "سارا"])
    assert result.exit_code == 0
    assert "سلام سارا!" in result.stdout

def test_even_required():
    result = runner.invoke(app.app, ["process", "3"])
    assert result.exit_code != 0
    assert "عدد باید زوج باشد" in result.stdout

اجرا:

pytest -q

بسته‌بندی و ساخت دستور سیستمی

برای اینکه CLI شما بدون python app.py اجرا شود، آن را در قالب پکیج نصب‌پذیر منتشر کنید. با pyproject.toml می‌توانید اسکریپت سیستمی بسازید.

# pyproject.toml (بخش‌های مرتبط)
[project]
name = "mycli"
version = "0.1.0"
description = "A demo CLI with Typer"
readme = "README.md"
requires-python = ">=3.8"
dependencies = ["typer[all]"]

[project.scripts]
mycli = "mycli.cli:app"

ساختار فایل‌ها:

mycli/
  mycli/
    __init__.py
    cli.py   # شامل app = typer.Typer() و دستورات
  pyproject.toml

نصب محلی:

pip install -e .  # نصب editable در حالت توسعه
mycli --help      # حالا دستور در سیستم قابل اجراست

تکمیل خودکار (Autocomplete)

اگر Typer را با extras نصب کرده باشید، دستورات زیر معمولا در برنامه شما فعال هستند:

mycli --install-completion   # نصب اسکریپت تکمیل خودکار برای شل جاری
mycli --show-completion      # نمایش اسکریپت تکمیل برای افزودن دستی

پس از نصب، با زدن Tab هنگام نوشتن گزینه‌ها و زیرفرمان‌ها، پیشنهادها نمایش داده می‌شود.

یک سناریوی کامل: Todo CLI کوچک

در این بخش یک Todo CLI ساده با فایل JSON می‌سازیم. هدف: نمایش ذخیره‌سازی، افزودن، لیست و اتمام کار.

# file: todo.py
import json
from pathlib import Path
from typing import List
import typer

app = typer.Typer(help="مدیریت ساده کارها")
DB_PATH = Path("todos.json")

def load() -> List[dict]:
    if not DB_PATH.exists():
        return []
    return json.loads(DB_PATH.read_text(encoding="utf-8"))

def save(items: List[dict]) -> None:
    DB_PATH.write_text(json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8")

@app.command()
def add(title: str = typer.Argument(..., help="عنوان کار")):
    items = load()
    items.append({"title": title, "done": False})
    save(items)
    typer.secho("کار افزوده شد.", fg=typer.colors.GREEN)

@app.command("list")
def list_():
    items = load()
    if not items:
        typer.echo("فهرست خالی است.")
        raise typer.Exit()
    for i, it in enumerate(items, start=1):
        status = "✓" if it["done"] else "•"
        typer.echo(f"{i}. {status} {it['title']}")

@app.command()
def done(index: int = typer.Argument(..., min=1, help="شماره کار (از 1 شروع می‌شود)")):
    items = load()
    if index <= 0 or index > len(items):
        raise typer.BadParameter("شماره کار معتبر نیست.")
    items[index - 1]["done"] = True
    save(items)
    typer.secho("به عنوان انجام‌شده علامت خورد.", fg=typer.colors.GREEN)

if __name__ == "__main__":
    app()

بررسی سریع:

python todo.py add "مرور PRها"
python todo.py add "نوشتن تست‌ها"
python todo.py list
python todo.py done 1
python todo.py list

نکات کاربردی و خطاهای رایج

  • فراموشی تایپ‌هینت: بدون تعیین نوع، اعتبارسنجی خودکار ضعیف می‌شود. همیشه نوع پارامترها را مشخص کنید.
  • نامگذاری گزینه‌ها: برای گزینه‌های پرکاربرد نام کوتاه هم تعیین کنید (مثل -p). نام‌های طولانی توصیفی باشند.
  • پیام‌های خطا: از typer.BadParameter برای خطاهای ورودی استفاده کنید تا خروجی –help تمیز بماند و کد خروج مناسب برگردد.
  • تست CLI: از typer.testing.CliRunner استفاده کنید؛ تست کردن فقط منطق نیست، قرارداد رابط کاربری را نیز پایدار نگه می‌دارد.
  • ساختار پروژه: برای توسعه‌پذیری، هر گروه فرمان را در ماژول جداگانه نگه دارید و با app.add_typer جمع کنید.
  • عملیات طولانی: برای تجربه بهتر از نوار پیشرفت استفاده کنید: with typer.progressbar(items) as bar: ...

چه زمانی گزینه دیگری مناسب است؟

اگر به کمترین وابستگی و کنترل دستی روی پارسینگ نیاز دارید، argparse کفایت می‌کند. اما اگر می‌خواهید با حداقل کد، راهنمای خودکار، تایپ‌هینت، زیرگروه‌ها و تجربه کاربری بهتر داشته باشید، Typer انتخاب مناسبی است.

گام بعدی چیست؟

یک CLI کوچک واقعی در پروژه‌تان اضافه کنید: یکی از اسکریپت‌های تکراری را به Typer منتقل کنید، برای آن –help کامل بنویسید، چند تست پایه اضافه کنید و سپس با pyproject آن را به یک دستور سیستمی تبدیل کنید. همین چرخه کوچک، استاندارد توسعه ابزارهای خط فرمان شما را بالا می‌برد.