Commands with multiple common options going into one argument using custom decorator

自闭症网瘾萝莉.ら 提交于 2019-12-04 04:44:53

You can distill multiple options into a single object quite simply by constructing a decorator like:

Code:

def magic_options(func):
    @click.option('--magic-bar')
    @click.option('--magic-foo')
    def distill_magic(magic_foo, magic_bar, **kwargs):
        kwargs['magic'] = Magic(magic_foo, magic_bar)
        func(**kwargs)

    return distill_magic

Using the decorator

You can then apply the decorator to the command function like:

@click.command('Colored-Magic')
@click.option('--color')
@magic_options
def cli(magic, color):
    ...

It needs to be applied to the bare function. This is because the function returned by click.option has been modified by the click framework and it won't work the way you expected.

Test Code:

import click

@click.command('Colored-Magic')
@click.option('--color')
@magic_options
def cli(magic, color):
    click.echo(str(magic))
    click.echo(color)


class Magic(object):
    def __init__(self, magic_foo, magic_bar):
        self.magic_foo = magic_foo
        self.magic_bar = magic_bar

    def __str__(self):
        return "foo: {}  bar: {}".format(self.magic_foo, self.magic_bar)


if __name__ == "__main__":
    commands = (
        '--magic-foo fooby --magic-bar barbecue',
        '--magic-foo fooby',
        '--magic-bar barbecue',
        '',
        '--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)]
-----------
> --magic-foo fooby --magic-bar barbecue
foo: fooby  bar: barbecue

-----------
> --magic-foo fooby
foo: fooby  bar: None

-----------
> --magic-bar barbecue
foo: None  bar: barbecue

-----------
> 
foo: None  bar: None

-----------
> --help
Usage: test.py [OPTIONS]

Options:
  --color TEXT
  --magic-bar TEXT
  --magic-foo TEXT
  --help            Show this message and exit.

Changing some "magic attrs" of a function in a decorator is perfectly normal: functools.wraps does just that. So you can:

  1. Use @wraps
  2. Use @click.option "after" @wraps
  3. define magic options as a list, and parse args/kwargs using values in that list.
from functools import wraps, WRAPPER_ASSIGNMENTS

DEFAULT_MAGIC_OPTIONS = ('--magic-foo', '--magic-bar')


def magic_command(name, magic_options=DEFAULT_MAGIC_OPTIONS):
    magic_options = magic_options or []
    magic_kwarg_names = [opt.split('--', 1)[1].replace('-', '_') for opt in magic_options]

    def decorator(func):
        @wraps(func, assigned=WRAPPER_ASSIGNMENTS+('__click_params__', ))
        def wrapper(*args, **kwargs):
            num_used_magic_args = min(len(magic_kwarg_names), len(args))

            magic_args = args[:num_used_magic_args]

            # If you want magic options to be "args only", then:
            # * you can raise TypeError if num_used_magic_args != len(magic_kwarg_names)
            # * you should not calculate `magic_kwargs`
            magic_kwargs = {}
            for kwarg_name in magic_kwarg_names[num_used_magic_args:]:
                if kwarg_name in kwargs:
                    magic_kwargs[kwarg_name] = kwargs.pop(kwarg_name)

            print(f'Initializing Magic with args={magic_args}, kwargs={magic_kwargs}')
            magic = Magic(*magic_args, **magic_kwargs)
            return func(magic, *args[num_used_magic_args:], **kwargs)

        for magic_option in magic_options[::-1]:  # Reverse order, to have proper positional arguments
            wrapper = click.option(magic_option)(wrapper)

        return click.command(f'{name}-Magic')(wrapper)

    return decorator

Usage:

@magic_command('Colored')
@click.option('--color')  # Note: wrapper will be properly updated 
# with this @click.option, but related argument will not be passed
# into `Magic(...)` initialization.
# If you want `color` to be passed into `Magic`: specify it as one 
# of the items in `magic_options` argument of magic_command decorator:
# `@magic_command('Colored', magic_options=DEFAULT_MAGIC_OPTIONS+('color', ))`
# AND remove it from function definition here (keep only `magic`)
def cmd(magic, color):
    assert isinstance(magic, Magic)
    pass

I don't know if it's possible to do what you want without using click internals but, there must be a way, right?

Anyway, here is a solution that makes use of another decorator. This one is put just above the function and its function is to group the magic_* parameters.

def magic_command(f):
    f = click.option('--magic-bar')(f)
    f = click.option('--magic-foo')(f)
    f = click.command()(f)
    return f

def group_magic_args(f):
    def new_f(magic_foo, magic_bar, *args, **kwargs):
        magic = Magic(magic_foo, magic_bar)
        f(magic=magic, *args, **kwargs)
    return new_f

You would use the new decorator like this:

@magic_command
@click.option('--color')
@group_magic_args
def cmd(magic, color):
    pass
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!