def load(path, format=None): """ Expects a filename, returns a dictionary. Raises ConfigurationError if the file could not be read or parsed. :param path: path to the file. :param format: format in which the configuration dictionary in serialized in the file (one of: "yaml", "json"). """ if not os.path.exists(path): raise ConfigurationError('File "%s" does not exist' % path) # guess file format if not format: for known_format in FORMATS: if path.endswith('.%s' % known_format): format = known_format break else: raise ConfigurationError('Could not guess format for "%s"' % path) assert format in FORMATS, 'unknown format %s' % format # deserialize file contents to a Python dictionary try: f = open(path) except IOError as e: raise ConfigurationError('Could not open "%s": %s' % (path, e)) data = f.read() try: loader = import_attribute(FORMATS[format]) except ImportError as e: raise ConfigurationError('Could not import "%s" format loader: %s' % (format, e)) try: conf = loader(data) except Exception as e: raise ConfigurationError('Could not deserialize config data: %s' % e) if not isinstance(conf, dict): raise ConfigurationError('Deserialized config must be a dict, got "%s"' % conf) return conf
def _load_extensions(self): # configured extensions (instances) are indexed by full dotted path # (including class name). It is also possible to access them by feature # name: see Application.get_feature(). logger.debug('Loading extensions...') _extensions = {} _features = {} # collect extensions, make sure they can be imported and group them by # identity. The identity is declared by the extension class. It is usually # the dotted path to the extension module but can be otherwse if the # extension implements a named role (e.g. "storage" or "templating"). if not 'extensions' in self.settings: import warnings warnings.warn('No extensions configured. Application is unusable.') for path in self.settings.get('extensions', []): assert isinstance(path, basestring), ( 'cannot load extension by module path: expected a string, ' 'got {0}'.format(repr(bundle))) logger.debug('Loading (importing) extension {0}'.format(path)) # # NOTE: the "smart" stuff is commented out because in most cases # this hides the valuable call stack; moreover, this happens on # start so wrapping is really unnecessary. # #try: # cls = import_attribute(path) #except (ImportError, AttributeError) as e: # raise ConfigurationError( # 'Could not load extension "{0}": {1}'.format(path, e)) # cls = import_attribute(path) conf = self.settings['extensions'][path] _extensions[path] = cls, conf if getattr(cls, 'features', None): # XXX here "features" is a verb, not a noun; may be misleading # for feature in cls.features: if not isinstance(cls.features, basestring): raise ConfigurationError( 'Extension should supply its feature name as string; ' '{path} defines {cls.features}'.format(**locals())) assert cls.features not in _features, ( '{feature} must be unique'.format(**locals())) _features[cls.features] = path # now actually initialize the extensions and save them to the application # manager instance. This involves checking dependencies. They are # listed as dotted paths ("foo.bar") or features ("{quux}"). The # features must be dereferenced to dotted paths. We can only do it # after we have an imported class that declares itself an # implementation of given feature (i.e. "class MyExt: features='foo'"). # That's why we are messing with two loading stages. self._extensions = {} self._features = _features stacked = {} loaded = {} # TODO: refactor (too complex, must be easily readable) def load_extension(path): if path in stacked: # raise a helpful exception to track down a circular dependency classes = [_extensions[x][0] for x in stacked] related = [p for p in classes if p.requires and path in [r.format(**_features) for r in p.requires]] strings = ['.'.join([p.__module__,p.__name__]) for p in related] raise RuntimeError('Cannot load extension "{0}": circular ' 'dependency with {1}'.format(path, strings)) if path in loaded: # this happens if the plugin has already been loaded as a # dependency of another plugin logger.debug('Already loaded: {0}'.format(path)) return cls, conf = _extensions[path] assert path not in self._extensions, ( 'Cannot load extension {0}: path "{1}" is already loaded as ' '{2}.'.format(cls, path, self._extensions[path])) stacked[path] = True # to prevent circular dependencies # load dependencies if getattr(cls, 'requires', None): assert isinstance(cls.requires, (tuple, list)), ( '{0}.{1}.requires must be a list or tuple'.format( cls.__module__, cls.__name__)) for req_string in cls.requires: # TODO: document this behaviour, i.e. "{templating}" -> "tool.ext.jinja" try: requirement = req_string.format(**_features) except KeyError as e: raise ConfigurationError( 'Unknown feature "{0}". Expected one of ' '{1}'.format(e, list(_features))) logger.debug('Dependency: {0} requires {1}'.format(path, requirement)) if path == requirement: raise ConfigurationError('{0} requires itself.'.format(path)) if requirement not in _extensions: raise ConfigurationError( 'Plugin {0}.{1} requires extension "{2}" which is ' 'not configured. These are configured: ' '{3}.'.format(cls.__module__, cls.__name__, requirement, _extensions.keys())) load_extension(requirement) # recursion # initialize and register the extension extension = cls(self, conf) self._extensions[path] = extension loaded[path] = True stacked.pop(path) # load each extension with recursive dependencies for path in _extensions: load_extension(path)