pythoncommand-linecommand-line-interfacetyper

Python command line tool with subcommands in typer - How do I include a Typer instance with only one command?


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.


Solution

  • 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: