1

When writing a command line tool in Python using Typer one can create a CLI with commands and even subcommands. If you define a CLI with only one command the CLI will be optimized such that you do not have to provide the command - let's call this modul cli_a.py:

#!env python

import typer

app = typer.Typer()

@app.command()
def main():
    print('This is the output of main')

if __name__ == '__main__':
    app()

Now, you can call this CLI like so

$ ./cli_a.py --help

 Usage: cli_a.py [OPTIONS]

╭─ Options ───────────────────────────────────────────────────────────────────╮
│ --install-completion          Install completion for the current shell.     │
│ --show-completion             Show completion for the current shell, to     │
│                               copy it or customize the installation.        │
│ --help                        Show this message and exit.                   │
╰─────────────────────────────────────────────────────────────────────────────╯

and

$ ./cli_a.py 
This is the output of main

Notice, that there has to be no command called main!

On the other hand you can have a CLI with multiple commands - let's call this cli_b.py:

#!env python
import typer

app = typer.Typer()

@app.command()
def cmd1():
    print('This is the output of cmd1')

@app.command()
def cmd2():
    print('This is the output of cmd2')

if __name__ == '__main__':
    app()

With the following output:

$ ./cli_b.py --help

 Usage: cli_b.py [OPTIONS] COMMAND [ARGS]...

╭─ Options ───────────────────────────────────────────────────────────────────╮
│ --install-completion          Install completion for the current shell.     │
│ --show-completion             Show completion for the current shell, to     │
│                               copy it or customize the installation.        │
│ --help                        Show this message and exit.                   │
╰─────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────────╮
│ cmd1                                                                        │
│ cmd2                                                                        │
╰─────────────────────────────────────────────────────────────────────────────╯

In this case you have to provide the command to be called:

$ ./cli_b.py cmd1
This is the output of cmd1

I want to combine these two CLI into one - let's call this super_cli.py:

#!env python
import typer
import cli_a
import cli_b

app = typer.Typer()
app.add_typer(cli_a.app, name='cli_a')
app.add_typer(cli_b.app, name='cli_b')

if __name__ == '__main__':
    app()

This behaves as expected for cli_b:

$ ./super_cli.py cli_b cmd1
This is the output of cmd1

But requires an unwanted additional command main on cli_a:

$ ./super_cli.py cli_a main
This is the output of main

How can it be achieved that cli_a is callable without specifying the additional command main?

I was expecting to get

$ ./super_cli.py cli_a
This is the output of main

but I do get

$ ./super_cli.py cli_a
Usage: super_cli.py cli_a [OPTIONS] COMMAND [ARGS]...
Try 'super_cli.py cli_a --help' for help.
╭─ Error ─────────────────────────────────────────────────────────────────────╮
│ Missing command.                                                            │
╰─────────────────────────────────────────────────────────────────────────────╯

instead.

1

2 Answers 2

0

Searching typer default command in Google I found:
Set the default command in Python Typer CLI

You need to change command() into callback(invoke_without_command=True) in cli_a.py and it will treat main() as default command.

import typer

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main():
    print('This is the output of main')

if __name__ == '__main__':
    app()

And now ./super_cli cli_a runs cli_a.main()

Executing directly ./cli_a.py still work as before.


I used callback() on cmd1 in cli_b

import typer

app = typer.Typer()

@app.callback(invoke_without_command=True)
def cmd1():
    print('This is the output of cmd1')

@app.command()
def cmd2():
    print('This is the output of cmd2')

if __name__ == '__main__':
    app()

and it was running cmd1 automatically for super_cli.py cli_b BUT it was running it autmatically also for super_cli.py cli_b cmd2 :)

$ ./super_cli.py cli_b

This is the output of cmd1


$ ./super_cli.py cli_b cmd2

This is the output of cmd1
This is the output of cmd2

Some links to documentation:

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your answer. I do use callbacks to add in help and options available accross all commands of one cli. However, what I am not happy about is that extending a cli with a second command at some later point in time requires to refactor the callback back into a command.
I don't remeber if I tried to run script (which has only one function) using only callback. If it would work then you could always use callback for first function. OR I would always add both command() and callback() but callback in comment - so it would be simpler to replace it when I would add second function. Other idea: check source code to see how it works when script has only one function. But this can be hard job. I wonder if it could be possible to use some if/else which would add differen decorators if code has more functions. Some decorators can be used like functions.
0

Also, if you want to do something different when the subcommand is called without any other argument, use ctx.invoked_subcommand

import subprocess
import typer

app = typer.Typer()

@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
    """Seed the database with default data."""
    if ctx.invoked_subcommand is None:
        print("Seeding all the data...")

@app.command()
def clients() -> None:
    """Seed the clients"""
    print("Seeding clients...")


if __name__ == "__main__":
    app()

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.