pythonpython-decoratorspython-click

How to override the help text of a shared python click option?


I am using python click options that are shared by multiple commands, as described at https://stackoverflow.com/a/77732441.

Is there a simple way to customize the help= text of the list option in the example below such for cmd1 it will be "List the apples in the basket" and for cmd2 it will be "List the oranges in the container".

My goal is to avoid having a complete --list option definition for each possible help text.

EDIT: I am looking for a solution where per-command help text is specified near the @list_option of that command, without modifying any shared definition. This is because in my application, each of the commands and shared option definitions are in their own .py files.

import sys
import click

# Shared option definition.
list_option = click.option(
  "-l",
  "--list",
  is_flag=True,
  help="List items."
)

@click.group()
@click.pass_context
def cli(ctx):
    pass

@cli.command()
@click.pass_context
@list_option
def cmd1(ctx, **kwargs):
    print(f'Running cmd1: {kwargs = }')

@cli.command()
@click.pass_context
@list_option
def cmd2(ctx, **kwargs):
    print(f'Running cmd2: {kwargs = }')

if __name__ == '__main__':
    sys.exit(cli())

Solution

  • Explanation for added things:

    Use a CustomHelpOption Class which overrides get_help_record method to change dynamically the help-text based on the command running.

    get_help_record method checks the name of the current command (ctx.command.name) and updates the self.help attribute accordingly before calling the parent class' get_help_record method.

    list_option is defined with class CustomHelpOption using cls parameter to have dynamic help text based on the command.

    import sys
    import click
    
    # Custom Option class to dynamically set the help text
    class CustomHelpOption(click.Option):
        def get_help_record(self, ctx):
            if ctx.command.name == 'cmd1':
                self.help = 'List the apples in the basket'
            elif ctx.command.name == 'cmd2':
                self.help = 'List the oranges in the container'
            return super().get_help_record(ctx)
    
    # Shared option definition with the custom class
    list_option = click.option(
        "-l",
        "--list",
        is_flag=True,
        cls=CustomHelpOption,  # Use the custom class
        help="List items.",  # This is a placeholder help text
    )
    
    @click.group()
    @click.pass_context
    def cli(ctx):
        pass
    
    @cli.command()
    @click.pass_context
    @list_option
    def cmd1(ctx, **kwargs):
        print(f'Running cmd1: {kwargs = }')
    
    @cli.command()
    @click.pass_context
    @list_option
    def cmd2(ctx, **kwargs):
        print(f'Running cmd2: {kwargs = }')
    
    if __name__ == '__main__':
        sys.exit(cli())
        
        
    

    Output :

    For cmd1, the help text for --list will be 'List the apples in the basket'. For cmd2, the help text for --list will be 'List the oranges in the container'.

    Hope I have understood your question correctly.

    Edit after reading the OP's comment:

    import sys
    import click
    
    # Shared option definition with generic help text
    def list_option(func):
        @click.option(
            '-l',
            '--list',
            is_flag=True,
            help='List items.'  # Placeholder help text
        )
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    
    # Custom decorator to set specific help text
    def custom_list_option(help_text):
        def decorator(func):
            # Create a new function with the specific help text
            @list_option
            @click.option(
                '-l',
                '--list',
                is_flag=True,
                help=help_text  # Custom help text
            )
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
        return decorator
    
    @click.group()
    @click.pass_context
    def cli(ctx): pass
    
    @cli.command()
    @click.pass_context
    @custom_list_option('List the apples in the basket')
    def cmd1(ctx, **kwargs): print(f'Running cmd1: {kwargs = }')
    
    @cli.command()
    @click.pass_context
    @custom_list_option('List the oranges in the container')
    def cmd2(ctx, **kwargs): print(f'Running cmd2: {kwargs = }')
    
    if __name__ == '__main__': sys.exit(cli())