# HG changeset patch # User Jeff Hammel # Date 1332904646 25200 # Node ID 81996be938bb51fc44fef6ca1250fed422a0a929 # Parent ef2d1c6211b9dcf6258cdacf88a69ae4af4d668e parity between module name and package name diff -r ef2d1c6211b9 -r 81996be938bb configuration/__init__.py --- a/configuration/__init__.py Tue Mar 27 17:04:31 2012 -0700 +++ b/configuration/__init__.py Tue Mar 27 20:17:26 2012 -0700 @@ -1,2 +1,2 @@ # -from config import * +from configuration import * diff -r ef2d1c6211b9 -r 81996be938bb configuration/config.py --- a/configuration/config.py Tue Mar 27 17:04:31 2012 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,467 +0,0 @@ -#!/usr/bin/env python - -""" -multi-level unified configuration -""" - -import copy -import os -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', 'MissingValueException', 'ConfigurationProviderException', 'TypeCastException', 'ConfigurationOption'] - -### exceptions - -class MissingValueException(Exception): - """exception raised when a required value is missing""" - -class ConfigurationProviderException(Exception): - """exception raised when a configuration provider is missing, etc""" - -class TypeCastException(Exception): - """exception raised when a configuration item cannot be coerced to a type""" - -### 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(ConfigurationProvider): - 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(ConfigurationProvider): - 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()) - -# TODO: add a configuration provider for taking command-line arguments -# from a file - -__all__.extend([i.__class__.__name__ for i in configuration_providers]) - -### optparse interface - -class ConfigurationOption(optparse.Option): - """option that keeps track if it is seen""" - # TODO: this should be configurable or something - def take_action(self, action, dest, opt, value, values, parser): - """add the parsed option to the set of things parsed""" - optparse.Option.take_action(self, action, dest, opt, value, values, parser) - if not hasattr(parser, 'parsed'): - parser.parsed = set() - parser.parsed.add(dest) - -### 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 - -# TODO: 'dict'-type cli interface - -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(optparse.OptionParser): - """declarative configuration object""" - - options = {} # configuration basis - - def __init__(self, configuration_providers=configuration_providers, types=types, **parser_args): - - # setup configuration - self.config = {} - self.configuration_providers = configuration_providers - self.types = types - - # setup optionparser - 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_args.setdefault('option_class', ConfigurationOption) - optparse.OptionParser.__init__(self, **parser_args) - self.parsed = set() - self.optparse_options(self) - - ### 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: - try: - config[key] = _type(value) - except BaseException, e: - raise TypeCastException("Could not coerce %s, %s, to type %s: %s" % (key, value, _type.__name__, e)) - - return config - - def validate(self): - """validate resultant configuration""" - for key, value in self.options.items(): - if key not in self.config: - required = value.get('required') - if required: - if isinstance(required, basestring): - required_message = required - else: - required_message = "Parameter %s is required but not present" % key - # TODO: more specific exception - # Also, this should probably raise all missing values vs - # one by one - raise MissingValueException(required_message) - # 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) - - # add defaults if not present - for key, value in self.options.items(): - if 'default' in value and key not in self.config: - self.config[key] = value['default'] - - # validate total configuration - self.validate() - # 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, dump='--dump', **parser_args): - """ - return OptionParser for this Configuration instance - - configuration_provider_options : option for configuration files - (or '-' for taking from the extensions names) - - dump : option for dumping configuration - - 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_args.setdefault('option_class', ConfigurationOption) - parser = optparse.OptionParser(**parser_args) - parser.parsed = set() - self.optparse_options(parser) - - # add option(s) for configuration_providers - if configuration_provider_option: - if configuration_provider_option == '-': - raise NotImplementedError(""" -The arguments will be interspersed. Will need to be more clever to get this -to work properly. -""") - - # take option from configuration_provider extensions - for format in self.formats(): - parser.add_option('--%s' % format, - dest='load_%s' % format, - action='append', - help="load configuration from a %s file" % format) - else: - parser.add_option(configuration_provider_option, - dest='load', action='append', - help="load configuration from a file") - - # add an option for dumping - formats = self.formats() - if formats and dump: - parser.add_option(dump, dest='dump', - help="dump configuration to a file; Formats: %s" % formats) - - return parser - - def parse(self, args=sys.argv[1:], parser=None, configuration_provider_option=None): - """ - parse configuration including command line options - - args: command line arguments to parse (default: system arguments) - - parser: a parser instance - - config_provider_option: option for configuration files; if None, - will be taken from remaining args - """ - - # 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]) - if hasattr(parser, 'parsed'): - # only use parsed arguments - # (though i'm not sure what to do with parser doesn't have the parsed attribute) - cli_config = dict([(key, value) for key, value in cli_config.items() - if key in parser.parsed]) - - # deserialize configuration - configuration_files = getattr(options, 'load', args) - missing = [i for i in configuration_files - if not os.path.exists(i)] - if missing: - parser.error("Missing files: %s" % ', '.join(missing)) - config = [] - for f in configuration_files: - try: - config.append(self.deserialize(f)) - except BaseException, e: - parser.error(str(e)) - config.append(cli_config) - - missingvalues = None - try: - # generate configuration - self(*config) - except MissingValueException, missingvalues: - pass - - # dump configuration, if specified - dump = getattr(options, 'dump') - if dump: - # TODO: have a way of specifying format other than filename - self.serialize(dump) - - if missingvalues and not dump: - # XXX assuming if you don't have values you were just dumping - raise missingvalues - - # return parsed arguments - return options, args - - ### serialization/deserialization - - def formats(self): - """formats for deserialization""" - retval = [] - for provider in self.configuration_providers: - if provider.extensions and hasattr(provider, 'write'): - retval.append(provider.extensions[0]) - return retval - - 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 filename2format(self, filename): - extension = os.path.splitext(filename)[-1] - return extension.lstrip('.') or None - - def serialize(self, filename, format=None, full=False): - """ - serialize configuration to a file - - filename: path of file to serialize to - - format: format of configuration provider - - full: whether to serialize non-set optional strings [TODO] - """ - # TODO: allow file object vs file name - - if not format: - format = self.filename2format(filename) - if not format: - raise Exception('Please specify a format') - # TODO: more specific exception type - - provider = self.configuration_provider(format) - if not provider: - raise Exception("Provider not found for format: %s" % format) - - config = copy.deepcopy(self.config) - - provider.write(config, filename) - - def deserialize(self, filename, format=None): - """load configuration from a file""" - # TODO: allow file object vs file name - - assert os.path.exists(filename) - - # get the format - if not format: - format = self.filename2format(filename) - - # get the providers in some sensible order - providers = self.configuration_providers[:] - if format: - providers.sort(key=lambda x: int(format in x.extensions), reverse=True) - - # deserialize the data - for provider in providers: - try: - return provider.read(filename) - except: - continue - else: - raise ConfigurationProviderException("Could not load %s" % filename) diff -r ef2d1c6211b9 -r 81996be938bb configuration/configuration.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/configuration/configuration.py Tue Mar 27 20:17:26 2012 -0700 @@ -0,0 +1,467 @@ +#!/usr/bin/env python + +""" +multi-level unified configuration +""" + +import copy +import os +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', 'MissingValueException', 'ConfigurationProviderException', 'TypeCastException', 'ConfigurationOption'] + +### exceptions + +class MissingValueException(Exception): + """exception raised when a required value is missing""" + +class ConfigurationProviderException(Exception): + """exception raised when a configuration provider is missing, etc""" + +class TypeCastException(Exception): + """exception raised when a configuration item cannot be coerced to a type""" + +### 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(ConfigurationProvider): + 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(ConfigurationProvider): + 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()) + +# TODO: add a configuration provider for taking command-line arguments +# from a file + +__all__.extend([i.__class__.__name__ for i in configuration_providers]) + +### optparse interface + +class ConfigurationOption(optparse.Option): + """option that keeps track if it is seen""" + # TODO: this should be configurable or something + def take_action(self, action, dest, opt, value, values, parser): + """add the parsed option to the set of things parsed""" + optparse.Option.take_action(self, action, dest, opt, value, values, parser) + if not hasattr(parser, 'parsed'): + parser.parsed = set() + parser.parsed.add(dest) + +### 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 + +# TODO: 'dict'-type cli interface + +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(optparse.OptionParser): + """declarative configuration object""" + + options = {} # configuration basis + + def __init__(self, configuration_providers=configuration_providers, types=types, **parser_args): + + # setup configuration + self.config = {} + self.configuration_providers = configuration_providers + self.types = types + + # setup optionparser + 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_args.setdefault('option_class', ConfigurationOption) + optparse.OptionParser.__init__(self, **parser_args) + self.parsed = set() + self.optparse_options(self) + + ### 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: + try: + config[key] = _type(value) + except BaseException, e: + raise TypeCastException("Could not coerce %s, %s, to type %s: %s" % (key, value, _type.__name__, e)) + + return config + + def validate(self): + """validate resultant configuration""" + for key, value in self.options.items(): + if key not in self.config: + required = value.get('required') + if required: + if isinstance(required, basestring): + required_message = required + else: + required_message = "Parameter %s is required but not present" % key + # TODO: more specific exception + # Also, this should probably raise all missing values vs + # one by one + raise MissingValueException(required_message) + # 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) + + # add defaults if not present + for key, value in self.options.items(): + if 'default' in value and key not in self.config: + self.config[key] = value['default'] + + # validate total configuration + self.validate() + # 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, dump='--dump', **parser_args): + """ + return OptionParser for this Configuration instance + - configuration_provider_options : option for configuration files + (or '-' for taking from the extensions names) + - dump : option for dumping configuration + - 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_args.setdefault('option_class', ConfigurationOption) + parser = optparse.OptionParser(**parser_args) + parser.parsed = set() + self.optparse_options(parser) + + # add option(s) for configuration_providers + if configuration_provider_option: + if configuration_provider_option == '-': + raise NotImplementedError(""" +The arguments will be interspersed. Will need to be more clever to get this +to work properly. +""") + + # take option from configuration_provider extensions + for format in self.formats(): + parser.add_option('--%s' % format, + dest='load_%s' % format, + action='append', + help="load configuration from a %s file" % format) + else: + parser.add_option(configuration_provider_option, + dest='load', action='append', + help="load configuration from a file") + + # add an option for dumping + formats = self.formats() + if formats and dump: + parser.add_option(dump, dest='dump', + help="dump configuration to a file; Formats: %s" % formats) + + return parser + + def parse(self, args=sys.argv[1:], parser=None, configuration_provider_option=None): + """ + parse configuration including command line options + - args: command line arguments to parse (default: system arguments) + - parser: a parser instance + - config_provider_option: option for configuration files; if None, + will be taken from remaining args + """ + + # 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]) + if hasattr(parser, 'parsed'): + # only use parsed arguments + # (though i'm not sure what to do with parser doesn't have the parsed attribute) + cli_config = dict([(key, value) for key, value in cli_config.items() + if key in parser.parsed]) + + # deserialize configuration + configuration_files = getattr(options, 'load', args) + missing = [i for i in configuration_files + if not os.path.exists(i)] + if missing: + parser.error("Missing files: %s" % ', '.join(missing)) + config = [] + for f in configuration_files: + try: + config.append(self.deserialize(f)) + except BaseException, e: + parser.error(str(e)) + config.append(cli_config) + + missingvalues = None + try: + # generate configuration + self(*config) + except MissingValueException, missingvalues: + pass + + # dump configuration, if specified + dump = getattr(options, 'dump') + if dump: + # TODO: have a way of specifying format other than filename + self.serialize(dump) + + if missingvalues and not dump: + # XXX assuming if you don't have values you were just dumping + raise missingvalues + + # return parsed arguments + return options, args + + ### serialization/deserialization + + def formats(self): + """formats for deserialization""" + retval = [] + for provider in self.configuration_providers: + if provider.extensions and hasattr(provider, 'write'): + retval.append(provider.extensions[0]) + return retval + + 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 filename2format(self, filename): + extension = os.path.splitext(filename)[-1] + return extension.lstrip('.') or None + + def serialize(self, filename, format=None, full=False): + """ + serialize configuration to a file + - filename: path of file to serialize to + - format: format of configuration provider + - full: whether to serialize non-set optional strings [TODO] + """ + # TODO: allow file object vs file name + + if not format: + format = self.filename2format(filename) + if not format: + raise Exception('Please specify a format') + # TODO: more specific exception type + + provider = self.configuration_provider(format) + if not provider: + raise Exception("Provider not found for format: %s" % format) + + config = copy.deepcopy(self.config) + + provider.write(config, filename) + + def deserialize(self, filename, format=None): + """load configuration from a file""" + # TODO: allow file object vs file name + + assert os.path.exists(filename) + + # get the format + if not format: + format = self.filename2format(filename) + + # get the providers in some sensible order + providers = self.configuration_providers[:] + if format: + providers.sort(key=lambda x: int(format in x.extensions), reverse=True) + + # deserialize the data + for provider in providers: + try: + return provider.read(filename) + except: + continue + else: + raise ConfigurationProviderException("Could not load %s" % filename)