【问题标题】:Setting options from environment variables when using argparse使用 argparse 时从环境变量设置选项
【发布时间】:2023-04-04 19:32:01
【问题描述】:

我有一个脚本,它具有某些选项,可以在命令行上传递,也可以从环境变量传递。如果两者都存在,则 CLI 应该优先,如果两者都没有设置,则会发生错误。

我可以在解析后检查该选项是否已分配,但我更愿意让 argparse 来完成繁重的工作,并在解析失败时负责显示使用语句。

我已经提出了几种替代方法(我将在下面发布它们作为答案,以便可以单独讨论),但我觉得它们很笨拙,我认为我遗漏了一些东西。

是否有公认的“最佳”方法?

(当 CLI 选项和环境变量都未设置时,编辑以明确所需的行为)

【问题讨论】:

    标签: python argparse


    【解决方案1】:

    一种选择是检查是否设置了环境变量,并相应地修改对 add_argument 的调用 例如

    import argparse
    import os
    
    parser=argparse.ArgumentParser()
    if 'CVSWEB_URL' in os.environ:
        cvsopt = { 'default': os.environ['CVSWEB_URL'] }
    else:
        cvsopt = { 'required': True }
    parser.add_argument(
        "-u", "--cvsurl", help="Specify url (overrides CVSWEB_URL environment variable)", 
        **cvsopt)
    args=parser.parse_args()
    

    【讨论】:

      【解决方案2】:

      我经常使用这种模式,所以我打包了一个简单的动作类来处理它:

      import argparse
      import os
      
      class EnvDefault(argparse.Action):
          def __init__(self, envvar, required=True, default=None, **kwargs):
              if not default and envvar:
                  if envvar in os.environ:
                      default = os.environ[envvar]
              if required and default:
                  required = False
              super(EnvDefault, self).__init__(default=default, required=required, 
                                               **kwargs)
      
          def __call__(self, parser, namespace, values, option_string=None):
              setattr(namespace, self.dest, values)
      

      然后我可以从我的代码中调用它:

      import argparse
      from envdefault import EnvDefault
      
      parser=argparse.ArgumentParser()
      parser.add_argument(
          "-u", "--url", action=EnvDefault, envvar='URL', 
          help="Specify the URL to process (can also be specified using URL environment variable)")
      args=parser.parse_args()
      

      【讨论】:

      • 是否应该默认将其编辑为在 os.environ 中查找? "如果 os.environ 中的 envvar:默认 = envvar" --> "如果 os.environ 中的 envvar:默认 = os.environ[envvar]"
      • 如果没有默认值,为什么只使用 envvar 指定的值?它不应该覆盖默认值吗,因为调用者已经明确提供了一个值?
      • 这个解决方案很棒。但是,它有一个副作用:根据您的环境(是否设置了 env var),使用文本可能会有所不同。
      • 这是一个很好的答案,因为随着您的需求发生变化,您可以轻松地向此类添加其他功能。我一直使用这个解决方案,效果很好。
      【解决方案3】:

      您可以将参数的default= 设置为os.environ.get(),并使用您要获取的环境变量。

      您还可以在.get() 调用中传递第二个参数,如果.get() 没有找到该名称的环境变量,这是默认值(默认情况下.get() 在这种情况下返回None) .

      import argparse
      import os
      
      parser = argparse.ArgumentParser(description='test')
      parser.add_argument('--url', default=os.environ.get('URL'))
      
      args = parser.parse_args()
      if not args.url:
          exit(parser.print_usage())
      

      【讨论】:

      • None 是 .get() 的默认值,因此不需要像这样明确声明。我可能应该在问题中更清楚 - 该选项至少需要在环境变量或 CLI 之一中。如果你像这样设置默认值,那么 args.url 很可能会以 None 结束,这是我想要避免的......
      • 啊,我明白你在找什么了。老实说,我会使用我写的内容,并在解析 args 后检查 if not args.url: exit(parser.print_usage()) 并退出。
      • 一种很好的快速处理方式。我已经打包了自己的动作处理程序,因为我经常使用这种模式,但这肯定是我对快速简单脚本的后备。
      • 在我看来,这似乎是更具表现力的方式。如果没有一个非常具体的原因变得更复杂,这只是优雅而简短。因此我认为这应该是“正确”的答案,尽管@RussellHeilling 也提出了一个很好的选择。
      • 这个答案提出的方式是糟糕的用户体验。 “默认”是指“应用程序的默认值”,而不是“当前环境中的默认值”。人们可能会查看 --help,查看特定默认值,假设它永远是默认值,转到另一台机器/会话,获取 kaboom。
      【解决方案4】:

      我想我会发布我的解决方案,因为最初的问题/答案给了我很多帮助。

      我的问题与罗素的问题有些不同。我使用的是 OptionParser,而不是每个参数的环境变量,我只有一个模拟命令行的环境变量。

      MY_ENVIRONMENT_ARGS = --arg1 "马耳他语" --arg2 "猎鹰" -r "1930" -h

      解决方案:

      def set_defaults_from_environment(oparser):
      
          if 'MY_ENVIRONMENT_ARGS' in os.environ:
      
              environmental_args = os.environ[ 'MY_ENVIRONMENT_ARGS' ].split()
      
              opts, _ = oparser.parse_args( environmental_args )
      
              oparser.defaults = opts.__dict__
      
      oparser = optparse.OptionParser()
      oparser.add_option('-a', '--arg1', action='store', default="Consider")
      oparser.add_option('-b', '--arg2', action='store', default="Phlebas")
      oparser.add_option('-r', '--release', action='store', default='1987')
      oparser.add_option('-h', '--hardback', action='store_true', default=False)
      
      set_defaults_from_environment(oparser)
      
      options, _ = oparser.parse_args(sys.argv[1:])
      

      在这里,如果找不到参数,我不会抛出错误。但如果我愿意,我可以做类似的事情

      for key in options.__dict__:
          if options.__dict__[key] is None:
              # raise error/log problem/print to console/etc
      

      【讨论】:

        【解决方案5】:

        这个话题很老了,但我有类似的问题,我想我会与你分享我的解决方案。不幸的是,@Russell Heilling 建议的自定义操作解决方案对我不起作用,原因如下:

        • 它阻止我使用predefined actions(如store_true
        • envvar 不在os.environ 中时,我希望它回退到default(这很容易解决)
        • 我希望在不指定actionenvvar(应始终为action.dest.upper())的情况下对我的所有参数都采用这种行为

        这是我的解决方案(在 Python 3 中):

        class CustomArgumentParser(argparse.ArgumentParser):
            class _CustomHelpFormatter(argparse.ArgumentDefaultsHelpFormatter):
                def _get_help_string(self, action):
                    help = super()._get_help_string(action)
                    if action.dest != 'help':
                        help += ' [env: {}]'.format(action.dest.upper())
                    return help
        
            def __init__(self, *, formatter_class=_CustomHelpFormatter, **kwargs):
                super().__init__(formatter_class=formatter_class, **kwargs)
        
            def _add_action(self, action):
                action.default = os.environ.get(action.dest.upper(), action.default)
                return super()._add_action(action)
        

        【讨论】:

        • 这对于 type=bool 失败有任何关于如何使其工作的想法吗?
        • @codebreach:我猜你打的是this problem
        【解决方案6】:

        ConfigArgParse 为 argparse 添加了对环境变量的支持,因此您可以执行以下操作:

        p = configargparse.ArgParser()
        p.add('-m', '--moo', help='Path of cow', env_var='MOO_PATH') 
        options = p.parse_args()
        

        【讨论】:

        • 很遗憾,env_var 不能与位置参数一起使用
        【解决方案7】:

        我通常必须为多个参数(身份验证和 API 密钥)执行此操作。这很简单直接。使用 **kwargs。

        def environ_or_required(key):
            return (
                {'default': os.environ.get(key)} if os.environ.get(key)
                else {'required': True}
            )
        
        parser.add_argument('--thing', **environ_or_required('THING'))
        

        【讨论】:

        • 不幸的是,它并不能真正使用真正的default 值(如果我想在 env 和 param 都不存在时回退到 argparse 默认值)
        • @TheGodfather : 如果你想指定一个“真实的”default,那么将参数设为必需是没有意义的!
        【解决方案8】:

        有一个example use-case for ChainMap,您可以在其中合并默认值、环境变量和命令行参数。

        import os, argparse
        
        defaults = {'color': 'red', 'user': 'guest'}
        
        parser = argparse.ArgumentParser()
        parser.add_argument('-u', '--user')
        parser.add_argument('-c', '--color')
        namespace = parser.parse_args()
        command_line_args = {k:v for k, v in vars(namespace).items() if v}
        
        combined = ChainMap(command_line_args, os.environ, defaults)
        

        a great talk 来找我关于美丽和惯用的 python。

        但是,我不确定如何区分小写和大写字典键。在-u foobar 作为参数传递并且环境设置为USER=bazbaz 的情况下,combined 字典看起来像{'user': 'foobar', 'USER': 'bazbaz'}

        【讨论】:

          【解决方案9】:

          这是一个相对简单(看起来更长,因为它的注释很好)但完整的解决方案,通过使用parse_args 的命名空间参数避免混淆default。默认情况下,它解析环境变量与命令行参数没有什么不同,尽管可以轻松更改。

          import shlex
          
          # Notes:
          #   * Based on https://github.com/python/cpython/blob/
          #               15bde92e47e824369ee71e30b07f1624396f5cdc/
          #               Lib/argparse.py
          #   * Haven't looked into handling "required" for mutually exclusive groups
          #   * Probably should make new attributes private even though it's ugly.
          class EnvArgParser(argparse.ArgumentParser):
              # env_k:    The keyword to "add_argument" as well as the attribute stored
              #           on matching actions.
              # env_f:    The keyword to "add_argument". Defaults to "env_var_parse" if
              #           not provided.
              # env_i:    Basic container type to identify unfilled arguments.
              env_k = "env_var"
              env_f = "env_var_parse"
              env_i = type("env_i", (object,), {})
          
              def add_argument(self, *args, **kwargs):
                  map_f = (lambda m,k,f=None,d=False:
                              (k, k in m, m.pop(k,f) if d else m.get(k,f)))
          
                  env_k = map_f(kwargs, self.env_k, d=True, f="")
                  env_f = map_f(kwargs, self.env_f, d=True, f=self.env_var_parse)
          
                  if env_k[1] and not isinstance(env_k[2], str):
                      raise ValueError(f"Parameter '{env_k[0]}' must be a string.")
          
                  if env_f[1] and not env_k[1]:
                      raise ValueError(f"Parameter '{env_f[0]}' requires '{env_k[0]}'.")
          
                  if env_f[1] and not callable(env_f[2]):
                      raise ValueError(f"Parameter '{env_f[0]}' must be callable.")
          
                  action = super().add_argument(*args, **kwargs)
          
                  if env_k[1] and not action.option_strings:
                      raise ValueError(f"Positional parameters may not specify '{env_k[0]}'.")
          
                  # We can get the environment now:
                  #   * We need to know now if the keys exist anyway
                  #   * os.environ is static
                  env_v = map_f(os.environ, env_k[2], f="")
          
                  # Examples:
                  # env_k:
                  #   ("env_var", True,  "FOO_KEY")
                  # env_v:
                  #   ("FOO_KEY", False, "")
                  #   ("FOO_KEY", True,  "FOO_VALUE")
                  #
                  # env_k:
                  #   ("env_var", False, "")
                  # env_v:
                  #   (""       , False, "")
                  #   ("",        True,  "RIDICULOUS_VALUE")
          
                  # Add the identifier to all valid environment variable actions for
                  # later access by i.e. the help formatter.
                  if env_k[1]:
                      if env_v[1] and action.required:
                          action.required = False
                      i = self.env_i()
                      i.a = action
                      i.k = env_k[2]
                      i.f = env_f[2]
                      i.v = env_v[2]
                      i.p = env_v[1]
                      setattr(action, env_k[0], i)
          
                  return action
          
              # Overriding "_parse_known_args" is better than "parse_known_args":
              #   * The namespace will already have been created.
              #   * This method runs in an exception handler.
              def _parse_known_args(self, arg_strings, namespace):
                  """precedence: cmd args > env var > preexisting namespace > defaults"""
          
                  for action in self._actions:
                      if action.dest is argparse.SUPPRESS:
                          continue
                      try:
                          i = getattr(action, self.env_k)
                      except AttributeError:
                          continue
                      if not i.p:
                          continue
                      setattr(namespace, action.dest, i)
          
                  namespace, arg_extras = super()._parse_known_args(arg_strings, namespace)
          
                  for k,v in vars(namespace).copy().items():
                      # Setting "env_i" on the action is more effective than using an
                      # empty unique object() and mapping namespace attributes back to
                      # actions.
                      if isinstance(v, self.env_i):
                          fv = v.f(v.a, v.k, v.v, arg_extras)
                          if fv is argparse.SUPPRESS:
                              delattr(namespace, k)
                          else:
                              # "_parse_known_args::take_action" checks for action
                              # conflicts. For simplicity we don't.
                              v.a(self, namespace, fv, v.k)
          
                  return (namespace, arg_extras)
          
              def env_var_parse(self, a, k, v, e):
                  # Use shlex, yaml, whatever.
                  v = shlex.split(v)
          
                  # From "_parse_known_args::consume_optional".
                  n = self._match_argument(a, "A"*len(v))
          
                  # From the main loop of "_parse_known_args". Treat additional
                  # environment variable arguments just like additional command-line
                  # arguments (which will eventually raise an exception).
                  e.extend(v[n:])
          
                  return self._get_values(a, v[:n])
          
          
          # Derived from "ArgumentDefaultsHelpFormatter".
          class EnvArgHelpFormatter(argparse.HelpFormatter):
              """Help message formatter which adds environment variable keys to
              argument help.
              """
          
              env_k = EnvArgParser.env_k
          
              # This is supposed to return a %-style format string for "_expand_help".
              # Since %-style strings don't support attribute access we instead expand
              # "env_k" ourselves.
              def _get_help_string(self, a):
                  h = super()._get_help_string(a)
                  try:
                      i = getattr(a, self.env_k)
                  except AttributeError:
                      return h
                  s = f" ({self.env_k}: {i.k})"
                  if s not in h:
                      h += s
                  return h
          
          
          # An example mix-in.
          class DefEnvArgHelpFormatter\
                  ( EnvArgHelpFormatter
                  , argparse.ArgumentDefaultsHelpFormatter
                  ):
              pass
          

          示例程序:

          parser = EnvArgParser\
                  ( prog="Test Program"
                  , formatter_class=DefEnvArgHelpFormatter
                  )
          
          parser.add_argument\
                  ( '--bar'
                  , required=True
                  , env_var="BAR"
                  , type=int
                  , nargs="+"
                  , default=22
                  , help="Help message for bar."
                  )
          
          parser.add_argument\
                  ( 'baz'
                  , type=int
                  )
          
          args = parser.parse_args()
          print(args)
          

          示例程序输出:

          $ BAR="1 2 3 '45  ' 6 7" ./envargparse.py 123
          Namespace(bar=[1, 2, 3, 45, 6, 7], baz=123)
          
          $ ./envargparse.py -h
          usage: Test Program [-h] --bar BAR [BAR ...] baz
          
          positional arguments:
            baz
          
          optional arguments:
            -h, --help           show this help message and exit
            --bar BAR [BAR ...]  Help message for bar. (default: 22) (env_var: BAR)
          

          【讨论】:

            【解决方案10】:

            你可以使用OptionParser()

            from optparse import OptionParser
            
            def argument_parser(self, parser):
                parser.add_option('--foo', dest="foo", help="foo", default=os.environ.get('foo', None))
                parser.add_option('--bar', dest="bar", help="bar", default=os.environ.get('bar', None))
                return(parser.parse_args())
            
            parser = OptionParser()
            (options, args) = argument_parser(parser)
            foo = options.foo
            bar = options.bar
            print("foo: {}".format(foo))
            print("bar: {}".format(bar))
            

            外壳:

            export foo=1
            export bar=2
            python3 script.py
            

            【讨论】:

            【解决方案11】:

            Click 库显式处理此问题:

            import click
            
            @click.command()
            @click.argument('src', envvar='SRC', type=click.File('r'))
            def echo(src):
                """Print value of SRC environment variable."""
                click.echo(src.read())
            

            从命令行:

            $ export SRC=hello.txt
            $ echo
            Hello World!
            

            https://click.palletsprojects.com/en/master/arguments/#environment-variables

            你可以安装它

            pip install click
            

            【讨论】:

              【解决方案12】:

              另一种选择:

                  parser = argparse.ArgumentParser()
                  env = os.environ
                  def add_argument(key, *args, **kwargs):
                      if key in env:
                          kwargs['default'] = env[key]
                      parser.add_argument(*args, **kwargs)
              
                  add_argument('--type', type=str)
              

              或者这个,使用os.getenv设置默认值:

              parser = argparse.ArgumentParser()
              parser.add_argument('--type', type=int, default=os.getenv('type',100))
              

              【讨论】:

                猜你喜欢
                • 2014-12-17
                • 2019-10-02
                • 2017-06-23
                • 2012-02-06
                • 1970-01-01
                • 2013-03-04
                • 2015-10-21
                • 2018-08-28
                • 1970-01-01
                相关资源
                最近更新 更多