import re import os import sys import pkg_resources import new import types __all__ = ['load_ob'] _group_re = re.compile('\[([\w.-]+)\]$') _invalid_chars_re = re.compile('[^a-z0-9_]+', re.I) _valid_identifier_re = re.compile('^[a-z_][a-z0-9_]*$', re.I) LOADERS = {} class LoadError(ValueError): """ Parent class for errors loading object. """ def __init__(self, message, string, position=None): ValueError.__init__(self, message) self.string = string self.position = position def __str__(self): s = ValueError.__str__(self) s += ' in %r' % self.string if self.position: s += ' at %s' % self.format_position(self.position) return s def format_position(self, position): if isinstance(position, (tuple, list)): if len(position) == 1: position = position[0] elif len(position) == 2: position = '%s line %s' % (position[0], position[1]) elif len(position) == 3: position = '%s line %s, column %s' % (position[0], position[1], position[2]) if isinstance(position, basestring): return position class ConfigSyntaxError(LoadError): """ Raised when the syntax of the configuration loader is incorrect. """ class BadGroupError(LoadError): """ Raised when an explicit group isn't allowed. """ class NotFoundError(LoadError): """ Raised when the resource cannot be found """ class PythonError(LoadError): """ Raised when there's a problem loading a Python object, from a file or expression """ def load_ob(string, groups=None, position=None, extra_schemes=None): orig_string = string if extra_schemes is None: extra_schemes = {} if isinstance(groups, basestring): raise TypeError( "Groups must be None or a sequence, not a string") if groups is None: groups = [] string = string.strip() match = _group_re.search(string) if match: group = match.group(1) string = string[:match.start()].strip() if groups and group not in groups: raise BadGroupError( "The group %r is not supported in this context (must be one of: %s)" % (group, ', '.join(groups)), orig_string, position) else: group = None if string.startswith('egg:'): # Backward compatibility for Paste Deploy ## FIXME: add warning parts = string.split(':', 1) else: parts = string.split(None, 1) if len(parts) == 1: raise ConfigSyntaxError( "Object references must start with a scheme like 'python ' or 'file '", string=orig_string, position=position) scheme, string = parts scheme = scheme.lower() if scheme not in LOADERS and scheme not in extra_schemes: raise ConfigSyntaxError( "Object reference scheme (%r) unknown (choose one of: %s)" % (scheme, ', '.join(sorted(LOADERS.keys() + extra_schemes.keys()))), string=orig_string, position=position) if scheme in extra_schemes: loader = extra_schemes[scheme] else: loader = LOADERS[scheme] return loader(string, groups, orig_string, position=position, group=group) def load_egg(string, groups, orig_string, position, group): if not groups and not group: raise ConfigSyntaxError( "Egg object references are not supported in this context", string=orig_string, position=position) if group: groups = [group] if ':' in string: req, ep_name = string.split(':', 1) elif '#' in string: # For Paste Deploy compatibility ## FIXME: add warning req, ep_name = string.split(':', 1) else: req = string ## FIXME: should also support 'default' for zc.buildout ep_name = 'main' try: dist = pkg_resources.get_distribution(req) except pkg_resources.DistributionNotFound, e: ## FIXME: test exception raise NotFoundError( "The distribution %r was not found: %s" % (req, e), orig_string, position) ep = None for group in groups: try: ep = dist.get_entry_info(group, ep_name) except 0: ## FIXME: figure out exception pass else: break if ep is None: raise NotFoundError( "The entry point %s (from group %s) in the distribution %s could not be found" % (ep_name, ', '.join(groups), req), orig_string, position) try: ep = ep.load() except ImportError, e: ## FIXME: do something useful with orig_string and position raise return ep, group LOADERS['egg'] = load_egg def load_file(string, groups, orig_string, position, group): drive, rest = os.path.splitdrive(string) if ':' in rest: rest, expr = rest.split(':', 1) else: expr = None filename = os.path.join(drive, rest) module_name = _invalid_chars_re.sub('_', filename).strip('_') module = new.module(module_name) module.__name__ = module_name module.__file__ = filename ## FIXME: should set __doc__ too, if I could; but it's hard if not os.path.exists(filename): raise NotFoundError( 'The filename %s does not exist' % filename, orig_string, position) if os.path.isdir(filename): raise NotFoundError( 'The filename %s is a directory, and cannot be loaded' % filename, orig_string, position) try: execfile(filename, module.__dict__) except: exc_info = sys.exc_info() exc = PythonError('%s: %s' % (exc_info[0].__name__, exc_info[1]), orig_string, position) raise exc.__class__, exc, exc_info[2] obj = module if expr: obj = eval_object(module, filename, expr, orig_string, position) return obj, group LOADERS['file'] = load_file def eval_object(root, root_name, expr, orig_string, position): ## FIXME: allowing obj[key] would also be nice, but harder parts = expr.split('.') name = root_name + ':' obj = root for part in parts: if not _valid_identifier_re.match(part): raise ConfigSyntaxError( "The attribute %r is not a valid Python attribute" % part, orig_string, position) if not name.endswith(':'): name += '.' name += part try: obj = getattr(obj, part) except AttributeError, e: raise PythonError( "Could not get attribute %s from object %s (%r): %s" % (part, name, obj, e), orig_string, position) return obj def load_python(string, groups, orig_string, position, group): if ':' in string: module_name, expr = string.split(':', 1) else: module_name = string expr = None ## FIXME: should catch errors better module = try_import_module(module_name) if module is None: raise PythonError( "No module by the name %s exists" % module_name, orig_string, position) if expr: obj = eval_object(module, module_name, expr, orig_string, position) else: obj = module return obj, group LOADERS['python'] = load_python def try_import_module(module_name): """ Imports a module, but catches import errors. Only catches errors when that module doesn't exist; if that module itself has an import error it will still get raised. Returns None if the module doesn't exist. """ try: return import_module(module_name) except ImportError, e: if not getattr(e, 'args', None): raise desc = e.args[0] if not desc.startswith('No module named '): raise desc = desc[len('No module named '):] # If you import foo.bar.baz, the bad import could be any # of foo.bar.baz, bar.baz, or baz; we'll test them all: parts = module_name.split('.') for i in range(len(parts)): if desc == '.'.join(parts[i:]): return None raise def import_module(s): """ Import a module. """ mod = __import__(s) parts = s.split('.') for part in parts[1:]: mod = getattr(mod, part) return mod for _name, _value in globals().items(): if (isinstance(_value, types.ClassType) and issubclass(_value, LoadError)): __all__.append(_name) del _name, _value