typer logo typer: subcommands

1 2 3 4 5 6 7 8 9 10
Notes

If you want to add hierarchy to your command line app then you might care to introduce groups of subcommands. This is also a great way to seperate concerns in your code.

Adding Subgroups in Typer

Let's split the code up into two files now. Here is the code in our main cli.py file.

import typer
from typing import List
from pathlib import Path

from .db import app as db_app

app = typer.Typer(name="demo", add_completion=False, help="This is a demo app.")

# Here is where we attach subcommands as a group
app.add_typer(db_app, name="db")

@app.command()
def hello_world(name):
    """Say hello"""
    print(f"hello {name}!")

@app.command()
def goodbye_world(name):
    """Say goodbye"""
    print(f"goodbye {name}!")

@app.command()
def add(n1: int = typer.Argument(..., help="An integer"),
        n2: int = typer.Argument(1, help="An integer")):
    """Add two numbers"""
    print(n1 + n2)

def check_file_exists(paths):
    for p in paths:
        if not p.exists():
            print(f"The path you've supplied {p} does not exist.")
            raise typer.Exit(code=1)
    return paths

@app.command()
def word_count(paths: List[Path] = typer.Argument(...,
                                                help="The file to count the words in.",
                                                callback=check_file_exists)):
    """Counts the number of words in a file"""
    for p in paths:
        texts = p.read_text().split("\n")
        n_words = len(set(w for t in texts for w in t.split(" ")))
        print(f"In total there are {n_words} words in {p}.")

@app.command()
def talk(text: str = typer.Argument(..., help="The text to type."),
        repeat: int = typer.Option(1, help="Number of times to repeat."),
        loud: bool = typer.Option(False, is_flag=True)):
    """Talks some text below"""
    if loud:
        text = text.upper()
    for _ in range(repeat):
        print(text)


if __name__ == "__main__":
    app()

The code for the "database" commands is found here:

# db.py
import typer
from typing import List
from pathlib import Path

app = typer.Typer(name="demo", add_completion=False, help="This is a demo app.")


@app.command()
def create_db(table: str = typer.Option(..., prompt="What is the name of the table?",
                                        confirmation_prompt=True)):
    """Pretend to make a database"""
    console.print(f"Creating table in database {table}", style="green")

@app.command()
def delete_db(table: str = typer.Option(..., prompt="What is the name of the table?",
                                        confirmation_prompt=True)):
    """Pretend to delete a database"""
    sure = typer.confirm("Are you really really really sure?")
    if sure:
        console.print(f"Deleting table in database {table}", style="red")
    else:
        console.print(f"Back to safety!", style="green")

You should now notice that when you run the command line, that there's a subgroup now.

> python cli.py
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

This is a demo app.

Options:
--help  Show this message and exit.

Commands:
add            Add two numbers
db             Database commands.
goodbye-world  Say goodbye
hello-world    Say hello
talk           Talks some text below
word-count     Counts the number of words in a file

That means that we're able to add hierarchy in both our command line app. But also in our code.