Python Click: custom error message
Asked Answered
G

1

10

I use the excellent Python Click library for handling command line options in my tool. Here's a simplified version of my code (full script here):

@click.command(
    context_settings = dict( help_option_names = ['-h', '--help'] )
)
@click.argument('analysis_dir',
                 type = click.Path(exists=True),
                 nargs = -1,
                 required = True,
                 metavar = "<analysis directory>"
)

def mytool(analysis_dir):
   """ Do stuff """

if __name__ == "__main__":
    mytool()

If someone runs the command without any flags, they get the default click error message:

$ mytool

Usage: mytool [OPTIONS] <analysis directory>

Error: Missing argument "analysis_dir".

This is nice, but I'd quite like to tell (very) novice users that more help is available by using the help flag. In other words, add a custom sentence to the error message when the command is invalid telling people to try mytool --help for more information.

Is there an easy way to do this? I know I could remove the required attribute and handle this logic in the main function, but that feels kind of hacky for such a minor addition.

Giselagiselbert answered 20/9, 2016 at 13:55 Comment(5)
I'm having exactly the same issue. I'd either like to automatically print out the help if a mistake happens, or at the very least tell the user how to print out help. Otherwise we have to assume that everyone using our tool is familiar with unix-based thinking. I like click so far but am disheartened that this question hasn't been answered, because I want something that will help build tools that novices can easily use.Kinfolk
Just found this, which might be a solution: #35642702Kinfolk
Nope, that didn't work.Kinfolk
What about click.pocoo.org/6/exceptions/#what-if-i-don-t-want-that ?Sackbut
Yes - I looked at exactly this @Sackbut but couldn't figure out how to actually implement it when not using the command.main(['command-name', 'args', 'go', 'here']) syntax. I couldn't find any examples anywhere and gave up. If you have any pointers as to how to apply this with the syntax in the example above, that would be really helpful!Giselagiselbert
C
7

Message construction for most errors in python-click is handled by the show method of the UsageError class: click.exceptions.UsageError.show.

So, if you redefine this method, you will be able to create your own customized error message. Below is an example of a customization which appends the help menu to any error message which answers this SO question:

def modify_usage_error(main_command):
    '''
        a method to append the help menu to an usage error

    :param main_command: top-level group or command object constructed by click wrapper 
    :return: None
    '''

    from click._compat import get_text_stderr
    from click.utils import echo
    def show(self, file=None):
        import sys
        if file is None:
            file = get_text_stderr()
        color = None
        if self.ctx is not None:
            color = self.ctx.color
            echo(self.ctx.get_usage() + '\n', file=file, color=color)
        echo('Error: %s\n' % self.format_message(), file=file, color=color)
        sys.argv = [sys.argv[0]]
        main_command()

    click.exceptions.UsageError.show = show

Once you define your main command, you can then run the modifier script:

import click
@click.group()
def cli():
    pass

modify_usage_error(cli)

I have not explored whether there are runtime invocations of ClickException other than usage errors. If there are, then you might need to modify your custom error handler to first check that ctx is an attribute before you add the line click.exceptions.ClickException.show = show since it does not appear that ClickException is fed ctx at initialization.

Choleric answered 11/5, 2017 at 17:28 Comment(4)
Fantastic, thanks RJ! This works perfectly with one exception - I had to remove the final function call to main_command() as this caused a recursion error.Giselagiselbert
This felt good! github.com/ewels/MultiQC/commit/62cc60 I've had a low level of annoyance about not being able to do this for seven months now! Thanks again!Giselagiselbert
Any suggestion how to overwrite all the click exception types at once? Ideally so it doesn't break when click add's new exceptions?Reynalda
You might try to iterate over dir(click.exceptions), exclude the built-in methods https://mcmap.net/q/532676/-dir-without-built-in-methods and then setattr(object, 'show', show). docs.python.org/3/library/functions.html#setattr However this may cause unexpected behavior.Choleric

© 2022 - 2024 — McMap. All rights reserved.