# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php from configparser import ConfigParser import os import re import sys from urllib.parse import unquote from paste.deploy.util import fix_call, importlib_metadata, lookup_object __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] ############################################################ # Utility functions ############################################################ def find_entry_point(dist, group, name): for entry in dist.entry_points: if entry.name == name and entry.group == group: return entry def _aslist(obj): """ Turn object into a list; lists and tuples are left as-is, None becomes [], and everything else turns into a one-element list. """ if obj is None: return [] elif isinstance(obj, (list, tuple)): return obj else: return [obj] def _flatten(lst): """ Flatten a nested list. """ if not isinstance(lst, (list, tuple)): return [lst] result = [] for item in lst: result.extend(_flatten(item)) return result class NicerConfigParser(ConfigParser): def __init__(self, filename, *args, **kw): ConfigParser.__init__(self, *args, **kw) self.filename = filename self._interpolation = self.InterpolateWrapper(self._interpolation) def defaults(self): """Return the defaults, with their values interpolated (with the defaults dict itself) Mainly to support defaults using values such as %(here)s """ defaults = ConfigParser.defaults(self).copy() for key, val in defaults.items(): defaults[key] = self.get('DEFAULT', key) or val return defaults class InterpolateWrapper: def __init__(self, original): self._original = original def __getattr__(self, name): return getattr(self._original, name) def before_get(self, parser, section, option, value, defaults): try: return self._original.before_get( parser, section, option, value, defaults ) except Exception: e = sys.exc_info()[1] args = list(e.args) args[0] = f'Error in file {parser.filename}: {e}' e.args = tuple(args) e.message = args[0] raise ############################################################ # Object types ############################################################ class _ObjectType: name = None egg_protocols = None config_prefixes = None def __init__(self): # Normalize these variables: self.egg_protocols = [_aslist(p) for p in _aslist(self.egg_protocols)] self.config_prefixes = [_aslist(p) for p in _aslist(self.config_prefixes)] def __repr__(self): return '<{} protocols={!r} prefixes={!r}>'.format( self.name, self.egg_protocols, self.config_prefixes ) def invoke(self, context): assert context.protocol in _flatten(self.egg_protocols) return fix_call(context.object, context.global_conf, **context.local_conf) class _App(_ObjectType): name = 'application' egg_protocols = [ 'paste.app_factory', 'paste.composite_factory', 'paste.composit_factory', ] config_prefixes = [ ['app', 'application'], ['composite', 'composit'], 'pipeline', 'filter-app', ] def invoke(self, context): if context.protocol in ('paste.composit_factory', 'paste.composite_factory'): return fix_call( context.object, context.loader, context.global_conf, **context.local_conf, ) elif context.protocol == 'paste.app_factory': return fix_call(context.object, context.global_conf, **context.local_conf) else: assert 0, "Protocol %r unknown" % context.protocol APP = _App() class _Filter(_ObjectType): name = 'filter' egg_protocols = [['paste.filter_factory', 'paste.filter_app_factory']] config_prefixes = ['filter'] def invoke(self, context): if context.protocol == 'paste.filter_factory': return fix_call(context.object, context.global_conf, **context.local_conf) elif context.protocol == 'paste.filter_app_factory': def filter_wrapper(wsgi_app): # This should be an object, so it has a nicer __repr__ return fix_call( context.object, wsgi_app, context.global_conf, **context.local_conf ) return filter_wrapper else: assert 0, "Protocol %r unknown" % context.protocol FILTER = _Filter() class _Server(_ObjectType): name = 'server' egg_protocols = [['paste.server_factory', 'paste.server_runner']] config_prefixes = ['server'] def invoke(self, context): if context.protocol == 'paste.server_factory': return fix_call(context.object, context.global_conf, **context.local_conf) elif context.protocol == 'paste.server_runner': def server_wrapper(wsgi_app): # This should be an object, so it has a nicer __repr__ return fix_call( context.object, wsgi_app, context.global_conf, **context.local_conf ) return server_wrapper else: assert 0, "Protocol %r unknown" % context.protocol SERVER = _Server() # Virtual type: (@@: There's clearly something crufty here; # this probably could be more elegant) class _PipeLine(_ObjectType): name = 'pipeline' def invoke(self, context): app = context.app_context.create() filters = [c.create() for c in context.filter_contexts] filters.reverse() for filter in filters: app = filter(app) return app PIPELINE = _PipeLine() class _FilterApp(_ObjectType): name = 'filter_app' def invoke(self, context): next_app = context.next_context.create() filter = context.filter_context.create() return filter(next_app) FILTER_APP = _FilterApp() class _FilterWith(_App): name = 'filtered_with' def invoke(self, context): filter = context.filter_context.create() filtered = context.next_context.create() if context.next_context.object_type is APP: return filter(filtered) else: # filtering a filter def composed(app): return filter(filtered(app)) return composed FILTER_WITH = _FilterWith() ############################################################ # Loaders ############################################################ def loadapp(uri, name=None, **kw): return loadobj(APP, uri, name=name, **kw) def loadfilter(uri, name=None, **kw): return loadobj(FILTER, uri, name=name, **kw) def loadserver(uri, name=None, **kw): return loadobj(SERVER, uri, name=name, **kw) def appconfig(uri, name=None, relative_to=None, global_conf=None): context = loadcontext( APP, uri, name=name, relative_to=relative_to, global_conf=global_conf ) return context.config() _loaders = {} def loadobj(object_type, uri, name=None, relative_to=None, global_conf=None): context = loadcontext( object_type, uri, name=name, relative_to=relative_to, global_conf=global_conf ) return context.create() def loadcontext(object_type, uri, name=None, relative_to=None, global_conf=None): if '#' in uri: if name is None: uri, name = uri.split('#', 1) else: # @@: Ignore fragment or error? uri = uri.split('#', 1)[0] if name is None: name = 'main' if ':' not in uri: raise LookupError("URI has no scheme: %r" % uri) scheme, path = uri.split(':', 1) scheme = scheme.lower() if scheme not in _loaders: raise LookupError( "URI scheme not known: {!r} (from {})".format( scheme, ', '.join(_loaders.keys()) ) ) return _loaders[scheme]( object_type, uri, path, name=name, relative_to=relative_to, global_conf=global_conf, ) def _loadconfig(object_type, uri, path, name, relative_to, global_conf): isabs = os.path.isabs(path) # De-Windowsify the paths: path = path.replace('\\', '/') if not isabs: if not relative_to: raise ValueError( "Cannot resolve relative uri %r; no relative_to keyword " "argument given" % uri ) relative_to = relative_to.replace('\\', '/') if relative_to.endswith('/'): path = relative_to + path else: path = relative_to + '/' + path if path.startswith('///'): path = path[2:] path = unquote(path) loader = ConfigLoader(path) if global_conf: loader.update_defaults(global_conf, overwrite=False) return loader.get_context(object_type, name, global_conf) _loaders['config'] = _loadconfig def _loadegg(object_type, uri, spec, name, relative_to, global_conf): loader = EggLoader(spec) return loader.get_context(object_type, name, global_conf) _loaders['egg'] = _loadegg def _loadfunc(object_type, uri, spec, name, relative_to, global_conf): loader = FuncLoader(spec) return loader.get_context(object_type, name, global_conf) _loaders['call'] = _loadfunc ############################################################ # Loaders ############################################################ class _Loader: def get_app(self, name=None, global_conf=None): return self.app_context(name=name, global_conf=global_conf).create() def get_filter(self, name=None, global_conf=None): return self.filter_context(name=name, global_conf=global_conf).create() def get_server(self, name=None, global_conf=None): return self.server_context(name=name, global_conf=global_conf).create() def app_context(self, name=None, global_conf=None): return self.get_context(APP, name=name, global_conf=global_conf) def filter_context(self, name=None, global_conf=None): return self.get_context(FILTER, name=name, global_conf=global_conf) def server_context(self, name=None, global_conf=None): return self.get_context(SERVER, name=name, global_conf=global_conf) _absolute_re = re.compile(r'^[a-zA-Z]+:') def absolute_name(self, name): """ Returns true if the name includes a scheme """ if name is None: return False return self._absolute_re.search(name) class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename = filename.strip() defaults = { 'here': os.path.dirname(os.path.abspath(filename)), '__file__': os.path.abspath(filename), } self.parser = NicerConfigParser(filename, defaults=defaults) self.parser.optionxform = str # Don't lower-case keys with open(filename) as f: self.parser.read_file(f) def update_defaults(self, new_defaults, overwrite=True): for key, value in new_defaults.items(): if not overwrite and key in self.parser._defaults: continue self.parser._defaults[key] = value def get_context(self, object_type, name=None, global_conf=None): if self.absolute_name(name): return loadcontext( object_type, name, relative_to=os.path.dirname(self.filename), global_conf=global_conf, ) section = self.find_config_section(object_type, name=name) defaults = self.parser.defaults() _global_conf = defaults.copy() if global_conf is not None: _global_conf.update(global_conf) global_conf = _global_conf local_conf = {} global_additions = {} get_from_globals = {} for option in self.parser.options(section): if option.startswith('set '): name = option[4:].strip() global_additions[name] = global_conf[name] = self.parser.get( section, option ) elif option.startswith('get '): name = option[4:].strip() get_from_globals[name] = self.parser.get(section, option) else: if option in defaults: # @@: It's a global option (?), so skip it continue local_conf[option] = self.parser.get(section, option) for local_var, glob_var in get_from_globals.items(): local_conf[local_var] = global_conf[glob_var] if object_type in (APP, FILTER) and 'filter-with' in local_conf: filter_with = local_conf.pop('filter-with') else: filter_with = None if 'require' in local_conf: for spec in local_conf['require'].split(): importlib_metadata.distribution(spec) del local_conf['require'] if section.startswith('filter-app:'): context = self._filter_app_context( object_type, section, name=name, global_conf=global_conf, local_conf=local_conf, global_additions=global_additions, ) elif section.startswith('pipeline:'): context = self._pipeline_app_context( object_type, section, name=name, global_conf=global_conf, local_conf=local_conf, global_additions=global_additions, ) elif 'use' in local_conf: context = self._context_from_use( object_type, local_conf, global_conf, global_additions, section ) else: context = self._context_from_explicit( object_type, local_conf, global_conf, global_additions, section ) if filter_with is not None: filter_with_context = LoaderContext( obj=None, object_type=FILTER_WITH, protocol=None, global_conf=global_conf, local_conf=local_conf, loader=self, ) filter_with_context.filter_context = self.filter_context( name=filter_with, global_conf=global_conf ) filter_with_context.next_context = context return filter_with_context return context def _context_from_use( self, object_type, local_conf, global_conf, global_additions, section ): use = local_conf.pop('use') context = self.get_context(object_type, name=use, global_conf=global_conf) context.global_conf.update(global_additions) context.local_conf.update(local_conf) if '__file__' in global_conf: # use sections shouldn't overwrite the original __file__ context.global_conf['__file__'] = global_conf['__file__'] # @@: Should loader be overwritten? context.loader = self if context.protocol is None: # Determine protocol from section type section_protocol = section.split(':', 1)[0] if section_protocol in ('application', 'app'): context.protocol = 'paste.app_factory' elif section_protocol in ('composit', 'composite'): context.protocol = 'paste.composit_factory' else: # This will work with 'server' and 'filter', otherwise it # could fail but there is an error message already for # bad protocols context.protocol = 'paste.%s_factory' % section_protocol return context def _context_from_explicit( self, object_type, local_conf, global_conf, global_addition, section ): possible = [] for protocol_options in object_type.egg_protocols: for protocol in protocol_options: if protocol in local_conf: possible.append((protocol, local_conf[protocol])) break if len(possible) > 1: raise LookupError( f"Multiple protocols given in section {section!r}: {possible}" ) if not possible: raise LookupError("No loader given in section %r" % section) found_protocol, found_expr = possible[0] del local_conf[found_protocol] value = importlib_metadata.EntryPoint( name=None, group=None, value=found_expr ).load() context = LoaderContext( value, object_type, found_protocol, global_conf, local_conf, self ) return context def _filter_app_context( self, object_type, section, name, global_conf, local_conf, global_additions ): if 'next' not in local_conf: raise LookupError( "The [%s] section in %s is missing a 'next' setting" % (section, self.filename) ) next_name = local_conf.pop('next') context = LoaderContext(None, FILTER_APP, None, global_conf, local_conf, self) context.next_context = self.get_context(APP, next_name, global_conf) if 'use' in local_conf: context.filter_context = self._context_from_use( FILTER, local_conf, global_conf, global_additions, section ) else: context.filter_context = self._context_from_explicit( FILTER, local_conf, global_conf, global_additions, section ) return context def _pipeline_app_context( self, object_type, section, name, global_conf, local_conf, global_additions ): if 'pipeline' not in local_conf: raise LookupError( "The [%s] section in %s is missing a 'pipeline' setting" % (section, self.filename) ) pipeline = local_conf.pop('pipeline').split() if local_conf: raise LookupError( "The [%s] pipeline section in %s has extra " "(disallowed) settings: %s" % (section, self.filename, ', '.join(local_conf.keys())) ) context = LoaderContext(None, PIPELINE, None, global_conf, local_conf, self) context.app_context = self.get_context(APP, pipeline[-1], global_conf) context.filter_contexts = [ self.get_context(FILTER, name, global_conf) for name in pipeline[:-1] ] return context def find_config_section(self, object_type, name=None): """ Return the section name with the given name prefix (following the same pattern as ``protocol_desc`` in ``config``. It must have the given name, or for ``'main'`` an empty name is allowed. The prefix must be followed by a ``:``. Case is *not* ignored. """ possible = [] for name_options in object_type.config_prefixes: for name_prefix in name_options: found = self._find_sections(self.parser.sections(), name_prefix, name) if found: possible.extend(found) break if not possible: raise LookupError( "No section %r (prefixed by %s) found in config %s" % ( name, ' or '.join(map(repr, _flatten(object_type.config_prefixes))), self.filename, ) ) if len(possible) > 1: raise LookupError( "Ambiguous section names %r for section %r (prefixed by %s) " "found in config %s" % ( possible, name, ' or '.join(map(repr, _flatten(object_type.config_prefixes))), self.filename, ) ) return possible[0] def _find_sections(self, sections, name_prefix, name): found = [] if name is None: if name_prefix in sections: found.append(name_prefix) name = 'main' for section in sections: if section.startswith(name_prefix + ':'): if section[len(name_prefix) + 1 :].strip() == name: found.append(section) return found class EggLoader(_Loader): def __init__(self, spec): self.spec = spec def get_context(self, object_type, name=None, global_conf=None): if self.absolute_name(name): return loadcontext(object_type, name, global_conf=global_conf) entry_point, protocol, ep_name = self.find_egg_entry_point( object_type, name=name ) return LoaderContext( entry_point, object_type, protocol, global_conf or {}, {}, self, distribution=importlib_metadata.distribution(self.spec), entry_point_name=ep_name, ) def find_egg_entry_point(self, object_type, name=None): """ Returns the (entry_point, protocol) for with the given ``name``. """ if name is None: name = 'main' dist = importlib_metadata.distribution(self.spec) possible = [] for protocol_options in object_type.egg_protocols: for protocol in protocol_options: entry = find_entry_point(dist, protocol, name) if entry is not None: possible.append((entry.load(), protocol, entry.name)) break if not possible: # Better exception raise LookupError( "Entry point %r not found in egg %r (protocols: %s; entry_points: %s)" % ( name, self.spec, ', '.join(_flatten(object_type.egg_protocols)), ', '.join( str(entry) for prot in protocol_options for entry in [find_entry_point(dist, prot, name)] if entry ), ) ) if len(possible) > 1: raise LookupError( "Ambiguous entry points for %r in egg %r (protocols: %s)" % (name, self.spec, ', '.join(_flatten(protocol_options))) ) return possible[0] class FuncLoader(_Loader): """Loader that supports specifying functions inside modules, without using eggs at all. Configuration should be in the format: use = call:my.module.path:function_name Dot notation is supported in both the module and function name, e.g.: use = call:my.module.path:object.method """ def __init__(self, spec): self.spec = spec if ':' not in spec: raise LookupError("Configuration not in format module:function") def get_context(self, object_type, name=None, global_conf=None): obj = lookup_object(self.spec) return LoaderContext( obj, object_type, None, # determine protocol from section type global_conf or {}, {}, self, ) class LoaderContext: def __init__( self, obj, object_type, protocol, global_conf, local_conf, loader, distribution=None, entry_point_name=None, ): self.object = obj self.object_type = object_type self.protocol = protocol # assert protocol in _flatten(object_type.egg_protocols), ( # "Bad protocol %r; should be one of %s" # % (protocol, ', '.join(map(repr, _flatten(object_type.egg_protocols))))) self.global_conf = global_conf self.local_conf = local_conf self.loader = loader self.distribution = distribution self.entry_point_name = entry_point_name def create(self): return self.object_type.invoke(self) def config(self): conf = AttrDict(self.global_conf) conf.update(self.local_conf) conf.local_conf = self.local_conf conf.global_conf = self.global_conf conf.context = self return conf class AttrDict(dict): """ A dictionary that can be assigned to. """ pass