Inspired by FastAPI, I thought it would be a fantastic idea to be able to define command-line interfaces based on Python function signatures with type-hints.
I decided try and extend the Python standard library argparse
with this capability. A few days of hacking later this library was born.
Of course I found out soon that exactly this already exists. It's called typer and it's written by, how could it be otherwise, the author of FastAPI.
Nevertheless, as an internet monument of my reinventing the wheel, here's FastCLI. It's like typer, but based on argparse. It's not thoroughly tested, but the examples below should work.
#!/bin/bash
from fastcli import CLI
VERBOSITY = 0
cli = CLI()
@cli.command()
def foo(x: int, y: str, z: str = 'Default'):
"""Lorum ipsum."""
if VERBOSITY > 0:
print(f'x: {x} ({type(x)})')
print(f'y: {y} ({type(y)})')
print(f'z: {z} ({type(z)})')
return x, y, z
@cli.command(name='bar', aliases=['b'], description='Bar')
def bar_defition(x: int, y: str, z: str = 'Default', flag: bool = False):
"""Lorum ipsum."""
if VERBOSITY > 0:
print(f'x: {x} ({type(x)})')
print(f'y: {y} ({type(y)})')
print(f'z: {z} ({type(z)})')
return x, y, z
if __name__ == '__main__':
cli.add_argument(
'-v',
'--verbosity',
type=int,
help='Set the verbosity level',
choices=[-1, 0, 1],
default=VERBOSITY
)
args = cli.parse_args()
VERBOSITY = args.verbosity
cli.execute()
FastCLI can also be used with async functions:
import asyncio
from fastcli import CLI
async def afoo(x: int):
await asyncio.sleep(x)
async def abar(x: bool = False):
return x
if __name__ == '__main__':
asyncio.run(cli.execute())
Running with the argument custom
prints the following usage message:
usage: tests.py custom [-h] [--z Z] [--flag] x y
Lorum ipsum.
positional arguments:
x type: int
y type: str
optional arguments:
-h, --help show this help message and exit
--z Z type: str, default: Default
--flag type: bool
- Docstrings are turned into help command descriptions
- Function arguments are converted into command line arguments
- Types and default values are shown in the help texts
FastCLI creates a command-line interface based on functions you write with the @cli.command()
decorator.
It coerces string arguments to python types based on type hints provided in the function signatures.
Under the hood, fastcli.CLI
is an ArgumentParser
from the argparse
package.
That means that most functionality that is available in argparse is available in FastCLI.
For each function you define with the @cli.command
decorator, a subparser is added that transforms command-line arguments into parameters of your function, coerced to the correct type.
While FastCLI is designed to maintain the behavior of argparse as much as possible, there are some nuances that you should be aware of.
However, it is also possible to nest sub-parsers:
cli = CLI()
cli.add_argument('-v', '--verbosity', type=int, help='Set the verbosity level', choices=[-1, 0, 1], default=0)
command = cli.add_command('foo') # takes care of calling cli.add_subparsers()
command.add_parameter('x', type_=int) # these methods would normally be invoked based on a function signature
command.add_parameter('y', type_=int, default)
Positional arguments and keyword-only arguments without a default value arguments are converted to positional command-line arguments. Keyword arguments with a default value are converted to command-line options.
For some types, special behaviours are defined that exploit the capabilities of argparse.
Boolean arguments with a default value of False are converted into flags.
This is achieved by creating an argument with action="store_true"
.
Enumerations are converted into arguments with a fixed set of choices.
This is achieved by creating an argument with choices=Enum.__members__.keys()
.
Lists, tuples and sets are converted into arguments that consume a variable number of parameters. Tuples may be defined to be of a fixed size, For positional arguments, it is only possible to have one positional argument if it is a list, tuple with unbounded number of elements, or set.