Examples¶
Below is a collection of standalone examples of specific argument/CLI behavior one might want, with comparison examples of how it might be done in each of Click/Typer/Argparse, where relevant.
Please bring up any specific usage questions about comparisons between Cappa and other libraries as an issue or a discussion. The list below can and should grow in response to feature-comparisons which are not clear just from reading the cappa-specific documentation.
Custom Option Value Name¶
i.e. “FOO” in
$ prog --help
Usage: prog --some-arg FOO
Cappa
from typing import Annotated
from dataclasses import dataclass
import cappa
@dataclass
class Args:
some_arg: Annotated[str, cappa.Arg(value_name='FOO')]
cappa.parse(Args)
Argparse
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--some-arg", metavar="FOO")
parser.parse_args()
Click
import click
@click.command()
@click.option('--some-arg', metavar="FOO")
def main():
...
main()
Typer
from typing_extensions import Annotated
import typer
def main(some_arg: Annotated[str, typer.Argument(metavar="FOO")]):
...
typer.run(main)
Unbounded option arguments¶
i.e. prog --foo 1 2 3 4 ...
Cappa
from typing import Annotated
from dataclasses import dataclass
import cappa
@dataclass
class Args:
foo: Annotated[list[str], cappa.Arg(num_args=-1)
print(cappa.parse(Args))
Argparse
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--foo", nargs="*")
print(parser.parse_args())
Click/Typer
Click’s (and thus Typer) parser, at time of writing, appears to not support this feature
Interactive Prompt¶
$ prog
What's your name?: Ron
Cappa
from typing import Annotated
from dataclasses import dataclass
from rich.prompt import Prompt
import cappa
@dataclass
class Args:
some_arg: Annotated[str, cappa.Arg(default=rich.Prompt("What's your name?"))]
cappa.parse(Args)
Argparse
Argparse requires custom code or a third party library to support this feature
Click
import click
@click.command()
@click.option('--name', prompt="What's your name?")
def main(name):
...
main()
Typer
import typer
from typing_extensions import Annotated
def main(name: str, lastname: Annotated[str, typer.Option(prompt="What's your name?")]):
...
typer.run(main)
Subcommands¶
$ prog double 123
246
$ prog print --title bar
Bar
Cappa
from __future__ import annotations
from dataclasses import dataclass
import cappa
@dataclass
class Args:
command: cappa.Subcommands[Double | Print]
@dataclass
class Double:
arg: int
def __call__(self):
print(self.arg * 2)
@dataclass
class Print:
text: str
title: bool = False
def __call__(self):
text = self.text.title() if self.title else self.text
print(text)
cappa.invoke(Args)
Not the shortest, but (I think) it’s still very intelligble. Certainly, the extra line or two dedicated to turning each command into a type can add some extra heft.
The mechanism by which the subcommands are declared is (I think) the clearest of the 4.
Argparse
import argparse
def double(arg: int):
print(arg * 2)
def print_command(text: str, title: bool):
text = text.title() if title else text
print(text)
parser = argparse.ArgumentParser()
subcommand = parser.add_subparsers(dest="command")
double_subcommand = subcommand.add_parser(name="double")
double_subcommand.add_argument("arg", type=int)
print_subcommand = subcommand.add_parser(name="print")
print_subcommand.add_argument("text")
print_subcommand.add_argument("--title", action="store_true", default=False)
args = parser.parse_args()
if args.command == "double":
double(args.arg)
elif args.command == "print":
print_command(args.text, args.title)
I had to look at the docs to implement, because the parser.add_subparsers
-> subparser.add_parser() API is non-intuitive.
Obviously the programmer needs to implement the subcommand dispatch that’s handled for you in all the other options.
Finally the resultant args namespace is completely untyped and you need to hope
that the shape you described matches how you access it.
Click
import click
@click.group()
def main():
...
@main.command()
@click.argument("arg", type=int)
def double(arg: int):
print(arg * 2)
@main.command(name="print")
@click.argument("text")
@click.option("--title", is_flag=True, default=False)
def print_command(text: str, title: str):
text = text.title() if title else text
print(text)
main()
Subcommands (particularly nested ones beyond the complexity of this example) are click’s strong suit.
The main annoyance is that it’s fairly repetitive. You need to manually choose the correct mapper/type, match the argument names yourself, and repeat the types if you want a typed function.
Typer
import typer
app = typer.Typer(name="example")
@app.command()
def double(arg: int):
print(arg * 2)
@app.command("print")
def print_command(text: str, title: bool = False):
text = text.title() if title else text
print(text)
app()
Typer is certainly the shortest in this example, and has the least repetition. This is probably Typer’s clearest case. It very simply and directly solves the repetition/mapping problem of click.
Console Output¶
$ prog World
Hello World
Cappa
from __future__ import annotations
from dataclasses import dataclass
import cappa
@dataclass
class Args:
text: str
def __call__(self, output: cappa.Output):
output(f"Hello {self.text}")
cappa.invoke(Args)
In order to manually output text, depend on an Output instance to be provided a
preconfigured output-able console object.
Importantly, this will natively route output through rich, which gains easy styling.
Argparse
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("text")
args = parser.parse_args()
print(f"Hello {args.text}")
Argparse just parses the arguments, so all choices of how to write output is left
to the programmer. (Note, you can do this with cappa also, by using the parse interface)
Click
import click
@main.command()
@click.argument("text")
def main(text: str):
click.echo(f"Hello {text})
main()
Typer
import typer
def main(text: str):
typer.echo(f"hello {text}")
typer.run(app)
Typer is almost identical to click, because Typer is built on click.
Typer also integrates well with rich, although it seems to encourage direct use of the rich Console-less API, which may(?) centralized control of styling within a CLI more challenging.