Better usage of `make_pass_decorator` in Python Click

One way to approach the problem of not wanting to instantiate Settings twice, is to inherit from click.Option, and insert the settings instance into the context directly like:

Custom Class:

def build_settings_option_class(settings_instance):

    def set_default(default_name):

        class Cls(click.Option):
            def __init__(self, *args, **kwargs):
                kwargs['default'] = getattr(settings_instance, default_name)
                super(Cls, self).__init__(*args, **kwargs)

            def handle_parse_result(self, ctx, opts, args):
                obj = ctx.find_object(type(settings_instance))
                if obj is None:
                    ctx.obj = settings_instance

                return super(Cls, self).handle_parse_result(ctx, opts, args)

        return Cls

    return set_default
    

Using Custom Class:

To use the custom class, pass the cls parameter to @click.option() decorator like:

# instantiate settings
settings = Settings()

# get the setting option builder
settings_option_cls = build_settings_option_class(settings)

# decorate with an option with an appropraie option name
@click.option("--an_option", cls=settings_option_cls('default_setting_name'))

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 the desired methods.

In this case we use a couple of closures to capture the Settings instance and parameter name. In the returned class we over ride click.Option.handle_parse_result() to allow us to insert the setting object into the context. This allows the pass_settings decorator to find the settings in the context, and thus it will not need to create a new instance.

Test Code:

import click

class Settings(object):

    def __init__(self):
        self.instance_disk_size = 100
        self.instance_disk_type = 'pd-ssd'


settings = Settings()
settings_option_cls = build_settings_option_class(settings)
pass_settings = click.make_pass_decorator(Settings)


@click.command()
@click.help_option('-h', '--help')
@click.option(
    '-s', '--disk-size',
    cls=settings_option_cls('instance_disk_size'),
    help="Disk size",
    show_default=True,
    type=int
)
@click.option(
    '-t', '--disk-type',
    cls=settings_option_cls('instance_disk_type'),
    help="Disk type",
    show_default=True,
    type=click.Choice(['pd-standard', 'pd-ssd'])
)
@pass_settings
def create(settings, disk_size, disk_type):
    print(disk_size)
    print(disk_type)


if __name__ == "__main__":
    commands = (
        '-t pd-standard -s 200',
        '-t pd-standard',
        '-s 200',
        '',
        '--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)
            create(cmd.split())

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

Test Results:

Click Version: 6.7
Python Version: 3.6.2 (default, Jul 17 2017, 23:14:31) 
[GCC 5.4.0 20160609]
-----------
> -t pd-standard -s 200
200
pd-standard
-----------
> -t pd-standard
100
pd-standard
-----------
> -s 200
200
pd-ssd
-----------
> 
100
pd-ssd
-----------
> --help
Usage: test.py [OPTIONS]

Options:
  -h, --help                      Show this message and exit.
  -s, --disk-size INTEGER         Disk size  [default: 100]
  -t, --disk-type [pd-standard|pd-ssd]
                                  Disk type  [default: pd-ssd]

Differing opinion

Instead of modifying the click invocation and using dynamic class construction, expose the default settings as a class attribute for the Settings class. IE:

@click.option(
    '-t', '--disk-type',
    default=settings.instance_disk_type,
    help="Disk type",
    show_default=True,
    type=click.Choice(['pd-standard', 'pd-ssd'])
)

becomes

@click.option(
    '-t', '--disk-type',
    default=Settings.defaults.instance_disk_type,
    help="Disk type",
    show_default=True,
    type=click.Choice(['pd-standard', 'pd-ssd'])
)

This is likely cleaner and makes the semantics (meaning) of your code much clearer than using a class constructor s in the accepted answer.

In fact, the Settings.defaults could well be an instance of Settings. It doesn't mater that you're instantiating twice, as this isn't really the issue here, rather that your client/consumer code for the Settings object has to perform the instantiation. If that's done in the Settings class, it remains a clean API and doesn't require the caller to instantiate twice.