Can I use a context value as a click.option() default?
Asked Answered
S

2

5

I want to use a value in my configuration (which I load into my context) as the default value for a click command option. I've read this section of the documentation, and I don't think I'm understanding what I need do.

This is my example script:

import sys
import click

@click.group()
@click.pass_context
def cli(ctx):
    """
    CLI
    """
    ctx.ensure_object(dict)
    ctx.obj['DEFAULT_ENVIRONMENT'] = "dev"


@cli.command()
@click.option('-e', '--environment', required=True, default=click.get_current_context().obj['DEFAULT_ENVIRONMENT'])
def show_env(environment):
    click.echo(environment)


if __name__ == '__main__':
    cli()

The goal, if I run python cli.py show-env, is to get it to output dev (because I didn't pass the parameter as it's loaded from context).

This fails with

Traceback (most recent call last):
  File "testcli.py", line 15, in <module>
    @click.option('-e', '--environment', required=True, default=click.get_current_context().obj['DEFAULT_ENVIRONMENT'])
  File "/home/devuser/.virtualenvs/cli/lib/python3.6/site-packages/click/globals.py", line 26, in get_current_context
    raise RuntimeError('There is no active click context.')
RuntimeError: There is no active click context.

I have also tried by using @pass_context on my show_env command like so:

@cli.command()
@click.option('-e', '--environment', required=True, default=ctx.obj['DEFAULT_ENVIRONMENT'])
@click.pass_context
def show_env(ctx, environment):
    click.echo(environment)

Which fails because ctx isn't defined at that point.

Traceback (most recent call last):
  File "testcli.py", line 15, in <module>
    @click.option('-e', '--environment', required=True, default=ctx.obj['DEFAULT_ENVIRONMENT'])
NameError: name 'ctx' is not defined

Am I able to use my context to set a command option default value?

Structuralism answered 8/5, 2019 at 14:9 Comment(0)
K
5

As you noted, the context does not yet exist at the time you are trying to examine it. You can delay the default lookup from context until the context does exist, by using a custom class like:

Custom Class

def default_from_context(default_name):

    class OptionDefaultFromContext(click.Option):

        def get_default(self, ctx):
            self.default = ctx.obj[default_name]
            return super(OptionDefaultFromContext, self).get_default(ctx)

    return OptionDefaultFromContext

Using the Custom Class

To use the custom class pass it to click.option via the cls parameter like:

@click.option('-e', '--environment', required=True,
              cls=default_from_context('DEFAULT_ENVIRONMENT'))

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 override click.Option.get_default(). In our get_default() we examine the context and set the default value. We then call the parent get_default() to continue further processing.

Test Code:

import click

@click.group()
@click.pass_context
def cli(ctx):
    """
    CLI
    """
    ctx.ensure_object(dict)
    ctx.obj['DEFAULT_ENVIRONMENT'] = "dev"


@cli.command()
@click.option('-e', '--environment', required=True,
              cls=default_from_context('DEFAULT_ENVIRONMENT'))
def show_env(environment):
    click.echo(environment)


if __name__ == "__main__":
    commands = (
        'show_env',
        '--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.5 (v3.6.5:f59c0932b4, Mar 28 2018, 05:52:31) 
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.57)]
-----------
> show_env
dev
-----------
> --help
Usage: click_prog.py [OPTIONS] COMMAND [ARGS]...

  CLI

Options:
  --help  Show this message and exit.

Commands:
  show_env
Knowling answered 8/5, 2019 at 15:14 Comment(0)
I
1

To expand on Stephen Rauch's answer above, for Click 8, the signature of click.Option.get_default is get_default(self, ctx: Context, call: bool = True), so you might need to add the call argument.

Innate answered 19/1, 2022 at 18:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.