Is there a way to handle exceptions automatically with Python Click?
Asked Answered
E

2

11

Click's exception handling documentation mentions that certain kinds of exceptions such as Abort, EOFError and KeyboardInterrupt are automatically handled gracefully by the framework.

For the application I'm writing, there are a lot of points from which exceptions could be generated. Terminating the application is the right step, but printing the stack trace isn't. I could always manually do this:

@cli.command()
def somecommand:
  try:
    # ...
  except Exception as e:
    click.echo(e)

However, is there a way to have Click handle all exceptions automatically?

Etherealize answered 25/8, 2017 at 6:54 Comment(0)
K
17

In our CLI, all commands are grouped under a single command group. This allowed us to implement some behavior that needed to be executed for each command. One part of that is the exception handling.

Our entry point looks something like this:

 @click.group()
 @click.pass_context
 def entry_point(ctx):
      ctx.obj = {"example": "This could be the configuration"}

We use it to run global code, e.g. configure the context, but you can also define an empty method that does nothing. Other commands can be added to this command group either by using the @entry_point.command() decorator or entry_point.add_command(cmd).

For the exception handling, we wrap the entry_point in another method that handles the exceptions:

 def safe_entry_point():
      try:
          entry_point()
      except Exception as e:
          click.echo(e)

In setup.py, we configure the entry point for the CLI and point it to the wrapper:

 entry_points={
    'console_scripts': [
        'cli = my.package:safe_entry_point'
    ]
}

The commands of the CLI can be executed through its command group: e.g. cli command.

There might be more elegant solutions out there, but this is how we solved it. While it introduces a command group as the highest-level element in your CLI, but it allows us do handle all exceptions in a single place without the need to duplicate our error handling in each and every command.

Klipspringer answered 25/8, 2017 at 12:41 Comment(4)
This seems like an elegant solution.Angleworm
Hi, I am wondering (haven't yet checked the click source code) whether it would be possible to access the object I am using for sub-commands. In cases like these, it seems logical to have a finally clause in that exception handler to shut things down properly after the exception.Gabionade
Update to my prior comment. Managed to use the call_on_close to add a cleanup actions in such case. ctx.call_on_close(ctx.obj.clean_up_function)Gabionade
This is good as long as you don't need details from within the app to handle the error. I'd like to access the verbosity level in the error handler, but I don't see a way (apart from global hacks). This makes sense, after all, click and the app it wraps are "dead" at this point.Benzine
T
3

If you only want to handle exception only for certain CLI commands. You could use another decorator, to handle exceptions.

Here's an example:

import click
from functools import wraps, partial


class NumberTooLarge(Exception):
    pass

def catch_exception(func=None, *, handle):
    if not func:
        return partial(catch_exception, handle=handle)

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except handle as e:
            raise click.ClickException(e)

    return wrapper

@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@catch_exception(handle=(NumberTooLarge, ValueError))
def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    if count > 100:
        raise NumberTooLarge('count cannot be greater than 100')
    if count < 0:
        raise ValueError('count too small')
    click.echo('Great choice!')


if __name__ == "__main__":
    hello()

Threedimensional answered 18/8, 2021 at 10:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.