view configuration/config.py @ 28:c516ab813079

begin stubbing serialization/deserialization
author Jeff Hammel <jhammel@mozilla.com>
date Mon, 26 Mar 2012 16:54:37 -0700
parents c6d966431498
children fadcc6ab51d4
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']
        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)

### plugins for option types
### TODO: this could use a bit of thought
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):
    options = {}

    def __init__(self, configuration_providers=configuration_providers, types=types):
        self.config = {}
        self.configuration_providers = configuration_providers
        self.types = types

    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"""

    def deserialize(self, filename, format=None):
        """load configuration from a file"""

        extension = os.path.splitext(filename)[-1]
        if not extension:
            return None