【问题标题】:Better usage of `make_pass_decorator` in Python Click在 Python Click 中更好地使用`make_pass_decorator`
【发布时间】:2018-09-05 19:21:06
【问题描述】:

我正在寻找一些建议来避免两次实例化一个类;这更像是一个设计模式问题。我正在使用Python Click 库创建一个应用程序。

我有一个Settings 类,它首先将所有初始默认设置加载到字典中(硬编码到应用程序中),然后将所有设置覆盖(如果指定)从用户计算机上的 TOML 文件加载到字典中,然后最后将两者合并,使其可用作类实例的属性 (settings.<something>)。

对于大多数这些设置,我希望能够指定命令行标志。然后优先级变为:

  1. 命令行标志。如果未指定,则回退到...
  2. TOML 文件中的用户设置。如果未指定,则最终回退到...
  3. 应用默认

为了达到这个效果,我发现在使用 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 似乎是交错命令的推荐方式;甚至提到了in their documentation。在函数内部,除了传递的CLI参数外,我有时还需要引用Settings类中的其他属性。

我的问题是,哪个更好?有没有办法在其他 click.option 装饰器中使用 pass_settings 装饰器?还是我应该完全放弃使用click.make_pass_decorator

【问题讨论】:

    标签: python decorator python-decorators python-click


    【解决方案1】:

    不同意见

    不要修改点击调用并使用动态类构造,而是将默认设置公开为 Settings 类的类属性。即:

    @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'])
    )
    

    这可能比在接受的答案中使用类构造函数更清晰,并使代码的语义(含义)更加清晰。

    事实上,Settings.defaults 很可能是Settings 的一个实例。您实例化两次并不重要,因为这不是真正的问题,而是您的 Settings 对象的客户端/消费者代码必须执行实例化。如果这是在 Settings 类中完成的,它仍然是一个干净的 API,并且不需要调用者实例化两次。

    【讨论】:

      【解决方案2】:

      解决不想实例化Settings 两次的问题的一种方法是从click.Option 继承,并将设置实例插入 上下文直接喜欢:

      自定义类:

      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
          
      

      使用自定义类:

      要使用自定义类,请将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 参数覆盖此行为。所以这是一个相对 在我们自己的类中从 click.Option 继承并覆盖所需的方法很容易。

      在这种情况下,我们使用几个闭包来捕获 Settings 实例和参数名称。在返回的 我们覆盖 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]
      

      【讨论】:

      • 斯蒂芬,如此高质量的答案,正是我想要的!我在 Python 方面越来越好,但像我这样的问题对于初学者/中级人员来说往往很麻烦。非常感谢!
      • @ScottCrooks,感谢您的客气话。作为回报,我会说你的问题质量也很高。去年我回答了fair number of Click Questions,但大多数都没有这个问题那么好。一个很好的问题通常更容易回答......祝你在 Python 之旅中好运。
      猜你喜欢
      • 2013-07-18
      • 2011-05-16
      • 1970-01-01
      • 2015-09-30
      • 1970-01-01
      • 2020-02-18
      • 1970-01-01
      • 1970-01-01
      • 2021-07-09
      相关资源
      最近更新 更多