view pyloader/factory.py @ 65:6bb431e41e0b

stub getting the override options
author Jeff Hammel <jhammel@mozilla.com>
date Thu, 09 Jun 2011 09:23:47 -0700
parents 995b831041be
children 2a9274608af3
line wrap: on
line source

#!/usr/bin/env python

"""
abstract factories
"""

import cast
import loader
import os
import sys
from optparse import OptionParser
from ConfigParser import InterpolationDepthError
from ConfigParser import InterpolationMissingOptionError
from ConfigParser import InterpolationSyntaxError
from ConfigParser import SafeConfigParser as ConfigParser

__all__ = ['CircularReferenceError', 'PyFactory', 'IniFactory']

class CircularReferenceError(Exception):
    """factory has detected a circular reference"""

class PyFactory(object):

    delimeters = ('%(', ')s')

    def __init__(self, config=None, main=''):
        self.main = main  # main section
        self.configure(config or {})

    def configure(self, config):
        """load a new configuration"""
        # TODO: this should really be a configuration update.  If you keep
        # track of all "apps" and their parents (i.e. as a ADG)
        # you should be able to update only relevent apps
        self.config = config
        self.seen = set() # already seen apps to note cyclic dependencies
        self.parsed = {}  # instantiated apps

    def load(self, name=None):
        """load an object"""
        
        name = name or self.main # load main section by default
        assert name in self.config, "'%s' not found in configuration"
        if name in self.parsed:
            return self.parsed[name]
        if name in self.seen:
            raise CircularReferenceError('Circular reference! : %s' % name)
        self.seen.add(name)

        # get section
        section = self.config[name]
        assert 'path' in section

        # load object
        obj = loader.load(section['path'])

        # get the object's arguments (if any)
        args = section.get('args', None)
        kwargs = section.get('kwargs', None)

        # if args and kwargs aren't there, you're done!
        if args is None and kwargs is None:
            self.parsed[name] = obj
            return obj

        # interpolate arguments
        if args:
            args = [self.interpolate(arg) for arg in args]
        else:
            args = []
        if kwargs:
            kwargs = dict([(key, self.interpolate(value))
                           for key, value in kwargs.items()])
        else:
            kwargs = {}

        # invoke
        self.parsed[name] = obj(*args, **kwargs)
        return self.parsed[name]

    def interpolate(self, value):

        # only interpolate strings
        if not isinstance(value, basestring):
            return value

        if value.startswith(self.delimeters[0]) and value.endswith(self.delimeters[1]):
            value = value[len(self.delimeters[0]):-len(self.delimeters[1])]
            if value in self.config:
                return self.load(value)
        return value
                                                            
class IniFactory(PyFactory):
    """load a python object from an .ini file"""

    def __init__(self, inifile, main=''):
        assert os.path.exists(inifile), "File not found: %s" % inifile
        self.inifile = inifile
        config = self.read(inifile)
        PyFactory.__init__(self, config, main)

    @classmethod
    def configuration(cls, iniconfig, **defaults):
        """interpret configuration from raw .ini syntax"""
        config = {}
        interpolated = set()
        seen = set()
        object_string = '%(object)s'

        # create a hash of section names
        names = {}
        for section in iniconfig:

            # sanity check
            assert ':' in section, "No : in section: %s" % section

            name = section.split(':',1)[0]
            names[name] = section

        def create_section(section, options):

            # split up the section identifier
            name, path = section.split(':', 1)

            # interpret decorators
            if ':' in path:
                wrapper, _path = path.split(':', 1)
                # TODO: could interpolate wrapper 
                if wrapper in names:

                    # TODO: wrapper arguments:
                    # [extended-fibonacci:@:four=4,five=5:fibonacci]

                    # TODO: will be correct for
                    # [foo:bar:%(here)s/objects.py:MyClass]
                    # but not for
                    # [foo:bar:fleem]
                    # in the latter case, just use fleem
                    # but check for cyclic consistency
                    if _path in names:
                        wrapped_name = _path
                    else:
                        wrapped_name = section

                    # get wrapper options
                    if wrapper not in config:
                        # load wrapper configuration 
                        wrapper_section = names[wrapper]
                        if wrapper_section in seen:
                            pass # TODO
                        create_section(wrapper_section, iniconfig[wrapper_section])
                    wrapper_options = config[wrapper].copy()

                    # interpolate wrapper_options
                    def interpolate(option):
                        if option == object_string:
                            return '%(' + wrapped_name + ')s'
                        return option
                    if 'args' in wrapper_options:
                        args = wrapper_options['args'][:]
                        args = [interpolate(i) for i in args]
                        wrapper_options['args'] = args
                    if 'kwargs' in wrapper_options:
                        kwargs = wrapper_options['kwargs'].copy()
                        kwargs = dict([(i,interpolate(j)) for i, j in kwargs.items()])
                        wrapper_options['kwargs'] = kwargs

                    # create wrapper
                    config[name] = wrapper_options
                    if _path == wrapped_name:
                        return
                    name = wrapped_name
                    path = _path

            elif path in names:
                # override section: [foo:bar]
                if path not in config:
                    # load overridden section
                    overridden_section = names[path]
                    if overridden_section in seen:
                        pass # TODO
                    create_section(overridden_section, iniconfig[overridden_section])
                # TODO: options = config[path].copy()

            # make a dict for the section
            path = path % defaults
            sect = config[name] = dict(path=path)

            # get arguments from .ini options
            for option, value in options.items():
                
                if option == '.': # positional arguments
                    sect['args'] = cast.str2list(value)
                else:
                    sect.setdefault('kwargs', {})[option] = value

        # get the object definitions
        for section, options in iniconfig.items():
            seen.add(section)
            if section not in interpolated:
                create_section(section, options)
            interpolated.add(section)
                    
        return config
        
    @classmethod
    def read(cls, inifile):
        """reads configuration from an .ini file"""

        here = os.path.dirname(os.path.abspath(inifile))
        
        # read configuration
        defaults={'here': here,
                  'this': os.path.abspath(inifile)}
        parser = ConfigParser(defaults=defaults)
        parser.optionxform = str # use whole case
        parser.read(inifile)

        # parse configuration
        config = {}
        for section in parser.sections():

            config[section] = {}

            # read the options
            for option in parser.options(section):

                if option in parser.defaults():
                    # don't include the defaults
                    continue

                # try to interpolate the option
                # otherwise, use the raw value
                try:
                    value = parser.get(section, option)
                except (InterpolationMissingOptionError, InterpolationSyntaxError, InterpolationDepthError):
                    value = parser.get(section, option, raw=True)

                config[section][option] = value

        return cls.configuration(config, **parser.defaults())

def main(args=sys.argv[1:]):
    """command line entry point"""
    usage = '%prog file1.ini -arg1 -arg2 --key1=value1 --key2=value2'
    parser = OptionParser(usage=usage, description=IniFactory.__doc__)
    options, args = parser.parse_args(args)

    if len(args) != 1:
        parser.print_usage()
        parser.exit()

    factory = IniFactory(args[0])
    obj = factory.load()
    print obj

if __name__ == '__main__':
    main()