parsingconfiguration文件,环境和命令行参数,以获得单个选项集合

Python的标准库有用于configuration文件parsing ( configparser ), 环境variables读取 ( os.environ )和命令行参数parsing ( argparse )的模块。 我想编写一个能够完成所有这些工作的程序,还有:

  • 有一个选项值级联

    • 默认选项值,被覆盖
    • configuration文件选项,覆盖
    • 环境variables,被重写
    • 命令行选项。
  • 允许在命令行上使用例如--config-file foo.conf 指定一个或多个configuration文件位置 ,并读取该configuration文件位置 (而不是通常的configuration文件)。 这仍然必须服从上述级联。

  • 在一个地方允许选项定义来确定configuration文件和命令行的parsing行为。

  • 将parsing的选项统一为一个选项值集合,以供程序的其余部分访问,而不必关心它们来自哪里。

我需要的东西显然是在Python标准库中,但是它们不能一起工作。

我怎样才能达到这个与Python标准库的最小偏差?

argparse模块使得这不是什么疯狂的事情,只要你对一个看起来像命令行的configuration文件感到满意。 (我认为这是一个优势,因为用户只需要学习一种语法。)从file_prefix_chars设置为例如@ ,使得它,

 my_prog --foo=bar 

相当于

 my_prog @baz.conf 

如果@baz.conf是,

 --foo bar 

你甚至可以通过修改argv自动查找foo.conf

 if os.path.exists('foo.conf'): argv = ['@foo.conf'] + argv args = argparser.parse_args(argv) 

这些configuration文件的格式可以通过创build一个ArgumentParser的子类并添加一个convert_arg_line_to_args方法来修改。

这里有一些我一起入侵的东西。 在评论中随意提出改进/错误报告:

 import argparse import ConfigParser import os def _identity(x): return x _SENTINEL = object() class AddConfigFile(argparse.Action): def __call__(self,parser,namespace,values,option_string=None): # I can never remember if `values` is a list all the time or if it # can be a scalar string; this takes care of both. if isinstance(values,basestring): parser.config_files.append(values) else: parser.config_files.extend(values) class ArgumentConfigEnvParser(argparse.ArgumentParser): def __init__(self,*args,**kwargs): """ Added 2 new keyword arguments to the ArgumentParser constructor: config --> List of filenames to parse for config goodness default_section --> name of the default section in the config file """ self.config_files = kwargs.pop('config',[]) #Must be a list self.default_section = kwargs.pop('default_section','MAIN') self._action_defaults = {} argparse.ArgumentParser.__init__(self,*args,**kwargs) def add_argument(self,*args,**kwargs): """ Works like `ArgumentParser.add_argument`, except that we've added an action: config: add a config file to the parser This also adds the ability to specify which section of the config file to pull the data from, via the `section` keyword. This relies on the (undocumented) fact that `ArgumentParser.add_argument` actually returns the `Action` object that it creates. We need this to reliably get `dest` (although we could probably write a simple function to do this for us). """ if 'action' in kwargs and kwargs['action'] == 'config': kwargs['action'] = AddConfigFile kwargs['default'] = argparse.SUPPRESS # argparse won't know what to do with the section, so # we'll pop it out and add it back in later. # # We also have to prevent argparse from doing any type conversion, # which is done explicitly in parse_known_args. # # This way, we can reliably check whether argparse has replaced the default. # section = kwargs.pop('section', self.default_section) type = kwargs.pop('type', _identity) default = kwargs.pop('default', _SENTINEL) if default is not argparse.SUPPRESS: kwargs.update(default=_SENTINEL) else: kwargs.update(default=argparse.SUPPRESS) action = argparse.ArgumentParser.add_argument(self,*args,**kwargs) kwargs.update(section=section, type=type, default=default) self._action_defaults[action.dest] = (args,kwargs) return action def parse_known_args(self,args=None, namespace=None): # `parse_args` calls `parse_known_args`, so we should be okay with this... ns, argv = argparse.ArgumentParser.parse_known_args(self, args=args, namespace=namespace) config_parser = ConfigParser.SafeConfigParser() config_files = [os.path.expanduser(os.path.expandvars(x)) for x in self.config_files] config_parser.read(config_files) for dest,(args,init_dict) in self._action_defaults.items(): type_converter = init_dict['type'] default = init_dict['default'] obj = default if getattr(ns,dest,_SENTINEL) is not _SENTINEL: # found on command line obj = getattr(ns,dest) else: # not found on commandline try: # get from config file obj = config_parser.get(init_dict['section'],dest) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): # Nope, not in config file try: # get from environment obj = os.environ[dest.upper()] except KeyError: pass if obj is _SENTINEL: setattr(ns,dest,None) elif obj is argparse.SUPPRESS: pass else: setattr(ns,dest,type_converter(obj)) return ns, argv if __name__ == '__main__': fake_config = """ [MAIN] foo:bar bar:1 """ with open('_config.file','w') as fout: fout.write(fake_config) parser = ArgumentConfigEnvParser() parser.add_argument('--config-file', action='config', help="location of config file") parser.add_argument('--foo', type=str, action='store', default="grape", help="don't know what foo does ...") parser.add_argument('--bar', type=int, default=7, action='store', help="This is an integer (I hope)") parser.add_argument('--baz', type=float, action='store', help="This is an float(I hope)") parser.add_argument('--qux', type=int, default='6', action='store', help="this is another int") ns = parser.parse_args([]) parser_defaults = {'foo':"grape",'bar':7,'baz':None,'qux':6} config_defaults = {'foo':'bar','bar':1} env_defaults = {"baz":3.14159} # This should be the defaults we gave the parser print ns assert ns.__dict__ == parser_defaults # This should be the defaults we gave the parser + config defaults d = parser_defaults.copy() d.update(config_defaults) ns = parser.parse_args(['--config-file','_config.file']) print ns assert ns.__dict__ == d os.environ['BAZ'] = "3.14159" # This should be the parser defaults + config defaults + env_defaults d = parser_defaults.copy() d.update(config_defaults) d.update(env_defaults) ns = parser.parse_args(['--config-file','_config.file']) print ns assert ns.__dict__ == d # This should be the parser defaults + config defaults + env_defaults + commandline commandline = {'foo':'3','qux':4} d = parser_defaults.copy() d.update(config_defaults) d.update(env_defaults) d.update(commandline) ns = parser.parse_args(['--config-file','_config.file','--foo=3','--qux=4']) print ns assert ns.__dict__ == d os.remove('_config.file') 

去做

这个实现还不完整。 这是一个部分TODO列表:

  • (简单)与parsing器默认值进行交互
  • (简单)如果types转换不起作用,请检查argparse如何处理错误消息

符合logging的行为

  • (简单)写一个从add_argument args算出dest的函数,而不是依赖Action对象
  • (微不足道)编写一个使用parse_known_argsparse_args函数。 (例如从cpython实现复制parse_args以保证它调用parse_known_args 。)

较不容易的东西…

我还没有尝试过。 这是不可能的,但仍然有可能! – 它可以工作…

  • (难吗?) 相互排斥
  • (硬?) 参数组 (如果实施,这些组应该在configuration文件中获得一个section 。)
  • (硬?) 子命令 (子命令也应该在configuration文件中获得一个section 。)

有一个叫做configglue的库。

configglue是一个将python的optparse.OptionParser和ConfigParser.ConfigParser粘合在一起的库,所以当你想将相同的选项导出到configuration文件和命令行界面时,你不必重复自己。

它也支持环境variables。

另外还有一个名为ConfigArgParse的库

argparse的替代品,允许通过configuration文件和/或环境variables来设置选项。

您可能对PyCon谈论ŁukaszLanga的configuration感兴趣 – 让他们configuration!

看起来标准库并没有解决这个问题,所以每个程序员都用笨拙的方式凑齐configparserargparseos.environ

虽然我还没有尝试过,但有ConfigArgParse库,它说明它可以完成大部分你想要的事情:

argparse的替代品,允许通过configuration文件和/或环境variables来设置选项。

据我所知,Python标准库不提供这个。 我通过编写代码来使用optparseConfigParserparsing命令行和configuration文件,并在其上提供一个抽象层来解决这个问题。 但是,您需要将此作为单独的依赖项,从您以前的评论中看来似乎是不合适的。

如果你想看看我写的代码,可以在http://liw.fi/cliapp/find 。 它被集成到我的“命令行应用程序框架”库中,因为这是框架需要做的很大一部分。

为了达到所有这些要求,我build议编写自己的库,使用[opt | arg] parse和configparser作为基础function。

鉴于前两个和最后一个要求,我会说你想要:

第一步:执行一个命令行parsing器,只查找–config-file选项。

第二步:parsingconfiguration文件。

第三步:使用configuration文件pass的输出作为默认设置第二个命令行parsing器传递。

第三个要求可能意味着你必须devise你自己的选项定义系统来公开你所关心的optparse和configparser的所有function,并且写一些pipe道来做转换。

我最近尝试了这样的一些东西,使用“optparse”。

我将其设置为OptonParser的子类,并带有“–Store”和“–Check”命令。

下面的代码几乎涵盖了你。 你只需要定义你自己的“加载”和“存储”的方法,接受/返回字典,你是猎物很多集。

 class SmartParse(optparse.OptionParser): def __init__(self,defaults,*args,**kwargs): self.smartDefaults=defaults optparse.OptionParser.__init__(self,*args,**kwargs) fileGroup = optparse.OptionGroup(self,'handle stored defaults') fileGroup.add_option( '-S','--Store', dest='Action', action='store_const',const='Store', help='store command line settings' ) fileGroup.add_option( '-C','--Check', dest='Action', action='store_const',const='Check', help ='check stored settings' ) self.add_option_group(fileGroup) def parse_args(self,*args,**kwargs): (options,arguments) = optparse.OptionParser.parse_args(self,*args,**kwargs) action = options.__dict__.pop('Action') if action == 'Check': assert all( value is None for (key,value) in options.__dict__.iteritems() ) print 'defaults:',self.smartDefaults print 'config:',self.load() sys.exit() elif action == 'Store': self.store(options.__dict__) sys.exit() else: config=self.load() commandline=dict( [key,val] for (key,val) in options.__dict__.iteritems() if val is not None ) result = {} result.update(self.defaults) result.update(config) result.update(commandline) return result,arguments def load(self): return {} def store(self,optionDict): print 'Storing:',optionDict 

这里是一个我一起攻击的模块,它可以读取命令行参数,环境设置,ini文件和keyring值。 这也是一个要点 。

 """ Configuration Parser Configurable parser that will parse config files, environment variables, keyring, and command-line arguments. Example test.ini file: [defaults] gini=10 [app] xini = 50 Example test.arg file: --xfarg=30 Example test.py file: import os import sys import config def main(argv): '''Test.''' options = [ config.Option("xpos", help="positional argument", nargs='?', default="all", env="APP_XPOS"), config.Option("--xarg", help="optional argument", default=1, type=int, env="APP_XARG"), config.Option("--xenv", help="environment argument", default=1, type=int, env="APP_XENV"), config.Option("--xfarg", help="@file argument", default=1, type=int, env="APP_XFARG"), config.Option("--xini", help="ini argument", default=1, type=int, ini_section="app", env="APP_XINI"), config.Option("--gini", help="global ini argument", default=1, type=int, env="APP_GINI"), config.Option("--karg", help="secret keyring arg", default=-1, type=int), ] ini_file_paths = [ '/etc/default/app.ini', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test.ini') ] # default usage conf = config.Config(prog='app', options=options, ini_paths=ini_file_paths) conf.parse() print conf # advanced usage cli_args = conf.parse_cli(argv=argv) env = conf.parse_env() secrets = conf.parse_keyring(namespace="app") ini = conf.parse_ini(ini_file_paths) sources = {} if ini: for key, value in ini.iteritems(): conf[key] = value sources[key] = "ini-file" if secrets: for key, value in secrets.iteritems(): conf[key] = value sources[key] = "keyring" if env: for key, value in env.iteritems(): conf[key] = value sources[key] = "environment" if cli_args: for key, value in cli_args.iteritems(): conf[key] = value sources[key] = "command-line" print '\n'.join(['%s:\t%s' % (k, v) for k, v in sources.items()]) if __name__ == "__main__": if config.keyring: config.keyring.set_password("app", "karg", "13") main(sys.argv) Example results: $APP_XENV=10 python test.py api --xarg=2 @test.arg <Config xpos=api, gini=1, xenv=10, xini=50, karg=13, xarg=2, xfarg=30> xpos: command-line xenv: environment xini: ini-file karg: keyring xarg: command-line xfarg: command-line """ import argparse import ConfigParser import copy import os import sys try: import keyring except ImportError: keyring = None class Option(object): """Holds a configuration option and the names and locations for it. Instantiate options using the same arguments as you would for an add_arguments call in argparse. However, you have two additional kwargs available: env: the name of the environment variable to use for this option ini_section: the ini file section to look this value up from """ def __init__(self, *args, **kwargs): self.args = args or [] self.kwargs = kwargs or {} def add_argument(self, parser, **override_kwargs): """Add an option to a an argparse parser.""" kwargs = {} if self.kwargs: kwargs = copy.copy(self.kwargs) try: del kwargs['env'] except KeyError: pass try: del kwargs['ini_section'] except KeyError: pass kwargs.update(override_kwargs) parser.add_argument(*self.args, **kwargs) @property def type(self): """The type of the option. Should be a callable to parse options. """ return self.kwargs.get("type", str) @property def name(self): """The name of the option as determined from the args.""" for arg in self.args: if arg.startswith("--"): return arg[2:].replace("-", "_") elif arg.startswith("-"): continue else: return arg.replace("-", "_") @property def default(self): """The default for the option.""" return self.kwargs.get("default") class Config(object): """Parses configuration sources.""" def __init__(self, options=None, ini_paths=None, **parser_kwargs): """Initialize with list of options. :param ini_paths: optional paths to ini files to look up values from :param parser_kwargs: kwargs used to init argparse parsers. """ self._parser_kwargs = parser_kwargs or {} self._ini_paths = ini_paths or [] self._options = copy.copy(options) or [] self._values = {option.name: option.default for option in self._options} self._parser = argparse.ArgumentParser(**parser_kwargs) self.pass_thru_args = [] @property def prog(self): """Program name.""" return self._parser.prog def __getitem__(self, key): return self._values[key] def __setitem__(self, key, value): self._values[key] = value def __delitem__(self, key): del self._values[key] def __contains__(self, key): return key in self._values def __iter__(self): return iter(self._values) def __len__(self): return len(self._values) def get(self, key, *args): """ Return the value for key if it exists otherwise the default. """ return self._values.get(key, *args) def __getattr__(self, attr): if attr in self._values: return self._values[attr] else: raise AttributeError("'config' object has no attribute '%s'" % attr) def build_parser(self, options, **override_kwargs): """.""" kwargs = copy.copy(self._parser_kwargs) kwargs.update(override_kwargs) if 'fromfile_prefix_chars' not in kwargs: kwargs['fromfile_prefix_chars'] = '@' parser = argparse.ArgumentParser(**kwargs) if options: for option in options: option.add_argument(parser) return parser def parse_cli(self, argv=None): """Parse command-line arguments into values.""" if not argv: argv = sys.argv options = [] for option in self._options: temp = Option(*option.args, **option.kwargs) temp.kwargs['default'] = argparse.SUPPRESS options.append(temp) parser = self.build_parser(options=options) parsed, extras = parser.parse_known_args(argv[1:]) if extras: valid, pass_thru = self.parse_passthru_args(argv[1:]) parsed, extras = parser.parse_known_args(valid) if extras: raise AttributeError("Unrecognized arguments: %s" % ' ,'.join(extras)) self.pass_thru_args = pass_thru + extras return vars(parsed) def parse_env(self): results = {} for option in self._options: env_var = option.kwargs.get('env') if env_var and env_var in os.environ: value = os.environ[env_var] results[option.name] = option.type(value) return results def get_defaults(self): """Use argparse to determine and return dict of defaults.""" parser = self.build_parser(options=self._options) parsed, _ = parser.parse_known_args([]) return vars(parsed) def parse_ini(self, paths=None): """Parse config files and return configuration options. Expects array of files that are in ini format. :param paths: list of paths to files to parse (uses ConfigParse logic). If not supplied, uses the ini_paths value supplied on initialization. """ results = {} config = ConfigParser.SafeConfigParser() config.read(paths or self._ini_paths) for option in self._options: ini_section = option.kwargs.get('ini_section') if ini_section: try: value = config.get(ini_section, option.name) results[option.name] = option.type(value) except ConfigParser.NoSectionError: pass return results def parse_keyring(self, namespace=None): """.""" results = {} if not keyring: return results if not namespace: namespace = self.prog for option in self._options: secret = keyring.get_password(namespace, option.name) if secret: results[option.name] = option.type(secret) return results def parse(self, argv=None): """.""" defaults = self.get_defaults() args = self.parse_cli(argv=argv) env = self.parse_env() secrets = self.parse_keyring() ini = self.parse_ini() results = defaults results.update(ini) results.update(secrets) results.update(env) results.update(args) self._values = results return self @staticmethod def parse_passthru_args(argv): """Handles arguments to be passed thru to a subprocess using '--'. :returns: tuple of two lists; args and pass-thru-args """ if '--' in argv: dashdash = argv.index("--") if dashdash == 0: return argv[1:], [] elif dashdash > 0: return argv[0:dashdash], argv[dashdash + 1:] return argv, [] def __repr__(self): return "<Config %s>" % ', '.join([ '%s=%s' % (k, v) for k, v in self._values.iteritems()]) def comma_separated_strings(value): """Handles comma-separated arguments passed in command-line.""" return map(str, value.split(",")) def comma_separated_pairs(value): """Handles comma-separated key/values passed in command-line.""" pairs = value.split(",") results = {} for pair in pairs: key, pair_value = pair.split('=') results[key] = pair_value return results