diff options
author | Bert JW Regeer <xistence@0x58.com> | 2022-05-15 20:16:25 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-15 20:16:25 -0600 |
commit | ac91794dfb7617b473c9d06d04900debfcdf585e (patch) | |
tree | de311f1e71d5306a9499e230a9537c2a530cc66f /src | |
parent | ee1d364dabd544fe4bf355b2c6578e4237b03992 (diff) | |
parent | d73a3389898cb18f5def996c252c494e6ad1966a (diff) | |
download | pastedeploy-git-ac91794dfb7617b473c9d06d04900debfcdf585e.tar.gz |
Merge pull request #35 from Pylons/src-folder
Diffstat (limited to 'src')
-rw-r--r-- | src/paste/__init__.py | 18 | ||||
-rw-r--r-- | src/paste/deploy/__init__.py | 3 | ||||
-rw-r--r-- | src/paste/deploy/config.py | 305 | ||||
-rw-r--r-- | src/paste/deploy/converters.py | 37 | ||||
-rw-r--r-- | src/paste/deploy/loadwsgi.py | 713 | ||||
-rw-r--r-- | src/paste/deploy/paster_templates.py | 34 | ||||
-rw-r--r-- | src/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl | 23 | ||||
-rw-r--r-- | src/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl | 25 | ||||
-rw-r--r-- | src/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 22 | ||||
-rw-r--r-- | src/paste/deploy/util.py | 71 |
10 files changed, 1251 insertions, 0 deletions
diff --git a/src/paste/__init__.py b/src/paste/__init__.py new file mode 100644 index 0000000..cdb6121 --- /dev/null +++ b/src/paste/__init__.py @@ -0,0 +1,18 @@ +# (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 +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + # don't prevent use of paste if pkg_resources isn't installed + from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) + +try: + import modulefinder +except ImportError: + pass +else: + for p in __path__: + modulefinder.AddPackagePath(__name__, p) + diff --git a/src/paste/deploy/__init__.py b/src/paste/deploy/__init__.py new file mode 100644 index 0000000..94c63a8 --- /dev/null +++ b/src/paste/deploy/__init__.py @@ -0,0 +1,3 @@ +# (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 paste.deploy.loadwsgi import * diff --git a/src/paste/deploy/config.py b/src/paste/deploy/config.py new file mode 100644 index 0000000..f448350 --- /dev/null +++ b/src/paste/deploy/config.py @@ -0,0 +1,305 @@ +# (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 +"""Paste Configuration Middleware and Objects""" +import threading +import re + +# Loaded lazily +wsgilib = None +local = None + +__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware', 'PrefixMiddleware'] + + +def local_dict(): + global config_local, local + try: + return config_local.wsgi_dict + except NameError: + config_local = threading.local() + config_local.wsgi_dict = result = {} + return result + except AttributeError: + config_local.wsgi_dict = result = {} + return result + + +class DispatchingConfig: + + """ + This is a configuration object that can be used globally, + imported, have references held onto. The configuration may differ + by thread (or may not). + + Specific configurations are registered (and deregistered) either + for the process or for threads. + """ + + # @@: What should happen when someone tries to add this + # configuration to itself? Probably the conf should become + # resolved, and get rid of this delegation wrapper + + _constructor_lock = threading.Lock() + + def __init__(self): + self._constructor_lock.acquire() + try: + self.dispatching_id = 0 + while 1: + self._local_key = 'paste.processconfig_%i' % self.dispatching_id + if not self._local_key in local_dict(): + break + self.dispatching_id += 1 + finally: + self._constructor_lock.release() + self._process_configs = [] + + def push_thread_config(self, conf): + """ + Make ``conf`` the active configuration for this thread. + Thread-local configuration always overrides process-wide + configuration. + + This should be used like:: + + conf = make_conf() + dispatching_config.push_thread_config(conf) + try: + ... do stuff ... + finally: + dispatching_config.pop_thread_config(conf) + """ + local_dict().setdefault(self._local_key, []).append(conf) + + def pop_thread_config(self, conf=None): + """ + Remove a thread-local configuration. If ``conf`` is given, + it is checked against the popped configuration and an error + is emitted if they don't match. + """ + self._pop_from(local_dict()[self._local_key], conf) + + def _pop_from(self, lst, conf): + popped = lst.pop() + if conf is not None and popped is not conf: + raise AssertionError( + "The config popped (%s) is not the same as the config " + "expected (%s)" + % (popped, conf)) + + def push_process_config(self, conf): + """ + Like push_thread_config, but applies the configuration to + the entire process. + """ + self._process_configs.append(conf) + + def pop_process_config(self, conf=None): + self._pop_from(self._process_configs, conf) + + def __getattr__(self, attr): + conf = self.current_conf() + if conf is None: + raise AttributeError( + "No configuration has been registered for this process " + "or thread") + return getattr(conf, attr) + + def current_conf(self): + thread_configs = local_dict().get(self._local_key) + if thread_configs: + return thread_configs[-1] + elif self._process_configs: + return self._process_configs[-1] + else: + return None + + def __getitem__(self, key): + # I thought __getattr__ would catch this, but apparently not + conf = self.current_conf() + if conf is None: + raise TypeError( + "No configuration has been registered for this process " + "or thread") + return conf[key] + + def __contains__(self, key): + # I thought __getattr__ would catch this, but apparently not + return key in self + + def __setitem__(self, key, value): + # I thought __getattr__ would catch this, but apparently not + conf = self.current_conf() + conf[key] = value + +CONFIG = DispatchingConfig() + + +class ConfigMiddleware: + + """ + A WSGI middleware that adds a ``paste.config`` key to the request + environment, as well as registering the configuration temporarily + (for the length of the request) with ``paste.CONFIG``. + """ + + def __init__(self, application, config): + """ + This delegates all requests to `application`, adding a *copy* + of the configuration `config`. + """ + self.application = application + self.config = config + + def __call__(self, environ, start_response): + global wsgilib + if wsgilib is None: + import pkg_resources + pkg_resources.require('Paste') + from paste import wsgilib + popped_config = None + if 'paste.config' in environ: + popped_config = environ['paste.config'] + conf = environ['paste.config'] = self.config.copy() + app_iter = None + CONFIG.push_thread_config(conf) + try: + app_iter = self.application(environ, start_response) + finally: + if app_iter is None: + # An error occurred... + CONFIG.pop_thread_config(conf) + if popped_config is not None: + environ['paste.config'] = popped_config + if type(app_iter) in (list, tuple): + # Because it is a concrete iterator (not a generator) we + # know the configuration for this thread is no longer + # needed: + CONFIG.pop_thread_config(conf) + if popped_config is not None: + environ['paste.config'] = popped_config + return app_iter + else: + def close_config(): + CONFIG.pop_thread_config(conf) + new_app_iter = wsgilib.add_close(app_iter, close_config) + return new_app_iter + + +def make_config_filter(app, global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return ConfigMiddleware(app, conf) + +make_config_middleware = ConfigMiddleware.__doc__ + + +class PrefixMiddleware: + """Translate a given prefix into a SCRIPT_NAME for the filtered + application. + + PrefixMiddleware provides a way to manually override the root prefix + (SCRIPT_NAME) of your application for certain, rare situations. + + When running an application under a prefix (such as '/james') in + FastCGI/apache, the SCRIPT_NAME environment variable is automatically + set to to the appropriate value: '/james'. Pylons' URL generating + functions, such as url_for, always take the SCRIPT_NAME value into account. + + One situation where PrefixMiddleware is required is when an application + is accessed via a reverse proxy with a prefix. The application is accessed + through the reverse proxy via the the URL prefix '/james', whereas the + reverse proxy forwards those requests to the application at the prefix '/'. + + The reverse proxy, being an entirely separate web server, has no way of + specifying the SCRIPT_NAME variable; it must be manually set by a + PrefixMiddleware instance. Without setting SCRIPT_NAME, url_for will + generate URLs such as: '/purchase_orders/1', when it should be + generating: '/james/purchase_orders/1'. + + To filter your application through a PrefixMiddleware instance, add the + following to the '[app:main]' section of your .ini file: + + .. code-block:: ini + + filter-with = proxy-prefix + + [filter:proxy-prefix] + use = egg:PasteDeploy#prefix + prefix = /james + + The name ``proxy-prefix`` simply acts as an identifier of the filter + section; feel free to rename it. + + Also, unless disabled, the ``X-Forwarded-Server`` header will be + translated to the ``Host`` header, for cases when that header is + lost in the proxying. Also ``X-Forwarded-Host``, + ``X-Forwarded-Scheme``, and ``X-Forwarded-Proto`` are translated. + + If ``force_port`` is set, SERVER_PORT and HTTP_HOST will be + rewritten with the given port. You can use a number, string (like + '80') or the empty string (whatever is the default port for the + scheme). This is useful in situations where there is port + forwarding going on, and the server believes itself to be on a + different port than what the outside world sees. + + You can also use ``scheme`` to explicitly set the scheme (like + ``scheme = https``). + """ + def __init__(self, app, global_conf=None, prefix='/', + translate_forwarded_server=True, + force_port=None, scheme=None): + self.app = app + self.prefix = prefix.rstrip('/') + self.translate_forwarded_server = translate_forwarded_server + self.regprefix = re.compile("^%s(.*)$" % self.prefix) + self.force_port = force_port + self.scheme = scheme + + def __call__(self, environ, start_response): + url = environ['PATH_INFO'] + url = re.sub(self.regprefix, r'\1', url) + if not url: + url = '/' + environ['PATH_INFO'] = url + environ['SCRIPT_NAME'] = self.prefix + if self.translate_forwarded_server: + if 'HTTP_X_FORWARDED_SERVER' in environ: + environ['SERVER_NAME'] = environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER').split(',')[0] + if 'HTTP_X_FORWARDED_HOST' in environ: + environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST').split(',')[0] + if 'HTTP_X_FORWARDED_FOR' in environ: + environ['REMOTE_ADDR'] = environ.pop('HTTP_X_FORWARDED_FOR').split(',')[0] + if 'HTTP_X_FORWARDED_SCHEME' in environ: + environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_SCHEME') + elif 'HTTP_X_FORWARDED_PROTO' in environ: + environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_PROTO') + if self.force_port is not None: + host = environ.get('HTTP_HOST', '').split(':', 1)[0] + if self.force_port: + host = f'{host}:{self.force_port}' + environ['SERVER_PORT'] = str(self.force_port) + else: + if environ['wsgi.url_scheme'] == 'http': + port = '80' + else: + port = '443' + environ['SERVER_PORT'] = port + environ['HTTP_HOST'] = host + if self.scheme is not None: + environ['wsgi.url_scheme'] = self.scheme + return self.app(environ, start_response) + + +def make_prefix_middleware( + app, global_conf, prefix='/', + translate_forwarded_server=True, + force_port=None, scheme=None): + from paste.deploy.converters import asbool + translate_forwarded_server = asbool(translate_forwarded_server) + return PrefixMiddleware( + app, prefix=prefix, + translate_forwarded_server=translate_forwarded_server, + force_port=force_port, scheme=scheme) + +make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ diff --git a/src/paste/deploy/converters.py b/src/paste/deploy/converters.py new file mode 100644 index 0000000..30a3290 --- /dev/null +++ b/src/paste/deploy/converters.py @@ -0,0 +1,37 @@ +# (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 +truthy = frozenset(['true', 'yes', 'on', 'y', 't', '1']) +falsy = frozenset(['false', 'no', 'off', 'n', 'f', '0']) + + +def asbool(obj): + if isinstance(obj, str): + obj = obj.strip().lower() + if obj in truthy: + return True + elif obj in falsy: + return False + else: + raise ValueError("String is not true/false: %r" % obj) + return bool(obj) + + +def asint(obj): + try: + return int(obj) + except (TypeError, ValueError): + raise ValueError("Bad integer value: %r" % obj) + + +def aslist(obj, sep=None, strip=True): + if isinstance(obj, str): + lst = obj.split(sep) + if strip: + lst = [v.strip() for v in lst] + return lst + elif isinstance(obj, (list, tuple)): + return obj + elif obj is None: + return [] + else: + return [obj] diff --git a/src/paste/deploy/loadwsgi.py b/src/paste/deploy/loadwsgi.py new file mode 100644 index 0000000..c5471e5 --- /dev/null +++ b/src/paste/deploy/loadwsgi.py @@ -0,0 +1,713 @@ +# (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 pkg_resources +import re +import sys +from urllib.parse import unquote + + +from paste.deploy.util import fix_call, lookup_object + +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + + +############################################################ +## Utility functions +############################################################ + + +def import_string(s): + ep = pkg_resources.EntryPoint.parse("x=" + s) + if hasattr(ep, 'resolve'): + # this is available on setuptools >= 10.2 + return ep.resolve() + else: + # this causes a DeprecationWarning on setuptools >= 11.3 + return ep.load(False) + + +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 %s)" + % (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(): + pkg_resources.require(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( + "Multiple protocols given in section %r: %s" + % (section, 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 = import_string(found_expr) + 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=pkg_resources.get_distribution(self.spec), + entry_point_name=ep_name) + + def find_egg_entry_point(self, object_type, name=None): + """ + Returns the (entry_point, protocol) for the with the given + ``name``. + """ + if name is None: + name = 'main' + possible = [] + for protocol_options in object_type.egg_protocols: + for protocol in protocol_options: + pkg_resources.require(self.spec) + entry = pkg_resources.get_entry_info( + self.spec, + protocol, + name) + if entry is not None: + possible.append((entry.load(), protocol, entry.name)) + break + if not possible: + # Better exception + dist = pkg_resources.get_distribution(self.spec) + raise LookupError( + "Entry point %r not found in egg %r (dir: %s; protocols: %s; " + "entry_points: %s)" + % (name, self.spec, + dist.location, + ', '.join(_flatten(object_type.egg_protocols)), + ', '.join(_flatten([ + list((pkg_resources.get_entry_info(self.spec, prot, name) or {}).keys()) + for prot in protocol_options] or '(no entry points)')))) + 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 diff --git a/src/paste/deploy/paster_templates.py b/src/paste/deploy/paster_templates.py new file mode 100644 index 0000000..edfa97a --- /dev/null +++ b/src/paste/deploy/paster_templates.py @@ -0,0 +1,34 @@ +# (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 +import os + +from paste.script.templates import Template + + +class PasteDeploy(Template): + + _template_dir = 'paster_templates/paste_deploy' + summary = "A web application deployed through paste.deploy" + + egg_plugins = ['PasteDeploy'] + + required_templates = ['PasteScript#basic_package'] + + def post(self, command, output_dir, vars): + for prereq in ['PasteDeploy']: + command.insert_into_file( + os.path.join(output_dir, 'setup.py'), + 'Extra requirements', + '%r,\n' % prereq, + indent=True) + command.insert_into_file( + os.path.join(output_dir, 'setup.py'), + 'Entry points', + (' [paste.app_factory]\n' + ' main = %(package)s.wsgiapp:make_app\n') % vars, + indent=False) + if command.verbose: + print('*' * 72) + print('* Run "paster serve docs/devel_config.ini" to run the sample application') + print('* on http://localhost:8080') + print('*' * 72) diff --git a/src/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/src/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl new file mode 100644 index 0000000..5514cfc --- /dev/null +++ b/src/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -0,0 +1,23 @@ +import cgi + +from paste.deploy.config import CONFIG + + +def application(environ, start_response): + # Note that usually you wouldn't be writing a pure WSGI + # application, you might be using some framework or + # environment. But as an example... + start_response('200 OK', [('Content-type', 'text/html')]) + greeting = CONFIG['greeting'] + content = [ + b'<html><head><title>%s</title></head>\n' % greeting.encode('utf-8'), + b'<body><h1>%s!</h1>\n' % greeting.encode('utf-8'), + b'<table border=1>\n', + ] + items = environ.items() + items = sorted(items) + for key, value in items: + content.append(b'<tr><td>%s</td><td>%s</td></tr>\n' + % (key.encode('utf-8'), cgi.escape(repr(value)).encode('utf-8'))) + content.append(b'</table></body></html>') + return content diff --git a/src/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl b/src/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl new file mode 100644 index 0000000..5684c31 --- /dev/null +++ b/src/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,25 @@ +from __future__ import absolute_import +from paste.deploy.config import ConfigMiddleware + +from . import sampleapp + + +def make_app( + global_conf, + # Optional and required configuration parameters + # can go here, or just **kw; greeting is required: + greeting, + **kw): + # This is a WSGI application: + app = sampleapp.application + # Here we merge all the keys into one configuration + # dictionary; you don't have to do this, but this + # can be convenient later to add ad hoc configuration: + conf = global_conf.copy() + conf.update(kw) + conf['greeting'] = greeting + # ConfigMiddleware means that paste.deploy.CONFIG will, + # during this request (threadsafe) represent the + # configuration dictionary we set up: + app = ConfigMiddleware(app, conf) + return app diff --git a/src/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl b/src/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl new file mode 100644 index 0000000..0c0ae35 --- /dev/null +++ b/src/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -0,0 +1,22 @@ +[filter-app:main] +# This puts the interactive debugger in place: +use = egg:Paste#evalerror +next = devel + +[app:devel] +# This application is meant for interactive development +use = egg:${project} +debug = true +# You can add other configuration values: +greeting = Aloha! + +[app:test] +# While this version of the configuration is for non-iteractive +# tests (unit tests) +use = devel + +[server:main] +use = egg:Paste#http +# Change to 0.0.0.0 to make public: +host = 127.0.0.1 +port = 8080 diff --git a/src/paste/deploy/util.py b/src/paste/deploy/util.py new file mode 100644 index 0000000..d30466a --- /dev/null +++ b/src/paste/deploy/util.py @@ -0,0 +1,71 @@ +# (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 +import inspect +import sys + + +def fix_type_error(exc_info, callable, varargs, kwargs): + """ + Given an exception, this will test if the exception was due to a + signature error, and annotate the error with better information if + so. + + Usage:: + + try: + val = callable(*args, **kw) + except TypeError: + exc_info = fix_type_error(None, callable, args, kw) + raise exc_info[0], exc_info[1], exc_info[2] + """ + if exc_info is None: + exc_info = sys.exc_info() + if (exc_info[0] != TypeError + or str(exc_info[1]).find('arguments') == -1 + or getattr(exc_info[1], '_type_error_fixed', False)): + return exc_info + exc_info[1]._type_error_fixed = True + argspec = inspect.formatargspec(*inspect.getargspec(callable)) + args = ', '.join(map(_short_repr, varargs)) + if kwargs and args: + args += ', ' + if kwargs: + kwargs = sorted(kwargs.items()) + args += ', '.join(['%s=...' % n for n, v in kwargs]) + gotspec = '(%s)' % args + msg = f'{exc_info[1]}; got {gotspec}, wanted {argspec}' + exc_info[1].args = (msg,) + return exc_info + + +def _short_repr(v): + v = repr(v) + if len(v) > 12: + v = v[:8] + '...' + v[-4:] + return v + + +def fix_call(callable, *args, **kw): + """ + Call ``callable(*args, **kw)`` fixing any type errors that come out. + """ + try: + val = callable(*args, **kw) + except TypeError: + exc_info = fix_type_error(None, callable, args, kw) + raise exc_info[1] from None + return val + + +def lookup_object(spec): + """ + Looks up a module or object from a some.module:func_name specification. + To just look up a module, omit the colon and everything after it. + """ + parts, target = spec.split(':') if ':' in spec else (spec, None) + module = __import__(parts) + + for part in parts.split('.')[1:] + ([target] if target else []): + module = getattr(module, part) + + return module |