Introduction
Click is a useful CLI library, but many projects might have have been written in argparse (the standard python library).
Also some people don't love the heavy use of decorators (the code look messier than it should), people find the argparse.Namespace way
to handle parameters better, some don't love the idea of "context", and there are plenty of features that are pretty confusing.
Let's introduce some pattern that can reduce the friction of using Click.
Part I - feed your main an argparse.Namespace
The starting point is a click command like that:
@click.command()
@click.option("-x", type=int, default=0)
@click.option("-y", type=float, default=0)
def main(x: int, y: float) -> None:
print(f"Got {x*2}")
The arguments to main are multiple variables (potentially many), but we want to replace them with a single argparse.Namespace instance like:
@command()
@click.option("-x", type=int, default=0)
@click.option("-y", type=float, default=0)
def main(args: argparse.Namespace) -> None:
print(f"Got {args.x*2}")
The only changes are:
- replacing
click.command()->command() - x (int) and y (floar) arguments are attribute to an instance arg of argparse.Namespace
This is what command looks like:
def command(klass=argparse.Namespace):
def _command0(fn):
@functools.wraps(fn)
def _command1(*args, **kwargs):
options = klass(**kwargs)
return fn(options)
wrapped = _command1
wrapped = click.command()(wrapped)
return wrapped
return _command0
[!NOTE] You can go fancy, replacing click.command() with cloup.command() from Cloup
Part II - create collections of options
It is nice to create collections of commonly used options, especially within an organization where multiple users/developers work side by side: there is a need for share a common set of flags.
This is what a collection of flags would look like:
def mygroup(fn):
fn = click.option("-v", "--verbose", count=True)(fn)
fn = click.option("-q", "--quiet", count=True)(fn)
return fn
Note the click style option wrapping: we preserve it, so it will look natural. This is the new command with the new options:
@command()
@click.option("-x", type=int, default=0)
@click.option("-y", type=float, default=0)
@mygroup
def main(args: argparse.Namespace) -> None:
print(f"Got {args.x*2}")
Part III - controlling the narrative
So far the collection of flags is quite useful, but we're left with two gaps:
- the verbose flag is passed to
mainthat is in charge of handling it - we don't control what happen to the flags once passed to
main
@command()
@mygroup
def main(args: argparse.Namespace) -> None:
... <- before continuing, we might want to set a logging based on the --verbose/--quiet option
print(f"Got {args.x*2}")
... <- we might want to perform a cleaup based on some option (eg. rolling back a transaction)
I'll write more about this in another blog post.