Problem

There is click module that allows you to create comman dline interfaces for your python scripts. The advantages of click are

  • nice syntax
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import click

@click.command()
@click.option('--count', default=1, help='Number of greetings.')
@click.option('--name', prompt='Your name',
              help='The person to greet.')
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello %s!' % name)

if __name__ == '__main__':
    hello()

  • straightforward argument parsing strategy (in comparison to the standard argparse )

Click doesn’t support config files by default. There is a number of additional modules that implement this feature.

Comparison

feature click-config click-configfile click_config_file SO snippet
Last commit in the repository May 5, 2015 Sep 24, 2017 Jan 23, 2018 -
Supports ini yes yes yes easy to implement
Supports json no no yes with strange extension technique easy to implement
Supports yaml yes no yes with strange extension technique yes
implementation notes click-config provides a decorator that takes a python object and overwrites its attributes with values passed into the program via command line arguments. through context_settings usable by simply adding the appropriate decorator Default values from click.option don’t work as expected. They override values from the file. One have to check for defaults manually. Not sure if environment variables work nicely.

Syntax examples

click-config

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from __future__ import print_function
import click
import click_config


class config(object):
    class logger(object):
        level = 'INFO'

    class mysql(object):
        host = 'localhost'


@click.command()
@click_config.wrap(module=config, sections=('logger', 'mysql'))
def main():
    print('log level: {}, mysql host: {}'.format(
        config.logger.level,
        config.mysql.host
    ))


if __name__ == '__main__':
    main()

click-configure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# BASIC SOLUTION FOR: Command that uses one or more configuration files.
import click

CONTEXT_SETTINGS = dict(default_map=ConfigFileProcessor.read_config())

@click.command(context_settings=CONTEXT_SETTINGS)
@click.option("-n", "--number", "numbers", type=int, multiple=True)
@click.pass_context
def command_with_config(ctx, numbers):
    """Example for a command that uses an configuration file"""
    pass
    ...

if __name__ == "__main__":
    command_with_config()

click_config_file

1
2
3
4
5
@click.command()
@click.option('--name', default='World', help='Who to greet.')
@click_config_file.configuration_option()
def hello(name):
    click.echo('Hello {}!'.format(name))

Stack overflow snippet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Custom Class:

def CommandWithConfigFile(config_file_param_name):

    class CustomCommandClass(click.Command):

        def invoke(self, ctx):
            config_file = ctx.params[config_file_param_name]
            if config_file is not None:
                with open(config_file) as f:
                    config_data = yaml.load(f)
                    for param, value in ctx.params.items():
                        if value is None and param in config_data:
                            ctx.params[param] = config_data[param]

            return super(CustomCommandClass, self).invoke(ctx)

    return CustomCommandClass
Using Custom Class:
1
2
3
4
5
@click.command(cls=CommandWithConfigFile('config_file'))
@click.argument("arg")
@click.option("--opt")
@click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
Test Code:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# !/usr/bin/env python
import click
import yaml

@click.command(cls=CommandWithConfigFile('config_file'))
@click.argument("arg")
@click.option("--opt")
@click.option("--config_file", type=click.Path())
def main(arg, opt, config_file):
    print("arg: {}".format(arg))
    print("opt: {}".format(opt))
    print("config_file: {}".format(config_file))


main('my_arg --config_file config_file'.split())

Test Results:

arg: my_arg
opt: my_opt
config_file: config_file