具有多个常用选项的命令使用自定义装饰器进入一个参数
Commands with multiple common options going into one argument using custom decorator
我想制作一个模块,使构建共享大量选项的点击命令变得非常简单。这些选项将被提炼成一个传递给命令的对象。作为说明性示例:
from magic import magic_command
import click
@magic_command('Colored')
@click.option('--color')
def cmd(magic, color):
pass
整个命令将有许多 --magic-...
选项进入 magic
传递给 cmd
的对象。我能够使用以下方法实现这一目标:
def magic_command(name):
def decorator(func):
@click.option('--magic-foo')
@click.option('--magic-bar')
def wrapper(magic_foo, magic_bar, **kwargs):
print(f'initializing Magic with {magic_foo} and {magic_bar}')
magic = Magic(magic_foo, magic_bar)
func(magic, **kwargs)
try:
wrapper.__click_params__.extend(func.__click_params__)
except AttributeError:
pass
return click.command(f'{name}-Magic')(wrapper)
return decorator
不过,乱用 __click_params__
似乎不是特别干净。
这个问题有点类似于,但是这种方法不允许我将许多魔法选项浓缩成一个魔法对象。
详细说明,使用这种方法我必须做
@magic_command('Colored')
@click.option('--color')
def cmd(magic_foo, magic_bar, color):
magic = Magic(magic_foo, magic_bar)
pass
但这意味着自定义代码需要知道有哪些魔法选项以及如何构建魔法。我想这可以使用 **kwargs
进行简化,但仍然 - 理想情况下我只想将准备好的 magic
对象传递给 cmd
.
在装饰器中更改函数的一些 "magic attrs" 是完全正常的:
functools.wraps
就是这样做的。所以你可以:
- 使用
@wraps
- 使用
@click.option
"after"@wraps
- 将魔术选项定义为列表,并使用该列表中的值解析 args/kwargs。
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
用法:
@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
我不知道是否可以在不使用点击内部的情况下做你想做的事,但是,一定有办法,对吧?
无论如何,这是一个使用另一个装饰器的解决方案。这个放在函数的正上方,它的作用是对magic_*
个参数进行分组。
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
您可以像这样使用新的装饰器:
@magic_command
@click.option('--color')
@group_magic_args
def cmd(magic, color):
pass
您可以通过构造像这样的装饰器非常简单地将多个选项提炼到一个对象中:
代码:
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
使用装饰器
然后您可以将装饰器应用于命令函数,例如:
@click.command('Colored-Magic')
@click.option('--color')
@magic_options
def cli(magic, color):
...
需要应用在bare函数上。这是因为 click.option
返回的函数已被 click 框架修改,它不会按您预期的方式工作。
测试代码:
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
结果:
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.
我想制作一个模块,使构建共享大量选项的点击命令变得非常简单。这些选项将被提炼成一个传递给命令的对象。作为说明性示例:
from magic import magic_command
import click
@magic_command('Colored')
@click.option('--color')
def cmd(magic, color):
pass
整个命令将有许多 --magic-...
选项进入 magic
传递给 cmd
的对象。我能够使用以下方法实现这一目标:
def magic_command(name):
def decorator(func):
@click.option('--magic-foo')
@click.option('--magic-bar')
def wrapper(magic_foo, magic_bar, **kwargs):
print(f'initializing Magic with {magic_foo} and {magic_bar}')
magic = Magic(magic_foo, magic_bar)
func(magic, **kwargs)
try:
wrapper.__click_params__.extend(func.__click_params__)
except AttributeError:
pass
return click.command(f'{name}-Magic')(wrapper)
return decorator
不过,乱用 __click_params__
似乎不是特别干净。
这个问题有点类似于
详细说明,使用这种方法我必须做
@magic_command('Colored')
@click.option('--color')
def cmd(magic_foo, magic_bar, color):
magic = Magic(magic_foo, magic_bar)
pass
但这意味着自定义代码需要知道有哪些魔法选项以及如何构建魔法。我想这可以使用 **kwargs
进行简化,但仍然 - 理想情况下我只想将准备好的 magic
对象传递给 cmd
.
在装饰器中更改函数的一些 "magic attrs" 是完全正常的:
functools.wraps
就是这样做的。所以你可以:
- 使用
@wraps
- 使用
@click.option
"after"@wraps
- 将魔术选项定义为列表,并使用该列表中的值解析 args/kwargs。
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
用法:
@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
我不知道是否可以在不使用点击内部的情况下做你想做的事,但是,一定有办法,对吧?
无论如何,这是一个使用另一个装饰器的解决方案。这个放在函数的正上方,它的作用是对magic_*
个参数进行分组。
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
您可以像这样使用新的装饰器:
@magic_command
@click.option('--color')
@group_magic_args
def cmd(magic, color):
pass
您可以通过构造像这样的装饰器非常简单地将多个选项提炼到一个对象中:
代码:
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
使用装饰器
然后您可以将装饰器应用于命令函数,例如:
@click.command('Colored-Magic')
@click.option('--color')
@magic_options
def cli(magic, color):
...
需要应用在bare函数上。这是因为 click.option
返回的函数已被 click 框架修改,它不会按您预期的方式工作。
测试代码:
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
结果:
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.