From 617bc4c2eba62701de64196e1199c93cc7277ce4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 15 May 2022 20:24:52 -0500 Subject: refactor to a src folder --- .coveragerc | 13 + .gitignore | 3 +- MANIFEST.in | 15 +- docs/news.rst | 5 + paste/__init__.py | 18 - paste/deploy/__init__.py | 3 - paste/deploy/config.py | 305 --------- paste/deploy/converters.py | 37 -- paste/deploy/loadwsgi.py | 713 --------------------- paste/deploy/paster_templates.py | 34 - .../paste_deploy/+package+/sampleapp.py_tmpl | 23 - .../paste_deploy/+package+/wsgiapp.py_tmpl | 25 - .../paste_deploy/docs/devel_config.ini_tmpl | 22 - paste/deploy/util.py | 71 -- pytest.ini | 5 +- setup.cfg | 13 +- src/paste/__init__.py | 18 + src/paste/deploy/__init__.py | 3 + src/paste/deploy/config.py | 305 +++++++++ src/paste/deploy/converters.py | 37 ++ src/paste/deploy/loadwsgi.py | 713 +++++++++++++++++++++ src/paste/deploy/paster_templates.py | 34 + .../paste_deploy/+package+/sampleapp.py_tmpl | 23 + .../paste_deploy/+package+/wsgiapp.py_tmpl | 25 + .../paste_deploy/docs/devel_config.ini_tmpl | 22 + src/paste/deploy/util.py | 71 ++ tox.ini | 57 +- 27 files changed, 1347 insertions(+), 1266 deletions(-) create mode 100644 .coveragerc delete mode 100644 paste/__init__.py delete mode 100644 paste/deploy/__init__.py delete mode 100644 paste/deploy/config.py delete mode 100644 paste/deploy/converters.py delete mode 100644 paste/deploy/loadwsgi.py delete mode 100644 paste/deploy/paster_templates.py delete mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl delete mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl delete mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl delete mode 100644 paste/deploy/util.py create mode 100644 src/paste/__init__.py create mode 100644 src/paste/deploy/__init__.py create mode 100644 src/paste/deploy/config.py create mode 100644 src/paste/deploy/converters.py create mode 100644 src/paste/deploy/loadwsgi.py create mode 100644 src/paste/deploy/paster_templates.py create mode 100644 src/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 src/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl create mode 100644 src/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl create mode 100644 src/paste/deploy/util.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..1dec06f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +source = + paste.deploy + +[paths] +source = + paste/deploy + */paste/deploy + */site-packages/paste/deploy + +[report] +show_missing = true +precision = 2 diff --git a/.gitignore b/.gitignore index 57d7cfa..fd8adfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ .coverage +.coverage.* .tox *.pyc -PasteDeploy.egg-info +src/PasteDeploy.egg-info coverage.xml htmlcov .eggs diff --git a/MANIFEST.in b/MANIFEST.in index 938fb5c..1108a6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,14 +1,21 @@ -graft .github +graft src/paste graft tests -graft paste/deploy/paster_templates +graft docs +graft .github -include docs/*.txt +include setup.cfg +include README.rst +include contributing.md include license.txt +include .coveragerc include pytest.ini include tox.ini include rtd.txt include pyproject.toml -include contributing.md + +prune docs/_build +prune docs/_themes +prune tests/fake_packages/FakeApp.egg/EGG-INFO global-exclude __pycache__ *.py[cod] global-exclude .DS_Store diff --git a/docs/news.rst b/docs/news.rst index b827317..05d8f9f 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -6,6 +6,11 @@ unreleased * Drop support for Python 2, as well as 3.4, 3.5, and 3.6. +* Fix a broken compatibility shim that would cause the ConfigParser to fail + on Python 3.12 when ``ConfigParser.readfp`` is removed. + +* Refactor repository into a src folder layout. + 2.1.1 (2020-10-12) ------------------ diff --git a/paste/__init__.py b/paste/__init__.py deleted file mode 100644 index cdb6121..0000000 --- a/paste/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# (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/paste/deploy/__init__.py b/paste/deploy/__init__.py deleted file mode 100644 index 94c63a8..0000000 --- a/paste/deploy/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# (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/paste/deploy/config.py b/paste/deploy/config.py deleted file mode 100644 index f448350..0000000 --- a/paste/deploy/config.py +++ /dev/null @@ -1,305 +0,0 @@ -# (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/paste/deploy/converters.py b/paste/deploy/converters.py deleted file mode 100644 index 30a3290..0000000 --- a/paste/deploy/converters.py +++ /dev/null @@ -1,37 +0,0 @@ -# (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/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py deleted file mode 100644 index c5471e5..0000000 --- a/paste/deploy/loadwsgi.py +++ /dev/null @@ -1,713 +0,0 @@ -# (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/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py deleted file mode 100644 index edfa97a..0000000 --- a/paste/deploy/paster_templates.py +++ /dev/null @@ -1,34 +0,0 @@ -# (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/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl deleted file mode 100644 index 5514cfc..0000000 --- a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl +++ /dev/null @@ -1,23 +0,0 @@ -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'%s\n' % greeting.encode('utf-8'), - b'

%s!

\n' % greeting.encode('utf-8'), - b'\n', - ] - items = environ.items() - items = sorted(items) - for key, value in items: - content.append(b'\n' - % (key.encode('utf-8'), cgi.escape(repr(value)).encode('utf-8'))) - content.append(b'
%s%s
') - return content diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl deleted file mode 100644 index 5684c31..0000000 --- a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl +++ /dev/null @@ -1,25 +0,0 @@ -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/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl deleted file mode 100644 index 0c0ae35..0000000 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ /dev/null @@ -1,22 +0,0 @@ -[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/paste/deploy/util.py b/paste/deploy/util.py deleted file mode 100644 index d30466a..0000000 --- a/paste/deploy/util.py +++ /dev/null @@ -1,71 +0,0 @@ -# (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 diff --git a/pytest.ini b/pytest.ini index 5ee6477..4b9d6ae 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,5 @@ [pytest] -testpaths = tests +python_files = test_*.py +testpaths = + tests +addopts = -W always --cov diff --git a/setup.cfg b/setup.cfg index 1544721..4d81c86 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,6 +39,8 @@ project_urls = Issue Tracker = https://github.com/Pylons/pastedeploy/issues [options] +package_dir = + =src packages = find: zip_safe = False install_requires = @@ -48,8 +50,7 @@ namespace_packages = paste [options.packages.find] -exclude = - tests +where = src [options.extras_require] config = @@ -58,6 +59,10 @@ paste = docs = Sphinx >= 1.7.5 pylons-sphinx-themes +testing = + Paste + pytest + pytest-cov [options.entry_points] paste.filter_app_factory = @@ -66,3 +71,7 @@ paste.filter_app_factory = paste.paster_create_template = paste_deploy = paste.deploy.paster_templates:PasteDeploy + +[check-manifest] +ignore-bad-ideas = + tests/fake_packages/** 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'%s\n' % greeting.encode('utf-8'), + b'

%s!

\n' % greeting.encode('utf-8'), + b'\n', + ] + items = environ.items() + items = sorted(items) + for key, value in items: + content.append(b'\n' + % (key.encode('utf-8'), cgi.escape(repr(value)).encode('utf-8'))) + content.append(b'
%s%s
') + 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 diff --git a/tox.ini b/tox.ini index 4046d48..8df803b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,63 @@ [tox] envlist = py37,py38,py39,py310,pypy3, + coverage, docs +isolated_build = True [testenv] -deps = - # Paste works on Python 3 since Paste 2.0 - Paste - pytest - pytest-cov commands = - py.test --cov=paste/deploy --cov-report=xml --cov-report=html --cov-report=term-missing {posargs} + python --version + pytest {posargs:} +extras = + testing +setenv = + COVERAGE_FILE=.coverage.{envname} + +[testenv:coverage] +commands = + coverage combine + coverage xml + coverage report --fail-under=100 +deps = + coverage +setenv = + COVERAGE_FILE=.coverage [testenv:docs] whitelist_externals = make commands = - make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E" + make -C docs html epub BUILDDIR={envdir} extras = docs + +[testenv:lint] +skip_install = True +commands = + check-manifest + # build sdist/wheel + python -m build . + twine check dist/* +deps = + build + check-manifest + readme_renderer + twine + +[testenv:build] +skip_install = True +commands = + # clean up build/ and dist/ folders + python -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' + # Make sure we aren't forgetting anything + check-manifest + # build sdist/wheel + python -m build . + # Verify all is well + twine check dist/* + +deps = + build + check-manifest + readme_renderer + twine -- cgit v1.2.1 From d73a3389898cb18f5def996c252c494e6ad1966a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 15 May 2022 20:41:51 -0500 Subject: enable linting --- .github/workflows/ci-tests.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 92231fe..7ada44c 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -70,15 +70,15 @@ jobs: architecture: x64 - run: pip install tox - run: tox -e docs - # lint: - # runs-on: ubuntu-latest - # name: Lint the package - # steps: - # - uses: actions/checkout@v2 - # - name: Setup python - # uses: actions/setup-python@v2 - # with: - # python-version: "3.10" - # architecture: x64 - # - run: pip install tox - # - run: tox -e lint + lint: + runs-on: ubuntu-latest + name: Lint the package + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + architecture: x64 + - run: pip install tox + - run: tox -e lint -- cgit v1.2.1