I am looking for some advice to avoid having to instantiate a class twice; this is more of a design pattern question. I am creating an application using the Python Click library.
I have a Settings
class that first loads all initial default settings into a dictionary (hard-coded into the application), then loads all settings overrides (if specified) from a TOML file on the user's computer into a dictionary, and then finally merges the two and makes them available as attributes of the class instance (settings.<something>
).
For most of these settings, I also want to be able to specify a command-line flag. The priority then becomes:
In order to achieve this result, I am finding that, when using Click's decorators, I have to do something like this:
import click
from myapp import Settings
settings = Settings()
pass_settings = click.make_pass_decorator(Settings, ensure=True)
@click.command()
@click.help_option('-h', '--help')
@click.option(
'-s', '--disk-size',
default=settings.instance_disk_size,
help="Disk size",
show_default=True,
type=int
)
@click.option(
'-t', '--disk-type',
default=settings.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)
settings = Settings()
line is needed to provide the @click.option
decorators with the default
value. The default
value could either come from the user override TOML file (if present), or from the application default.Settings
class.My question is, which is better? Is there a way to use the pass_settings
decorator in the other click.option
decorators? Or should I ditch using click.make_pass_decorator
entirely?
Python click module is used to create command-line (CLI) applications. It is an easy-to-use alternative to the standard optparse and argparse modules. It allows arbitrary nesting of commands, automatic help page generation, and supports lazy loading of subcommands at runtime.
This is a special attribute where commands are supposed to remember what they need to pass on to their children. In order for this to work, we need to mark our function with pass_context() , because otherwise, the context object would be entirely hidden from us.
Click, or “Command Line Interface Creation Kit” is a Python library for building command line interfaces. The three main points of Python Click are arbitrary nesting of commands, automatic help page generation, and supporting lazy loading of subcommands at runtime.
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:
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
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'))
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.
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
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]
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With