pyloader
view pyloader/factory.py @ 81:9203ca3a5182
comment
| author | Jeff Hammel <jhammel@mozilla.com> |
|---|---|
| date | Sat Jun 25 11:05:06 2011 -0700 (10 months ago) |
| parents | 20bdb8125817 |
| children |
line source
1 #!/usr/bin/env python
3 """
4 abstract factories
5 """
7 import cast
8 import loader
9 import os
10 import sys
11 from copy import deepcopy
12 from optparse import OptionParser
13 from ConfigParser import InterpolationDepthError
14 from ConfigParser import InterpolationMissingOptionError
15 from ConfigParser import InterpolationSyntaxError
16 from ConfigParser import SafeConfigParser as ConfigParser
18 __all__ = ['CircularReferenceError', 'PyFactory', 'IniFactory']
20 class CircularReferenceError(Exception):
21 """factory has detected a circular reference"""
23 class PyFactory(object):
25 # to evaluate arguments as objects
26 delimeters = ('%(', ')s')
28 def __init__(self, config=None, main=''):
29 self.main = main # main section
30 self.configure(config or {})
32 def configure(self, config):
33 """load a new configuration"""
34 # TODO: this should really be a configuration update. If you keep
35 # track of all "apps" and their parents (i.e. as a ADG)
36 # you should be able to update only relevent apps
37 self.config = config
38 self.seen = set() # already seen apps to note cyclic dependencies
39 self.parsed = {} # instantiated apps
41 def load(self, name=None):
42 """load an object"""
44 name = name or self.main # load main section by default
45 assert name in self.config, "'%s' not found in configuration"
46 if name in self.parsed:
47 return self.parsed[name]
48 if name in self.seen:
49 raise CircularReferenceError('Circular reference! : %s' % name)
50 self.seen.add(name)
52 # get section
53 section = self.config[name]
54 assert 'path' in section
56 # load object
57 obj = loader.load(section['path'])
59 # get the object's arguments (if any)
60 args = section.get('args', None)
61 kwargs = section.get('kwargs', None)
63 # if args and kwargs aren't there, you're done!
64 if args is None and kwargs is None:
65 self.parsed[name] = obj
66 return obj
68 # interpolate arguments
69 if args:
70 args = [self.interpolate(arg) for arg in args]
71 else:
72 args = []
73 if kwargs:
74 kwargs = dict([(key, self.interpolate(value))
75 for key, value in kwargs.items()])
76 else:
77 kwargs = {}
79 # invoke
80 self.parsed[name] = obj(*args, **kwargs)
81 return self.parsed[name]
83 def interpolate(self, value):
85 # only interpolate strings
86 if not isinstance(value, basestring):
87 return value
89 if value.startswith(self.delimeters[0]) and value.endswith(self.delimeters[1]):
90 value = value[len(self.delimeters[0]):-len(self.delimeters[1])]
91 if value in self.config:
92 return self.load(value)
93 return value
95 class IniFactory(PyFactory):
96 """load a python object from an .ini file"""
98 def __init__(self, inifile, main=''):
99 assert os.path.exists(inifile), "File not found: %s" % inifile
100 self.inifile = inifile
101 config = self.read(inifile)
102 PyFactory.__init__(self, config, main)
104 @classmethod
105 def configuration(cls, iniconfig, **defaults):
106 """interpret configuration from raw .ini syntax"""
107 config = {}
108 interpolated = set()
109 seen = set()
110 object_string = '%(object)s'
112 # create a hash of section names
113 names = {}
114 for section in iniconfig:
116 # sanity check
117 assert ':' in section, "No : in section: %s" % section
119 name = section.split(':',1)[0]
120 names[name] = section
122 def create_section(section, options):
124 # split up the section identifier
125 name, path = section.split(':', 1)
127 # make a dict for the section
128 sect = {}
130 # interpret decorators
131 if ':' in path:
132 wrapper, _path = path.split(':', 1)
133 # TODO: could interpolate wrapper
134 if wrapper in names:
136 # inline wrapper arguments:
137 # [extended-fibonacci:@:four=4,five=5:fibonacci]
138 _wrapper_args = None
139 _wrapper_kwargs = None
140 if ':' in _path:
141 _wrapper_options, __path = _path.split(':', 1)
142 if ',' in _wrapper_options or '=' in _wrapper_options:
143 # ,= : tokens to ensure these are wrapper options
144 # as these shouldn't be found in a real path (dotted path or file path)
145 _wrapper_args, _wrapper_kwargs = cast.str2args(_wrapper_options)
146 _path = __path
148 if _path in names:
149 # [foo:bar:fleem]
150 wrapped_name = _path
151 else:
152 # stub value for "anonymous" name
153 # [foo:bar:%(here)s/objects.py:MyClass]
154 wrapped_name = section
156 # get wrapper options
157 if wrapper not in config:
158 # load wrapper configuration
159 wrapper_section = names[wrapper]
160 if wrapper_section in seen:
161 pass # TODO
162 create_section(wrapper_section, iniconfig[wrapper_section])
163 wrapper_options = deepcopy(config[wrapper])
165 # add inline wrapper args, kwargs
166 if _wrapper_args is not None:
167 if 'args' in wrapper_options:
168 wrapper_options['args'].extend(_wrapper_args)
169 else:
170 wrapper_options['args'] = _wrapper_args
171 if _wrapper_kwargs is not None:
172 if 'kwargs' in wrapper_options:
173 wrapper_options['kwargs'].update(_wrapper_kwargs)
174 else:
175 wrapper_options['kwargs'] = _wrapper_kwargs
177 # interpolate wrapper_options
178 def interpolate(option):
179 if option == object_string:
180 return '%(' + wrapped_name + ')s'
181 return option
182 if 'args' in wrapper_options:
183 args = wrapper_options['args'][:]
184 args = [interpolate(i) for i in args]
185 wrapper_options['args'] = args
186 if 'kwargs' in wrapper_options:
187 kwargs = wrapper_options['kwargs'].copy()
188 kwargs = dict([(i,interpolate(j)) for i, j in kwargs.items()])
189 wrapper_options['kwargs'] = kwargs
191 # create wrapper
192 config[name] = wrapper_options
193 if _path == wrapped_name:
194 return
195 name = wrapped_name
196 path = _path
198 elif path in names:
199 # override section: [foo:bar]
200 if path not in config:
201 # load overridden section
202 overridden_section = names[path]
203 if overridden_section in seen:
204 pass # TODO
205 create_section(overridden_section, iniconfig[overridden_section])
206 sect = deepcopy(config[path])
208 if 'path' not in sect:
209 # add the path to section dict
210 path = path % defaults
211 sect['path'] = path
213 # get arguments from .ini options
214 for option, value in options.items():
215 if option == '.': # positional arguments
216 sect['args'] = cast.str2list(value)
217 else: # keyword arguments
218 sect.setdefault('kwargs', {})[option] = value
220 # set the configuration
221 config[name] = sect
223 # get the object definitions
224 for section, options in iniconfig.items():
225 seen.add(section)
226 if section not in interpolated:
227 create_section(section, options)
228 interpolated.add(section)
230 return config
232 @classmethod
233 def read(cls, inifile):
234 """reads configuration from an .ini file"""
236 here = os.path.dirname(os.path.abspath(inifile))
238 # read configuration
239 defaults={'here': here,
240 'this': os.path.abspath(inifile)}
241 parser = ConfigParser(defaults=defaults)
242 parser.optionxform = str # use whole case
243 parser.read(inifile)
245 # parse configuration
246 config = {}
247 for section in parser.sections():
249 config[section] = {}
251 # read the options
252 for option in parser.options(section):
254 if option in parser.defaults():
255 # don't include the defaults
256 continue
258 # try to interpolate the option
259 # otherwise, use the raw value
260 try:
261 value = parser.get(section, option)
262 except (InterpolationMissingOptionError, InterpolationSyntaxError, InterpolationDepthError):
263 value = parser.get(section, option, raw=True)
265 config[section][option] = value
267 return cls.configuration(config, **parser.defaults())
269 def main(args=sys.argv[1:]):
270 """command line entry point"""
271 usage = '%prog file1.ini -arg1 -arg2 --key1=value1 --key2=value2'
272 parser = OptionParser(usage=usage, description=IniFactory.__doc__)
273 options, args = parser.parse_args(args)
275 if len(args) != 1:
276 parser.print_usage()
277 parser.exit()
279 factory = IniFactory(args[0])
280 obj = factory.load()
281 print obj
283 if __name__ == '__main__':
284 main()
