在 Python 中更好地使用 `make_pass_decorator` 点击
Better usage of `make_pass_decorator` in Python Click
我正在寻找一些建议,以避免必须实例化一个 class 两次;这更像是一个设计模式问题。我正在使用 Python Click 库创建一个应用程序。
我有一个 Settings
class 首先将所有初始默认设置加载到字典中(硬编码到应用程序中),然后从 TOML 文件加载所有设置覆盖(如果指定)在用户的计算机上放入字典,然后最终将两者合并并使它们作为 class 实例 (settings.<something>
).
的属性可用
对于其中的大多数设置,我还希望能够指定一个命令行标志。然后优先级变为:
- 命令行标志。如果未指定,则回退到...
- TOML 文件中的用户设置。如果未指定,则最后回退到...
- 应用程序默认值
为了达到这个结果,我发现,在使用 Click 的装饰器时,我必须这样做:
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()
行来为 @click.option
装饰器提供 default
值。 default
值可以来自用户覆盖 TOML 文件(如果存在),也可以来自应用程序默认值。
- click.make_pass_decorator seems to be the recommended way for interleaved commands; it's even mentioned in their documentation。在函数内部,除了传递的CLI参数外,我有时还需要引用
Settings
class. 中的其他属性
我的问题是,哪个更好?有没有办法在其他 click.option
装饰器中使用 pass_settings
装饰器?或者我应该完全放弃使用 click.make_pass_decorator
吗?
解决不想实例化 Settings
两次的问题的一种方法是从 click.Option
继承,并将设置实例插入到
上下文直接像:
自定义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
使用自定义 Class:
要使用自定义 class,请将 cls
参数传递给 @click.option()
装饰器,例如:
# 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'))
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.option()
装饰器通常实例化一个
click.Option
对象,但允许使用 cls 参数覆盖此行为。所以是一个相对
在我们自己的 class 中继承 click.Option
并覆盖所需的方法很容易。
在这种情况下,我们使用几个闭包来捕获设置实例和参数名称。在返回的
class 我们超越了 click.Option.handle_parse_result()
以允许我们将设置对象插入到上下文中。
这允许 pass_settings
装饰器在上下文中找到设置,因此不需要创建新实例。
测试代码:
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]
不同意见
不是修改点击调用和使用动态 class 构造,而是将默认设置公开为设置 class 的 class 属性。即:
@click.option(
'-t', '--disk-type',
default=settings.instance_disk_type,
help="Disk type",
show_default=True,
type=click.Choice(['pd-standard', 'pd-ssd'])
)
变成
@click.option(
'-t', '--disk-type',
default=Settings.defaults.instance_disk_type,
help="Disk type",
show_default=True,
type=click.Choice(['pd-standard', 'pd-ssd'])
)
这可能比在已接受的答案中使用 class 构造函数更清晰,并且使代码的语义(含义)更清晰。
事实上,Settings.defaults
很可能是 Settings
的一个实例。实例化两次并不重要,因为这不是真正的问题,而是 Settings 对象的 client/consumer 代码必须执行实例化。如果在 Settings
class 中完成,它仍然是一个干净的 API 并且不需要调用者实例化两次。
我正在寻找一些建议,以避免必须实例化一个 class 两次;这更像是一个设计模式问题。我正在使用 Python Click 库创建一个应用程序。
我有一个 Settings
class 首先将所有初始默认设置加载到字典中(硬编码到应用程序中),然后从 TOML 文件加载所有设置覆盖(如果指定)在用户的计算机上放入字典,然后最终将两者合并并使它们作为 class 实例 (settings.<something>
).
对于其中的大多数设置,我还希望能够指定一个命令行标志。然后优先级变为:
- 命令行标志。如果未指定,则回退到...
- TOML 文件中的用户设置。如果未指定,则最后回退到...
- 应用程序默认值
为了达到这个结果,我发现,在使用 Click 的装饰器时,我必须这样做:
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()
行来为@click.option
装饰器提供default
值。default
值可以来自用户覆盖 TOML 文件(如果存在),也可以来自应用程序默认值。 - click.make_pass_decorator seems to be the recommended way for interleaved commands; it's even mentioned in their documentation。在函数内部,除了传递的CLI参数外,我有时还需要引用
Settings
class. 中的其他属性
我的问题是,哪个更好?有没有办法在其他 click.option
装饰器中使用 pass_settings
装饰器?或者我应该完全放弃使用 click.make_pass_decorator
吗?
解决不想实例化 Settings
两次的问题的一种方法是从 click.Option
继承,并将设置实例插入到
上下文直接像:
自定义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
使用自定义 Class:
要使用自定义 class,请将 cls
参数传递给 @click.option()
装饰器,例如:
# 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'))
这是如何工作的?
之所以可行,是因为 click 是一个设计良好的 OO 框架。 @click.option()
装饰器通常实例化一个
click.Option
对象,但允许使用 cls 参数覆盖此行为。所以是一个相对
在我们自己的 class 中继承 click.Option
并覆盖所需的方法很容易。
在这种情况下,我们使用几个闭包来捕获设置实例和参数名称。在返回的
class 我们超越了 click.Option.handle_parse_result()
以允许我们将设置对象插入到上下文中。
这允许 pass_settings
装饰器在上下文中找到设置,因此不需要创建新实例。
测试代码:
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]
不同意见
不是修改点击调用和使用动态 class 构造,而是将默认设置公开为设置 class 的 class 属性。即:
@click.option(
'-t', '--disk-type',
default=settings.instance_disk_type,
help="Disk type",
show_default=True,
type=click.Choice(['pd-standard', 'pd-ssd'])
)
变成
@click.option(
'-t', '--disk-type',
default=Settings.defaults.instance_disk_type,
help="Disk type",
show_default=True,
type=click.Choice(['pd-standard', 'pd-ssd'])
)
这可能比在已接受的答案中使用 class 构造函数更清晰,并且使代码的语义(含义)更清晰。
事实上,Settings.defaults
很可能是 Settings
的一个实例。实例化两次并不重要,因为这不是真正的问题,而是 Settings 对象的 client/consumer 代码必须执行实例化。如果在 Settings
class 中完成,它仍然是一个干净的 API 并且不需要调用者实例化两次。