Click - Dynamic Defaults for Prompts based on other options
Asked Answered
F

2

5

I'm using Click to build a CLI interface. Click offers dynamic defaults for prompts, which is great. Also this example gives some insights on how to implement dynamic defaults using a custom click-class and thus provide more flexible options when evaluating the default value.

What I'm trying to do now is to have dynamic defaults based on another provided click option, e.g.

python mymodule --param1 something --param2 somethingelse

Now if param2 is empty I want to try to get a dynamic default based on the provided param1 input, e.g.:

@click.command()
@click.option('--param1', prompt=True)
@click.option('--param2', prompt=True, default=lambda: myfunct(param1))
def cmd(param1, param2):
    pass

myfunct(param1:str=None):
    param2 = None
    #get param2 based on param1 input
    return param2

Any ideas on what would be the best way to get that done? And is it guaranteed that param1 is evaluated (and prompted for) before param2?

Flannery answered 14/8, 2018 at 17:12 Comment(0)
C
3

Extending the example you referenced the desired functionality can be done like:

###Custom Class:

import click

class OptionPromptNull(click.Option):
    _value_key = '_default_val'

    def __init__(self, *args, **kwargs):
        self.default_option = kwargs.pop('default_option', None)
        super(OptionPromptNull, self).__init__(*args, **kwargs)

    def get_default(self, ctx, **kwargs):
        if not hasattr(self, self._value_key):
            if self.default_option is None:
                default = super(OptionPromptNull, self).get_default(ctx, **kwargs)
            else:
                arg = ctx.params[self.default_option]
                default = self.type_cast_value(ctx, self.default(arg))
            setattr(self, self._value_key, default)
        return getattr(self, self._value_key)

    def prompt_for_value(self, ctx):
        default = self.get_default(ctx)

        # only prompt if the default value is None
        if default is None:
            return super(OptionPromptNull, self).prompt_for_value(ctx)

        return default

###Using the Custom Class:

To use the custom class you need to pass three parameters to the click.option decorator like:

@click.option('--param3', cls=OptionPromptNull, default_option='param1',
              default=lambda x: get_a_value(x), prompt="Enter Param3")
  • cls need to reference the custom class.

  • default_option needs to specify which option will be passed to the default callable.

  • default specifies the callable used to get the default.

###How does this work?

This works because click is a well designed OO framework. The @click.option() decorator usually instantiates a click.Option object but allows this behavior to be over ridden with the cls parameter. So it is a relatively easy matter to inherit from click.Option in our own class and over ride desired methods.

In this case we over ride the click.Option.get_default() and the click.Option.prompt_for_value() methods. In prompt_for_value() we only prompt if the default is None. And then in get_default() we call the default function passing the desired (previously entered) param.

And to clarify one part of the question, the options are evaluated first: in the order they were passed on the command line, and second: in the order they were declared for those that were not passed on the command line.

###Test Code:

@click.command()
@click.option('--param1', prompt="Enter Param1")
@click.option('--param2', cls=OptionPromptNull,
              default=lambda: get_value_none(), prompt="Enter Param2")
@click.option('--param3', cls=OptionPromptNull, default_option='param1',
              default=lambda x: get_a_value(x), prompt="Enter Param3")
def cli(param1, param2, param3):
    click.echo("param1: '{}'".format(param1))
    click.echo("param2: '{}'".format(param2))
    click.echo("param3: '{}'".format(param3))


def get_value_none():
    return None

def get_a_value(val):
    return val

if __name__ == "__main__":
    commands = (
        r'',
        r'--param3 5',
        '--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)]
-----------
> 
Enter Param1: 3
Enter Param2: 4
param1: '3'
param2: '4'
param3: '3'
-----------
> --param3 5
Enter Param1: 3
Enter Param2: 4
param1: '3'
param2: '4'
param3: '5'
-----------
> --help
Usage: test.py [OPTIONS]

Options:
  --param1 TEXT
  --param2 TEXT
  --param3 TEXT
  --help         Show this message and exit.
Clothe answered 15/8, 2018 at 3:8 Comment(1)
This works up through click 7.1.2 but fails with click 8.0.0: TypeError: get_default() got an unexpected keyword argument 'call'. The signature of get_default changes to def get_default(self, ctx, call: bool = True):.Damage
K
0

I'm not sure if this is considered best practice, but another way to achieve the same effect is to use click.get_current_context() as part of a function that computes the default value:

@click.command()
@click.option(
    "--param1",
    type=str,
    prompt="First parameter",
)
@click.option(
    "--param2",
    type=str,
    prompt="Second parameter",
    default=lambda: myfunct(click.get_current_context().params.get("param1", None)),
)
def cmd(param1, param2):
    pass
Kenyakenyatta answered 2/2, 2023 at 7:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.