2

I have a python click application that works great, but I want to be notified whenever a user types in an unknown command. For example, if mycli foo is valid, but they type in mycli bar, I want to override the default exception handling behavior and fire off an error to an error tracker, such as rollbar.

I found this page which describes how to override the exception handling, but it assumes I have a Command. The problem I've run into is that I've also integrated with setuptools by following this guide, and it points to my Command in the [console_scripts] section. For example, yourscript=yourscript:cli points to the cli command.

I'm not sure how to call cli.main() from within [console_scripts] or if that's even the right way of thinking about it.

0

1 Answer 1

1

With a custom click.Command class, you can capture the invoking command line and then report any error in the command line in an exception handler using a custom class like:

Custom Class

def CatchAllExceptions(cls, handler):

    class Cls(cls):

        _original_args = None

        def make_context(self, info_name, args, parent=None, **extra):

            # grab the original command line arguments
            self._original_args = ' '.join(args)

            try:
                return super(Cls, self).make_context(
                    info_name, args, parent=parent, **extra)
            except Exception as exc:
                # call the handler
                handler(self, info_name, exc)

                # let the user see the original error
                raise

        def invoke(self, ctx):
            try:
                return super(Cls, self).invoke(ctx)
            except Exception as exc:
                # call the handler
                handler(self, ctx.info_name, exc)

                # let the user see the original error
                raise

    return Cls


def handle_exception(cmd, info_name, exc):
    # send error info to rollbar, etc, here
    click.echo(':: Command line: {} {}'.format(info_name, cmd._original_args))
    click.echo(':: Raised error: {}'.format(exc))

Using the custom class

Then to use the custom command/group, pass it as the cls argument to the click.command or click.group decorator like one of:

@click.command(cls=CatchAllExceptions(click.Command, handler=report_exception))

@click.group(cls=CatchAllExceptions(click.Group, handler=report_exception))

@click.group(cls=CatchAllExceptions(click.MultiCommand, handler=report_exception))

Note the need to specify which click.Command subclass is required as well as the handler to send the exception information to.

How does this work?

This works because click is a well designed OO framework. The @click.group() and @click.command() decorators usually instantiates a click.Group or click.Command objects, but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Command (etc) in our own class and over ride desired methods.

In this case we over ride click.Command.make_context() to grab the original command line, and click.Command.invoke() to catch the exception and then call our exception handler.

Test Code:

import click

@click.group(cls=CatchAllExceptions(click.Group, handler=report_exception))
def cli():
    """A wonderful test program"""
    pass

@cli.command()
def foo():
    """A fooey command"""
    click.echo('Foo!')


if __name__ == "__main__":
    commands = (
        'foo',
        'foo --unknown',
        'foo still unknown',
        '',
        '--help',
        'foo --help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            cli(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> foo
Foo!
-----------
> foo --unknown
Error: no such option: --unknown
:: Command line: test.py foo --unknown
:: Raised error: no such option: --unknown
-----------
> foo still unknown
:: Command line: test.py foo still unknown
:: Raised error: Got unexpected extra arguments (still unknown)
Usage: test.py foo [OPTIONS]

Error: Got unexpected extra arguments (still unknown)
-----------
> 
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  A wonderful test program

Options:
  --help  Show this message and exit.

Commands:
  foo  A fooey command
-----------
> --help
Usage: test.py [OPTIONS] COMMAND [ARGS]...

  A wonderful test program

Options:
  --help  Show this message and exit.

Commands:
  foo  A fooey command
-----------
> foo --help
Usage: test.py foo [OPTIONS]

  A fooey command

Options:
  --help  Show this message and exit.
Sign up to request clarification or add additional context in comments.

1 Comment

Worked out of the box! Thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.