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 main that 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.