Generate argument parsers from Python 3 function annotations with minimal boilerplate.
#!/usr/bin/env python3.8
from clii import App
from pathlib import Path
from subprocess import run
cli = App()
@cli.cmd
def add(a: int, b: int = 3):
print(a + b)
@cli.cmd
@cli.arg('path', '-p', help='Destroy where?')
def subtract(path: Path):
run(f'rm -rf {path}')
@cli.main
def main():
print(f"{add(1, 2)} and {subtract('/uhoh')}")
if __name__ == '__main__':
cli.run()
-
No dependencies. This library has no dependencies and is a single file. You wanna vendor it? Vendor it.
-
Short implementation. Take 10 minutes, skim the implementation, convince yourself I'm not exfiltrating your
id_rsa
, then vendor this puppy and never think about anything again. -
Nothing to learn. if you know how to use Python function annotations, you already know 98% of this library.
-
Optimized for the common case. Check out
test_bad_git.py
. I know what you want to do (create a subpar reproduction of git), and I've made it concise. -
Functions can still be used as plain old functions. You can still call the clii-decorated functions regularly with no special ceremony. The decorators are only used to construct a parser.
Okay, you and I both know the last thing that anyone needs is another way to generate command line interfaces. The idea of adding an additional dependency to your project just so you can learn yet another only-slightly-more-ergonomic-than-stdlib interface for parsing args is right up there with rewriting all your Makefiles in whatever flavor-of-the-week Javascript-based build system. I get it.
Yes, instead of writing this library I should probably do something actually
useful like try to find a life partner or see how much grain alcohol I can
drink within the span of an X-Files episode, but each time I'm typing out some
overly verbose argparse
incantation that I had to look up on docs.python.org
for the sixteenth time in a year, one of the few remaining shreds of childlike
wonder for computing left in my over-caffeinated heart gets crosslegged and
sets itself on fire.
Click is the equivalent of calling in an architect to fix your kitchen sink. It's a lot of code and the interface is wordy and unintuitive. Docopt is neat but it's slow, a novelty, also a ton of code, and I have to read 3 examples each time before I use it. Argparse is an alright builtin, and the noble progenitor of this library, but it's overly verbose and the common task of wiring up subparsers that call functions is a pain.
Don't immolate your childlike wonder. Use function annotations. Use this stupid library.
# Requires Python >=3.7
python3 -m pip install --user clii
#!/usr/bin/env python3.8
"""
A really lame version of git.
"""
from pathlib import Path
import typing as t
from clii import App
cli = App(description=__doc__)
cli.add_arg('--verbose', '-v', action='store_true', default=False)
@cli.cmd
def clone(url: str, target: Path, branch: t.Optional[str] = None):
"""Clone the branch so you can melt your computer."""
branch = f' -b {branch}' if branch else ''
# We can reference global args since all parsed arguments are attached
# to `cli.args` after parsing.
if cli.args.verbose:
print(f'git clone{branch} {url} {target}')
@cli.cmd
def push(remote: str, branch: str, force: bool = False):
force_flag = ' -f' if force else ''
if cli.args.verbose:
print(f'git push{force_flag} {remote} {branch}')
@cli.cmd
@cli.arg('all', '-a') # add a short flag, or any other argparse setting
@cli.arg('message', '-m')
def commit(all: bool = False, message: str = ''):
# Arguments are --all, -a and --message, -m
print(all)
print(message)
@cli.cmd
@cli.arg('updated', '-u')
def add(*files, updated: bool = False):
# `files` will be a variadic positional arg, while --updated/-u is a bool
# flag.
if cli.args.verbose:
print(f"adding files: {files}")
if __name__ == '__main__':
cli.run()
which then gets you
% ./test_bad_git.py --help
usage: test_bad_git.py [-h] [--verbose] {clone,push,commit,add} ...
A really lame version of git.
positional arguments:
{clone,push,commit,add}
optional arguments:
-h, --help show this help message and exit
--verbose, -v
% ./test_bad_git.py clone --help
usage: test_bad_git.py clone [-h] [--branch BRANCH] url target
Clone the branch so you can melt your computer.
positional arguments:
url
target
optional arguments:
-h, --help show this help message and exit
--branch BRANCH default: None
Decorating any function with the @cli.main
decorator means that function
is the default subparser chosen if no function argument is given.
You can add arbitrary ArgumentParser.add_argument(...)
args for a given
parameter by using the @cli.arg('paramname', <additional args>...)
decorator.
This decorator must be applied on lines after the @cli.cmd
decorator.
clii will pull argument help text from docstrings that are formatted like so:
import clii
cli = clii.App()
@cli.cmd
def foo(bar: str):
"""
Args:
bar: some kind of helpful docstring.
"""
cli.run()
Specifically, the docstring is searched for " [parameter name]:" - if that pattern is found, the contents after the colon are used as help text.
Arguments that are declared type bool and given a default value are inferred
as being store_true
or store_false
depending upon their default value;
it is inferred that if the flag is given on the commandline, the reverse of
the default is desired.
For example, in
@cli.cmd
def commit(force: bool = False):
...
if --force
is given, the function will be called with force=True
, but
otherwise force
will stay False.
If you like using this library, consider sending me some magic internet money:
bc1qlj36t63qlgkywg83gwslcu3ehl76h2k2d6hcv2