Mercurial > hg > configuration
view configuration/config.py @ 31:5571d1608cba
add comments
author | Jeff Hammel <jhammel@mozilla.com> |
---|---|
date | Mon, 26 Mar 2012 17:06:18 -0700 |
parents | b27a7cb2dd5b |
children | 79aca36abd9a |
line wrap: on
line source
#!/usr/bin/env python """ multi-level unified configuration """ import sys import optparse # imports for contigent configuration providers try: import json except ImportError: try: import simplejson as json except ImportError: json = None try: import yaml except ImportError: yaml = None __all__ = ['Configuration', 'configuration_providers', 'types'] ### configuration providers for serialization/deserialization configuration_providers = [] class ConfigurationProvider(object): """ abstract base class for configuration providers for serialization/deserialization """ def read(self, filename): raise NotImplementedError("Abstract base class") def write(self, config, filename): if isinstance(filename, basestring): f = file(filename, 'w') newfile = True else: f = filename newfile = False try: self._write(f, config) finally: # XXX try: finally: works in python >= 2.5 if newfile: f.close() def _write(self, fp, config): raise NotImplementedError("Abstract base class") if json: class JSON(object): indent = 2 extensions = ['json'] def read(self, filename): return json.loads(file(filename).read()) def _write(self, fp, config): fp.write(json.dumps(config), indent=self.indent, sort_keys=True) # TODO: could use templates to get order down, etc configuration_providers.append(JSON()) if yaml: class YAML(object): extensions = ['yml', 'yaml'] def read(self, filename): f = file(filename) config = yaml.load(f) f.close() return config def _write(self, fp, config): fp.write(yaml.dump(config)) # TODO: could use templates to get order down, etc configuration_providers.append(YAML()) __all__.extend([i.__class__.__name__ for i in configuration_providers]) ### plugins for option types ### TODO: this could use a bit of thought ### They should probably be classes def base_cli(name, value): # CLI arguments args = value.get('flags', ['--%s' % name]) if not args: # No CLI interface return (), {} kw = {'dest': name} help = value.get('help', name) if 'default' in value: kw['default'] = value['default'] # TODO: use default pattern a la # - http://hg.mozilla.org/build/talos/file/c6013a2f09ce/talos/PerfConfigurator.py#l358 # - http://k0s.org/mozilla/hg/bzconsole/file/d5e88dadde69/bzconsole/command.py#l12 help += ' [DEFAULT: %s]' % value['default'] kw['help'] = help kw['action'] = 'store' return args, kw def bool_cli(name, value): # preserve the default values help = value.get('help') flags = value.get('flags') args, kw = base_cli(name, value) kw['help'] = help # reset if value.get('default'): kw['action'] = 'store_false' if not flags: args = ['--no-%s' % name] if not help: kw['help'] = 'disable %s' % name else: kw['action'] = 'store_true' if not help: kw['help'] = 'enable %s' % name return args, kw def list_cli(name, value): args, kw = base_cli(name, value) # TODO: could use 'extend' # - http://hg.mozilla.org/build/mozharness/file/5f44ba08f4be/mozharness/base/config.py#l41 # TODO: what about nested types? kw['action'] = 'append' return args, kw def int_cli(name, value): args, kw = base_cli(name, value) kw['type'] = 'int' return args, kw def float_cli(name, value): args, kw = base_cli(name, value) kw['type'] = 'float' return args, kw types = {bool: bool_cli, int: int_cli, float: float_cli, list: list_cli, None: base_cli} # default __all__ += [i.__name__ for i in types.values()] class Configuration(object): """declarative configuration object""" options = {} # configuration basis def __init__(self, configuration_providers=configuration_providers, types=types): self.config = {} self.configuration_providers = configuration_providers self.types = types ### methods for iteration ### TODO: make this a real iterator def items(self): # TODO: allow options to be a list of 2-tuples return self.options.items() ### methods for validating configuration def check(self, config): """ check validity of configuration to be added """ # TODO: should probably deepcopy config # ensure options in configuration are in self.options unknown_options = [i for i in config if i not in self.options] if unknown_options: # TODO: more specific error type raise Exception("Unknown options: %s" % ', '.join(unknown_options)) # TODO: ensure options are of the right type (if specified) for key, value in config.items(): _type = self.options[key].get('type') if _type is not None: config[key] = _type(value) return config def validate(self): """validate resultant configuration""" # TODO: configuration should be locked after this is called ### methods for adding configuration def __call__(self, *args): """add items to configuration and check it""" for config in args: self.add(config) self.validate() # validate total configuration # TODO: configuration should be locked after this is called def add(self, config, check=True): """update configuration: not undoable""" self.check(config) # check config to be added self.config.update(config) # TODO: option to extend; augment lists/dicts ### methods for optparse ### XXX could go in a subclass def optparse_options(self, parser): """add optparse options to a OptionParser instance""" for key, value in self.items(): handler = self.types[value.get('type')] args, kw = handler(key, value) if not args: # No CLI interface continue parser.add_option(*args, **kw) def parser(self, configuration_provider_option=None, **parser_args): """ return OptionParser for this Configuration instance - configuration_provider_options : option for configuration files [TODO] (also TODO: a special value that equates to the first file extension value for the configuration_providers) - parser_args : arguments to the OptionParser constructor """ if 'description' not in parser_args: parser_args['description'] = getattr(self, '__doc__', '') if 'formatter' not in parser_args: class PlainDescriptionFormatter(optparse.IndentedHelpFormatter): """description formatter for console script entry point""" def format_description(self, description): if description: return description.strip() + '\n' else: return '' parser_args['formatter'] = PlainDescriptionFormatter() parser = optparse.OptionParser(**parser_args) self.optparse_options(parser) return parser def parse(self, args=sys.argv[1:], parser=None, configuration_provider_option=None): """parse configuration including command line options""" # parse arguments if parser is None: parser = self.parser(configuration_provider_option=configuration_provider_option) options, args = parser.parse_args(args) # get CLI configuration options cli_config = dict([(key, value) for key, value in options.__dict__.items() if key in self.options]) # generate configuration self(cli_config) # return parsed arguments return options, args ### serialization/deserialization def configuration_provider(self, format): """configuration provider guess for a given filename""" for provider in self.configuration_providers: if format in provider.extensions: return provider def serialize(self, filename, format=None): """serialize configuration to a file""" # TODO: allow file object vs file name def deserialize(self, filename, format=None): """load configuration from a file""" # TODO: allow file object vs file name if not format: extension = os.path.splitext(filename)[-1]