-- cgit v1.2.1 From 35bff494868d2e179c6bc36af9fef9b64466735e Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 19 Aug 2005 22:10:19 +0000 Subject: Initial layout --- paste/__init__.py | 1 + paste/deploy/__init__.py | 1 + setup.cfg | 3 +++ setup.py | 8 ++++++++ 4 files changed, 13 insertions(+) create mode 100644 paste/__init__.py create mode 100644 paste/deploy/__init__.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/paste/__init__.py @@ -0,0 +1 @@ +# diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/paste/deploy/__init__.py @@ -0,0 +1 @@ +# diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..01bb954 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5b75df7 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages + +setup( + name="Paste-Deploy", + version="0.1", + namespace_packages=['paste'], + packages=find_packages(exclude='tests'), + ) -- cgit v1.2.1 From adb30cc4a6c82fa048702b01c9f8413ce4469677 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 19 Aug 2005 22:52:10 +0000 Subject: Added basic framework for egg-based tests --- tests/conftest.py | 23 ++++++++++++++++++++++ .../FakeApp/FakeApp.egg-info/PKG-INFO | 10 ++++++++++ .../FakeApp/FakeApp.egg-info/top_level.txt | 1 + tests/fake_packages/FakeApp/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp/setup.py | 7 +++++++ tests/test_load_package.py | 10 ++++++++++ 6 files changed, 52 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp/setup.py create mode 100644 tests/test_load_package.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c809711 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +import os +import sys +import glob + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +fake_packages = os.path.join(here, 'fake_packages') + +for egg_info_dir in glob.glob('%s/*/*.egg-info' % fake_packages): + sys.path.append(os.path.dirname(egg_info_dir)) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +sys.path.insert(0, base) +#pkg_resources.require('Paste-Deploy') + +# This is where applications we test go; these applications +# are only used for testing, they aren't "real". +sys.path.append(os.path.join(here, 'fake_packages')) + diff --git a/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp/fakeapp/__init__.py b/tests/fake_packages/FakeApp/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp/setup.py b/tests/fake_packages/FakeApp/setup.py new file mode 100644 index 0000000..4830ab3 --- /dev/null +++ b/tests/fake_packages/FakeApp/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + ) diff --git a/tests/test_load_package.py b/tests/test_load_package.py new file mode 100644 index 0000000..a66b7d0 --- /dev/null +++ b/tests/test_load_package.py @@ -0,0 +1,10 @@ +import sys, os +import pkg_resources +import site +from pprint import pprint + +def test_load_package(): + print 'Path:' + pprint(sys.path) + print pkg_resources.require('FakeApp') + -- cgit v1.2.1 From 436537a5ab6fb64e623cd971a0b34d672343873e Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 19 Aug 2005 23:13:34 +0000 Subject: Tests, copies, still broken. Broken tests hurray\! --- paste/deploy/loadwsgi.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++ paste/deploy/pipeline.py | 18 +++++ paste/deploy/servers.py | 7 ++ paste/deploy/urlmap.py | 12 ++++ 4 files changed, 210 insertions(+) create mode 100644 paste/deploy/loadwsgi.py create mode 100644 paste/deploy/pipeline.py create mode 100644 paste/deploy/servers.py create mode 100644 paste/deploy/urlmap.py diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py new file mode 100644 index 0000000..a52d256 --- /dev/null +++ b/paste/deploy/loadwsgi.py @@ -0,0 +1,173 @@ +import os +from ConfigParser import RawConfigParser +from paste import server +from paste.util import import_string +import pkg_resources + +class ConfigError(Exception): + + def __init__(self, deployment_config, *args): + self.deployment_config = deployment_config + Exception.__init__(self, *args) + +def make_paste_app(context, app_ops): + """ + Create a WSGI application using Paste conventions. app_ops + are options that are fixed for this application (i.e., don't + haveto be configured; things like 'framework' that Paste + treats specially). + """ + ops = app_ops.copy() + ops.update(context.app_config) + return server.make_app(ops) + +############################################################ +## Deployment utility functions +############################################################ + + +class DeploymentConfig(object): + + def __init__(self, filename): + self.filename = filename + parser = RawConfigParser() + parser.read([filename]) + self.data = {} + # @@: DEFAULT? + for section in parser.sections(): + self.data[section] = {} + for key in parser.options(section): + self.data[section][key] = parser.get(section, key) + + def make_app(self, name='main'): + """ + Create a WSGI application with the given name. The factories + are functions that are called like + ``factory(config_context)`` and + return a WSGI application. + """ + factory, context = self.make_factory( + 'application', 'wsgi.app_factory01', name) + return factory(context) + + def make_filter(self, name): + """ + Create a WSGI filter with the given name. No default/'main' + name applies here. + + The factory is called like ``factory(config_context)`` and + returns a function that is called like ``filter(app)`` and + returns a WSGI application. + """ + factory, context = self.make_factory( + 'filter', 'wsgi.filter_factory01', name) + return factory(context) + + def make_server(self, name='main'): + """ + Creates a WSGI server. The server is a factory that is called + like ``factory(options...)``, where all the settings in the + section are turned into keyword arguments (except 'use' and + 'factory' which are special). The server is called with a + single application to serve that application indefinitely. + """ + factory, context = self.make_factory( + 'server', 'wsgi.server_factory00', name) + ops = context.app_config.copy() + for key in 'factory', 'use', 'require': + if key in ops: + del ops[key] + return factory(**ops) + + def make_deployment(self): + """ + From a configuration, return both the server and main app, so + you can do:: + + conf = DeploymentConfig(...) + server, app = conf.make_deployment() + server(app) + """ + server = self.make_server() + app = self.make_app() + return server, app + + def make_factory(self, type, entry_point_type, name): + if name.startswith('file:'): + filename, app_name = self.split_filename(name[5:]) + filename = self.normalize_filename(filename) + deploy = self.__class__(filename) + return deploy.make_factory(type, entry_point_type, app_name) + section = self.find_match(type, name, self.data.keys()) + factory = self.make_factory_from_section( + type, entry_point_type, section) + context = self.make_context_from_section(section) + return factory, context + + def make_factory_from_section(self, type, entry_point_type, section): + conf = self.data[section] + if 'require' in conf: + for spec in conf['require'].split(): + pkg_resources.require(spec) + if 'config' in conf: + filename = conf['config'] + filename, app_name = self.split_filename(filename) + filename = self.normalize_filename(filename) + deploy = self.__class__(filename) + return deploy.make_factory( + type, entry_point_type, app_name) + if 'factory' in conf: + return import_string.eval_import(conf['factory']) + if 'use' in conf: + spec, name = conf['use'].split() + return pkg_resources.load_entry_point( + spec, entry_point_type, name) + raise ConfigError( + self, "No way to create a factory from section [%s] " + "(no factory or use key)" % section) + + def split_filename(self, filename): + """ + Given a filename with an optional :appname part, return + the ``(filename, app_name)`` + """ + # Make sure we don't catch Windows drives: + if filename.find(':') > 1: + new_f, app_name = filename[2:].split(':') + return filename[:2] + new_f, app_name + else: + return filename, 'main' + + def normalize_filename(self, filename): + return os.path.join(os.path.dirname(self.filename), filename) + + def find_match(self, type, name, possible): + type = type.lower() + for section in possible: + section = section.strip() + if section.lower() == type and (not name or name == 'main'): + return section + if not section.lower().startswith(type + ':'): + continue + section_name = section[len(type)+1:].strip() + if section_name == name: + return section + raise ConfigError( + self, "No section like [%s: %s] found" % (type, name)) + + def make_context_from_section(self, section): + conf = self.data[section] + return ConfigContext(self, conf) + +class ConfigContext(object): + + def __init__(self, deployment_config, app_config): + self.deployment_config = deployment_config + self.app_config = app_config + +if __name__ == '__main__': + import sys + conf = DeploymentConfig(sys.argv[1]) + server, app = conf.make_deployment() + server(app) + diff --git a/paste/deploy/pipeline.py b/paste/deploy/pipeline.py new file mode 100644 index 0000000..c33a23a --- /dev/null +++ b/paste/deploy/pipeline.py @@ -0,0 +1,18 @@ +from paste import wsgi_deploy + +def make_pipeline(context): + pipeline = filter( + None, context.app_config.get('pipeline', '').split()) + if not pipeline: + raise IndexError( + "You must give a pipeline setting") + filters = pipeline[:-1] + filters.reverse() + app_name = pipeline[-1] + deploy = context.deployment_config + app = deploy.make_app(app_name) + for filter_name in filters: + wsgi_filter = deploy.make_filter(filter_name) + app = wsgi_filter(app) + return app + diff --git a/paste/deploy/servers.py b/paste/deploy/servers.py new file mode 100644 index 0000000..2230c16 --- /dev/null +++ b/paste/deploy/servers.py @@ -0,0 +1,7 @@ +def make_wsgiutils(host, port): + from wsgiutils import wsgiServer + def server(app): + server = wsgiServer.WSGIServer( + (host, int(port)), {'': app}) + server.serve_forever() + return server diff --git a/paste/deploy/urlmap.py b/paste/deploy/urlmap.py new file mode 100644 index 0000000..26525b2 --- /dev/null +++ b/paste/deploy/urlmap.py @@ -0,0 +1,12 @@ +from paste import wsgi_deploy +from paste import urlmap + +def make_urlmap(context): + mapper = urlmap.URLMap() + for key, value in context.app_config.items(): + if (key.startswith('/') or key.startswith('http://') + or key.startswith('https://')): + app = context.deployment_config.make_app(value) + mapper[key] = app + return mapper + -- cgit v1.2.1 From 895335bbfd0698479443afda8ee176b31c57c529 Mon Sep 17 00:00:00 2001 From: ianb Date: Sat, 20 Aug 2005 03:39:36 +0000 Subject: Rearranged so the package is also a normal egg --- tests/conftest.py | 5 +---- tests/fake_packages/FakeApp.egg/EGG-INFO | 1 + tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO | 10 ++++++++++ tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt | 1 + tests/fake_packages/FakeApp.egg/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp.egg/setup.py | 7 +++++++ tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO | 10 ---------- tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt | 1 - tests/fake_packages/FakeApp/fakeapp/__init__.py | 1 - tests/fake_packages/FakeApp/setup.py | 7 ------- 10 files changed, 21 insertions(+), 23 deletions(-) create mode 120000 tests/fake_packages/FakeApp.egg/EGG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/setup.py delete mode 100644 tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO delete mode 100644 tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt delete mode 100644 tests/fake_packages/FakeApp/fakeapp/__init__.py delete mode 100644 tests/fake_packages/FakeApp/setup.py diff --git a/tests/conftest.py b/tests/conftest.py index c809711..0cc7d9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,10 @@ import os import sys -import glob here = os.path.dirname(__file__) base = os.path.dirname(here) fake_packages = os.path.join(here, 'fake_packages') - -for egg_info_dir in glob.glob('%s/*/*.egg-info' % fake_packages): - sys.path.append(os.path.dirname(egg_info_dir)) +sys.path.append(fake_packages) # We can only import this after we adjust the paths import pkg_resources diff --git a/tests/fake_packages/FakeApp.egg/EGG-INFO b/tests/fake_packages/FakeApp.egg/EGG-INFO new file mode 120000 index 0000000..1942929 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/EGG-INFO @@ -0,0 +1 @@ +FakeApp.egg-info \ No newline at end of file diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py new file mode 100644 index 0000000..4830ab3 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + ) diff --git a/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO deleted file mode 100644 index a2a1137..0000000 --- a/tests/fake_packages/FakeApp/FakeApp.egg-info/PKG-INFO +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 1.0 -Name: FakeApp -Version: 1.0 -Summary: UNKNOWN -Home-page: UNKNOWN -Author: UNKNOWN -Author-email: UNKNOWN -License: UNKNOWN -Description: UNKNOWN -Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt deleted file mode 100644 index 79ed67a..0000000 --- a/tests/fake_packages/FakeApp/FakeApp.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -fakeapp diff --git a/tests/fake_packages/FakeApp/fakeapp/__init__.py b/tests/fake_packages/FakeApp/fakeapp/__init__.py deleted file mode 100644 index 792d600..0000000 --- a/tests/fake_packages/FakeApp/fakeapp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/fake_packages/FakeApp/setup.py b/tests/fake_packages/FakeApp/setup.py deleted file mode 100644 index 4830ab3..0000000 --- a/tests/fake_packages/FakeApp/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="FakeApp", - version="1.0", - packages=find_packages(), - ) -- cgit v1.2.1 From e5bca9f16a383866e23e59edc5f694c1ad31dd8d Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 00:05:58 +0000 Subject: Initial run at tests --- tests/fixture.py | 8 ++++++++ tests/sample_configs/basic_app.ini | 5 +++++ tests/test_basic_app.py | 12 ++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 tests/fixture.py create mode 100644 tests/sample_configs/basic_app.ini create mode 100644 tests/test_basic_app.py diff --git a/tests/fixture.py b/tests/fixture.py new file mode 100644 index 0000000..a0f62e7 --- /dev/null +++ b/tests/fixture.py @@ -0,0 +1,8 @@ +import os +from paste.deploy import load + +def conf_fn(filename): + return os.path.join(os.path.dirname(__file__), 'sample_configs', filename) + +def loadtest(base_filename, *args **kw): + return load(conf_fn(base_filename), *args, **kw) diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini new file mode 100644 index 0000000..21bf5a2 --- /dev/null +++ b/tests/sample_configs/basic_app.ini @@ -0,0 +1,5 @@ +[application:main] +use = FakeApp basic_app + +[application:other] +use = FakeApp basic_app2 diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py new file mode 100644 index 0000000..170be42 --- /dev/null +++ b/tests/test_basic_app.py @@ -0,0 +1,12 @@ +from paste.deploy import loadwsgi +from fixture import * +import fakeapp.apps + +def test_main(): + app = loadtest('basic_app.ini') + assert app is fakeapp.apps.basic_app + +def test_other(): + app = loadtest('basic_app.ini', name='other') + assert app is fakeapp.apps.basic_app2 + -- cgit v1.2.1 From ca0372abd9c42e53cb132998e1fa7e5c805fda0f Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 19:47:22 +0000 Subject: Several typos --- .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 6 ++++++ tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 15 +++++++++++++++ tests/fake_packages/FakeApp.egg/setup.py | 6 ++++++ 3 files changed, 27 insertions(+) create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/apps.py diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt new file mode 100644 index 0000000..eede700 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -0,0 +1,6 @@ +[paste.app_factory1] + + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py new file mode 100644 index 0000000..1aefa4e --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -0,0 +1,15 @@ +def simple_app(response, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return [response] + +def basic_app(environ, start_response): + return simple_app('basic app', environ, start_response) + +def make_basic_app(global_conf, **conf): + return basic_app + +def basic_app2(environ, start_response): + return simple_app('basic app2', environ, start_response) + +def make_basic_app2(global_conf, **conf): + return basic_app2 diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index 4830ab3..a8b6856 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -4,4 +4,10 @@ setup( name="FakeApp", version="1.0", packages=find_packages(), + entry_points={ + 'paste.app_factory1': """ + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + """, + }, ) -- cgit v1.2.1 From a7d33ba0d32a01bd7201fca94f895fafb4d539fd Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 19:48:13 +0000 Subject: First tests, first part of functionality --- paste/deploy/__init__.py | 3 +- paste/deploy/loadwsgi.py | 453 ++++++++++++++++++++++++------------- tests/fixture.py | 8 +- tests/sample_configs/basic_app.ini | 4 +- tests/test_basic_app.py | 20 +- 5 files changed, 320 insertions(+), 168 deletions(-) diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index 792d600..5c58ac1 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1 +1,2 @@ -# +from loadwsgi import loadapp, loadfilter, loadserver + diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index a52d256..05e3d79 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,173 +1,316 @@ import os +import urllib from ConfigParser import RawConfigParser -from paste import server -from paste.util import import_string import pkg_resources -class ConfigError(Exception): +############################################################ +## Object types +############################################################ + +class _ObjectType(object): + + def __init__(self, name, egg_protocols, config_prefixes): + self.name = name + self.egg_protocols = map(_aslist, _aslist(egg_protocols)) + self.config_prefixes = map(_aslist, _aslist(config_prefixes)) + + def __repr__(self): + return '<%s protocols=%r prefixes=%r>' % ( + self.name, self.egg_protocols, self.config_prefixees) + +def _aslist(obj): + if obj is None: + return [] + elif isinstance(obj, (list, tuple)): + return obj + else: + return [obj] + +def _flatten(lst): + if not isinstance(lst, (list, tuple)): + return [lst] + result = [] + for item in lst: + result.extend(_flatten(item)) + return result + +APP = _ObjectType( + 'application', + ['paste.app_factory1', 'paste.composit_factory1'], + [['app', 'application'], 'composit']) + +def APP_invoke(context): + if context.protocol == 'paste.composit_factory1': + return context.object(context.loader, context.global_conf, + **context.local_conf) + elif context.protocol == 'paste.app_factory1': + return context.object(context.global_conf, **context.local_conf) + else: + assert 0, "Protocol %r unknown" % context.protocol + +APP.invoke = APP_invoke + +FILTER = _ObjectType( + 'filter', + ['paste.filter_factory1'], + ['filter']) + +def FILTER_invoke(context): + assert context.protocol == 'paste.filter_factory1' + return context.object(context.global_conf, **context.local_conf) + +FILTER.invoke = FILTER_invoke - def __init__(self, deployment_config, *args): - self.deployment_config = deployment_config - Exception.__init__(self, *args) +SERVER = _ObjectType( + 'server', + ['paste.server_factory1'], + ['server']) -def make_paste_app(context, app_ops): +def SERVER_invoke(context): + assert context.protocol == 'paste.server_factory1' + return context.object(context.global_conf, **context.local_conf) + +SERVER.invoke = SERVER_invoke + +def import_string(s): + return pkg_resources.EntryPoint.parse("x="+s).load(False) + +############################################################ +## Locators +############################################################ + +def find_egg_entry_point(object_type, egg_spec, name=None): """ - Create a WSGI application using Paste conventions. app_ops - are options that are fixed for this application (i.e., don't - haveto be configured; things like 'framework' that Paste - treats specially). + Returns the (entry_point, protocol) for the with the given + ``name`` and specification ``egg_spec``. """ - ops = app_ops.copy() - ops.update(context.app_config) - return server.make_app(ops) + if name is None: + name = 'main' + possible = [] + for protocol_options in object_type.egg_protocols: + for protocol in protocol_options: + entry = pkg_resources.get_entry_info( + egg_spec, + protocol, + name) + if entry is not None: + possible.append((entry.load(), protocol)) + break + if not possible: + # Better exception + print pkg_resources.get_entry_map(egg_spec) + raise LookupError( + "Entry point %r not found in egg %r (protocols: %s)" + % (name, egg_spec, + ', '.join(_flatten(object_type.egg_protocols)))) + if len(possible) > 1: + raise LookupError( + "Ambiguous entry points for %r in egg %r (protocols: %s)" + % (name, egg_spec, ', '.join(_flatten(protocol_list)))) + return possible[0] + +def find_config_section(object_type, config_sections, + 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 = _find_sections(config_sections, name_prefix, name) + if found: + possible.extend(found) + break + if not possible: + raise LookupError( + "No section %r (prefixed by %r) found in config" + % (name, _flatten(name_prefix_list))) + if len(possible) > 1: + raise LookupError( + "Ambiguous section names %r for section %r (prefixed by %r) " + "found in config" + % (possible, name, _flatten(name_prefix_list))) + return possible[0] + +def _find_sections(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 ############################################################ -## Deployment utility functions +## 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) -class DeploymentConfig(object): +def loadserver(uri, name=None, **kw): + return loadobj(SERVER, uri, name=name, **kw) + +_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 object_type.invoke(context) + +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] + 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): + # De-Windowsify the paths: + path = path.replace('\\', '/') + if not path.startswith('/'): + if not relative_to: + raise ValueError( + "Cannot resolve relative uri %r; no context 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 = urllib.unquote(path) + loader = ConfigLoader(path) + return loader.get(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(object_type, name, global_conf) + +_loaders['egg'] = _loadegg + +############################################################ +## Loaders +############################################################ + +class _Loader(object): + + def getapp(self, name=None, global_conf=None): + return self.get(APP, name=name, global_conf=global_conf) + + def getfilter(self, name=None, global_conf=None): + return self.get(FILTER, name=name, global_conf=global_conf) + + def getserver(self, name=None, global_conf=None): + return self.get(SERVER, name=name, global_conf=global_conf) + +class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename - parser = RawConfigParser() - parser.read([filename]) - self.data = {} - # @@: DEFAULT? - for section in parser.sections(): - self.data[section] = {} - for key in parser.options(section): - self.data[section][key] = parser.get(section, key) - - def make_app(self, name='main'): - """ - Create a WSGI application with the given name. The factories - are functions that are called like - ``factory(config_context)`` and - return a WSGI application. - """ - factory, context = self.make_factory( - 'application', 'wsgi.app_factory01', name) - return factory(context) - - def make_filter(self, name): - """ - Create a WSGI filter with the given name. No default/'main' - name applies here. - - The factory is called like ``factory(config_context)`` and - returns a function that is called like ``filter(app)`` and - returns a WSGI application. - """ - factory, context = self.make_factory( - 'filter', 'wsgi.filter_factory01', name) - return factory(context) - - def make_server(self, name='main'): - """ - Creates a WSGI server. The server is a factory that is called - like ``factory(options...)``, where all the settings in the - section are turned into keyword arguments (except 'use' and - 'factory' which are special). The server is called with a - single application to serve that application indefinitely. - """ - factory, context = self.make_factory( - 'server', 'wsgi.server_factory00', name) - ops = context.app_config.copy() - for key in 'factory', 'use', 'require': - if key in ops: - del ops[key] - return factory(**ops) - - def make_deployment(self): - """ - From a configuration, return both the server and main app, so - you can do:: - - conf = DeploymentConfig(...) - server, app = conf.make_deployment() - server(app) - """ - server = self.make_server() - app = self.make_app() - return server, app - - def make_factory(self, type, entry_point_type, name): - if name.startswith('file:'): - filename, app_name = self.split_filename(name[5:]) - filename = self.normalize_filename(filename) - deploy = self.__class__(filename) - return deploy.make_factory(type, entry_point_type, app_name) - section = self.find_match(type, name, self.data.keys()) - factory = self.make_factory_from_section( - type, entry_point_type, section) - context = self.make_context_from_section(section) - return factory, context - - def make_factory_from_section(self, type, entry_point_type, section): - conf = self.data[section] - if 'require' in conf: - for spec in conf['require'].split(): - pkg_resources.require(spec) - if 'config' in conf: - filename = conf['config'] - filename, app_name = self.split_filename(filename) - filename = self.normalize_filename(filename) - deploy = self.__class__(filename) - return deploy.make_factory( - type, entry_point_type, app_name) - if 'factory' in conf: - return import_string.eval_import(conf['factory']) - if 'use' in conf: - spec, name = conf['use'].split() - return pkg_resources.load_entry_point( - spec, entry_point_type, name) - raise ConfigError( - self, "No way to create a factory from section [%s] " - "(no factory or use key)" % section) - - def split_filename(self, filename): - """ - Given a filename with an optional :appname part, return - the ``(filename, app_name)`` - """ - # Make sure we don't catch Windows drives: - if filename.find(':') > 1: - new_f, app_name = filename[2:].split(':') - return filename[:2] + new_f, app_name + self.parser = RawConfigParser() + # Don't lower-case keys: + self.parser.optionxform = str + self.parser.read(filename) + + def get(self, object_type, name=None, global_conf=None): + if global_conf is None: + global_conf = {} else: - return filename, 'main' - - def normalize_filename(self, filename): - return os.path.join(os.path.dirname(self.filename), filename) - - def find_match(self, type, name, possible): - type = type.lower() - for section in possible: - section = section.strip() - if section.lower() == type and (not name or name == 'main'): - return section - if not section.lower().startswith(type + ':'): - continue - section_name = section[len(type)+1:].strip() - if section_name == name: - return section - raise ConfigError( - self, "No section like [%s: %s] found" % (type, name)) - - def make_context_from_section(self, section): - conf = self.data[section] - return ConfigContext(self, conf) - -class ConfigContext(object): - - def __init__(self, deployment_config, app_config): - self.deployment_config = deployment_config - self.app_config = app_config - -if __name__ == '__main__': - import sys - conf = DeploymentConfig(sys.argv[1]) - server, app = conf.make_deployment() - server(app) + global_conf = global_conf.copy() + section = find_config_section( + object_type, self.parser.sections(), name=name) + global_conf.update(self.parser.defaults()) + local_conf = {} + for option in self.parser.options(section): + if option.startswith('set '): + name = option[4:].strip() + global_conf[name] = self.parser.get(section, option) + else: + local_conf[option] = self.parser.get(section, option) + if 'use' in local_conf: + use = local_conf.pop('use') + context = loadcontext( + object_type, use, + relative_to=os.path.dirname(self.filename)) + context.global_conf.update(global_conf) + context.local_conf.update(local_conf) + # @@: Should loader be overwritten? + context.loader = self + return context + 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) + value = import_string(possible[0][1]) + context = LoaderContext( + value, object_type, possible[0][0], + global_conf, local_conf, self) + return context + +class EggLoader(_Loader): + + def __init__(self, spec): + self.spec = spec + + def get(self, object_type, name=None, global_conf=None): + entry_point, protocol = find_egg_entry_point( + object_type, self.spec, name=name) + return LoaderContext( + entry_point, + object_type, + protocol, + global_conf or {}, {}, + self) + +class LoaderContext(object): + + def __init__(self, obj, object_type, protocol, + global_conf, local_conf, loader): + self.object = obj + self.object_type = object_type + self.protocol = protocol + self.global_conf = global_conf + self.local_conf = local_conf + self.loader = loader + diff --git a/tests/fixture.py b/tests/fixture.py index a0f62e7..47bea2e 100644 --- a/tests/fixture.py +++ b/tests/fixture.py @@ -1,8 +1,2 @@ import os -from paste.deploy import load - -def conf_fn(filename): - return os.path.join(os.path.dirname(__file__), 'sample_configs', filename) - -def loadtest(base_filename, *args **kw): - return load(conf_fn(base_filename), *args, **kw) +from pkg_resources import * diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini index 21bf5a2..868f00e 100644 --- a/tests/sample_configs/basic_app.ini +++ b/tests/sample_configs/basic_app.ini @@ -1,5 +1,5 @@ [application:main] -use = FakeApp basic_app +use = egg:FakeApp#basic_app [application:other] -use = FakeApp basic_app2 +use = egg:FakeApp#other diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py index 170be42..2ba470c 100644 --- a/tests/test_basic_app.py +++ b/tests/test_basic_app.py @@ -1,12 +1,26 @@ -from paste.deploy import loadwsgi +from paste.deploy import loadapp, loadfilter from fixture import * +require('FakeApp') import fakeapp.apps +here = os.path.dirname(__file__) + def test_main(): - app = loadtest('basic_app.ini') + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#ignored', + relative_to=here, name='main') assert app is fakeapp.apps.basic_app def test_other(): - app = loadtest('basic_app.ini', name='other') + app = loadapp('config:sample_configs/basic_app.ini#other', + relative_to=here) assert app is fakeapp.apps.basic_app2 -- cgit v1.2.1 From eb09ede6e4ccd941466e2263782d41590ef7f8b9 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 19:55:03 +0000 Subject: Added description; added requirement of Paste for the composit applications, which are frontends to existing Paste applications --- setup.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/setup.py b/setup.py index 5b75df7..5450b30 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,26 @@ from setuptools import setup, find_packages setup( name="Paste-Deploy", version="0.1", + description="Load, configure, and compose WSGI applications and servers", + long_description="""\ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. +""", + classifiers=["Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Python Software Foundation License", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='web wsgi application server', + author="Ian Bicking", + author_email="ianb@colorstudy.com", + url="http://pythonpaste.org", namespace_packages=['paste'], + extras_require={'composit': ['Paste']}, packages=find_packages(exclude='tests'), + zip_safe=True, ) -- cgit v1.2.1 From 2b6ab28169a034539d69fa08536bdbcc217d814f Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 21:36:11 +0000 Subject: New composit application --- .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 5 +++++ tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 23 ++++++++++++++++++++++ tests/fake_packages/FakeApp.egg/setup.py | 3 +++ 3 files changed, 31 insertions(+) diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt index eede700..fa19625 100644 --- a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -4,3 +4,8 @@ other=fakeapp.apps:make_basic_app2 +[paste.composit_factory1] + + remote_addr=fakeapp.apps:make_remote_addr + + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py index 1aefa4e..f70f39a 100644 --- a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -13,3 +13,26 @@ def basic_app2(environ, start_response): def make_basic_app2(global_conf, **conf): return basic_app2 + +def make_remote_addr(loader, global_conf, **conf): + apps = {} + addrs = {} + for name, value in conf.items(): + if name.startswith('app.'): + apps[name[4:]] = loader.get_app(value, global_conf) + elif name.startswith('addr.'): + addrs[name[5:]] = value + dispatcher = RemoteAddrDispatch() + for name in apps: + dispatcher.map[addrs[name]] = apps[name] + return dispatcher + +class RemoteAddrDispatch(object): + def __init__(self, map=None): + self.map = map or {} + + def __call__(self, environ, start_response): + addr = environ['REMOTE_ADDR'] + app = self.map.get(addr) or self.map['0.0.0.0'] + return app(environ, start_response) + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index a8b6856..b161249 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -9,5 +9,8 @@ setup( basic_app=fakeapp.apps:make_basic_app other=fakeapp.apps:make_basic_app2 """, + 'paste.composit_factory1': """ + remote_addr=fakeapp.apps:make_remote_addr + """, }, ) -- cgit v1.2.1 From 1346a6221cf0584b73cc3b03eeb2b3b346953e2c Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 21:36:24 +0000 Subject: Tests of composit loading --- paste/deploy/loadwsgi.py | 44 ++++++++++++++++++++++++++------------ tests/sample_configs/basic_app.ini | 9 ++++++++ tests/test_basic_app.py | 9 ++++++++ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 05e3d79..b9fc2b0 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -129,12 +129,12 @@ def find_config_section(object_type, config_sections, if not possible: raise LookupError( "No section %r (prefixed by %r) found in config" - % (name, _flatten(name_prefix_list))) + % (name, _flatten(object_type.config_prefixes))) if len(possible) > 1: raise LookupError( "Ambiguous section names %r for section %r (prefixed by %r) " "found in config" - % (possible, name, _flatten(name_prefix_list))) + % (possible, name, _flatten(object_type.config_prefixes))) return possible[0] def _find_sections(sections, name_prefix, name): @@ -169,7 +169,7 @@ def loadobj(object_type, uri, name=None, relative_to=None, context = loadcontext( object_type, uri, name=name, relative_to=relative_to, global_conf=global_conf) - return object_type.invoke(context) + return context.create() def loadcontext(object_type, uri, name=None, relative_to=None, global_conf=None): @@ -208,14 +208,14 @@ def _loadconfig(object_type, uri, path, name, relative_to, path = path[2:] path = urllib.unquote(path) loader = ConfigLoader(path) - return loader.get(object_type, name, global_conf) + 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(object_type, name, global_conf) + return loader.get_context(object_type, name, global_conf) _loaders['egg'] = _loadegg @@ -225,14 +225,29 @@ _loaders['egg'] = _loadegg class _Loader(object): - def getapp(self, name=None, global_conf=None): - return self.get(APP, name=name, global_conf=global_conf) + def get_app(self, name=None, global_conf=None): + return self.app_context( + name=name, global_conf=global_conf).create() - def getfilter(self, name=None, global_conf=None): - return self.get(FILTER, name=name, global_conf=global_conf) + def get_filter(self, name=None, global_conf=None): + return self.filter_context( + name=naame, global_conf=global_conf).create() - def getserver(self, name=None, global_conf=None): - return self.get(SERVER, name=name, global_conf=global_conf) + def get_server(self, name=None, global_conf=None): + return self.server_context( + name=naame, 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) class ConfigLoader(_Loader): @@ -243,7 +258,7 @@ class ConfigLoader(_Loader): self.parser.optionxform = str self.parser.read(filename) - def get(self, object_type, name=None, global_conf=None): + def get_context(self, object_type, name=None, global_conf=None): if global_conf is None: global_conf = {} else: @@ -292,7 +307,7 @@ class EggLoader(_Loader): def __init__(self, spec): self.spec = spec - def get(self, object_type, name=None, global_conf=None): + def get_context(self, object_type, name=None, global_conf=None): entry_point, protocol = find_egg_entry_point( object_type, self.spec, name=name) return LoaderContext( @@ -313,4 +328,5 @@ class LoaderContext(object): self.local_conf = local_conf self.loader = loader - + def create(self): + return self.object_type.invoke(self) diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini index 868f00e..f1d931c 100644 --- a/tests/sample_configs/basic_app.ini +++ b/tests/sample_configs/basic_app.ini @@ -3,3 +3,12 @@ use = egg:FakeApp#basic_app [application:other] use = egg:FakeApp#other + +[composit:remote_addr] +use = egg:FakeApp#remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 + diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py index 2ba470c..92405ed 100644 --- a/tests/test_basic_app.py +++ b/tests/test_basic_app.py @@ -24,3 +24,12 @@ def test_other(): relative_to=here) assert app is fakeapp.apps.basic_app2 + +def test_composit(): + app = loadapp('config:sample_configs/basic_app.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 + + -- cgit v1.2.1 From 78a1da3b1835e40d3b6be9113f9c8b4e3c963b3d Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 23:04:12 +0000 Subject: Added tests for configuration and loading; allow name references to be used in addition to URIs when context is implied; reorganized loadwsgi.py some --- paste/deploy/loadwsgi.py | 258 ++++++++++++--------- .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 1 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 9 +- tests/fake_packages/FakeApp.egg/setup.py | 1 + tests/fixture.py | 1 + tests/sample_configs/test_config.ini | 26 +++ tests/sample_configs/test_config_included.ini | 10 + tests/test_basic_app.py | 1 - tests/test_config.py | 55 +++++ 9 files changed, 248 insertions(+), 114 deletions(-) create mode 100644 tests/sample_configs/test_config.ini create mode 100644 tests/sample_configs/test_config_included.ini create mode 100644 tests/test_config.py diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index b9fc2b0..12e9c5a 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,24 +1,23 @@ import os +import re import urllib from ConfigParser import RawConfigParser import pkg_resources +__all__ = ['loadapp', 'loadserver', 'loadfilter'] + ############################################################ -## Object types +## Utility functions ############################################################ -class _ObjectType(object): - - def __init__(self, name, egg_protocols, config_prefixes): - self.name = name - self.egg_protocols = map(_aslist, _aslist(egg_protocols)) - self.config_prefixes = map(_aslist, _aslist(config_prefixes)) - - def __repr__(self): - return '<%s protocols=%r prefixes=%r>' % ( - self.name, self.egg_protocols, self.config_prefixees) +def import_string(s): + return pkg_resources.EntryPoint.parse("x="+s).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)): @@ -27,6 +26,9 @@ def _aslist(obj): return [obj] def _flatten(lst): + """ + Flatten a nested list. + """ if not isinstance(lst, (list, tuple)): return [lst] result = [] @@ -34,6 +36,25 @@ def _flatten(lst): result.extend(_flatten(item)) return result +############################################################ +## Object types +############################################################ + +class _ObjectType(object): + + def __init__(self, name, egg_protocols, config_prefixes): + self.name = name + self.egg_protocols = map(_aslist, _aslist(egg_protocols)) + self.config_prefixes = map(_aslist, _aslist(config_prefixes)) + + def __repr__(self): + return '<%s protocols=%r prefixes=%r>' % ( + self.name, self.egg_protocols, self.config_prefixees) + + def invoke(self, context): + assert context.protocol in _flatten(self.egg_protocols) + return context.object(context.global_conf, **context.local_conf) + APP = _ObjectType( 'application', ['paste.app_factory1', 'paste.composit_factory1'], @@ -55,100 +76,11 @@ FILTER = _ObjectType( ['paste.filter_factory1'], ['filter']) -def FILTER_invoke(context): - assert context.protocol == 'paste.filter_factory1' - return context.object(context.global_conf, **context.local_conf) - -FILTER.invoke = FILTER_invoke - SERVER = _ObjectType( 'server', ['paste.server_factory1'], ['server']) -def SERVER_invoke(context): - assert context.protocol == 'paste.server_factory1' - return context.object(context.global_conf, **context.local_conf) - -SERVER.invoke = SERVER_invoke - -def import_string(s): - return pkg_resources.EntryPoint.parse("x="+s).load(False) - -############################################################ -## Locators -############################################################ - -def find_egg_entry_point(object_type, egg_spec, name=None): - """ - Returns the (entry_point, protocol) for the with the given - ``name`` and specification ``egg_spec``. - """ - if name is None: - name = 'main' - possible = [] - for protocol_options in object_type.egg_protocols: - for protocol in protocol_options: - entry = pkg_resources.get_entry_info( - egg_spec, - protocol, - name) - if entry is not None: - possible.append((entry.load(), protocol)) - break - if not possible: - # Better exception - print pkg_resources.get_entry_map(egg_spec) - raise LookupError( - "Entry point %r not found in egg %r (protocols: %s)" - % (name, egg_spec, - ', '.join(_flatten(object_type.egg_protocols)))) - if len(possible) > 1: - raise LookupError( - "Ambiguous entry points for %r in egg %r (protocols: %s)" - % (name, egg_spec, ', '.join(_flatten(protocol_list)))) - return possible[0] - -def find_config_section(object_type, config_sections, - 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 = _find_sections(config_sections, name_prefix, name) - if found: - possible.extend(found) - break - if not possible: - raise LookupError( - "No section %r (prefixed by %r) found in config" - % (name, _flatten(object_type.config_prefixes))) - if len(possible) > 1: - raise LookupError( - "Ambiguous section names %r for section %r (prefixed by %r) " - "found in config" - % (possible, name, _flatten(object_type.config_prefixes))) - return possible[0] - -def _find_sections(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 - ############################################################ ## Loaders ############################################################ @@ -249,6 +181,15 @@ class _Loader(object): 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): @@ -256,29 +197,43 @@ class ConfigLoader(_Loader): self.parser = RawConfigParser() # Don't lower-case keys: self.parser.optionxform = str + # Stupid ConfigParser ignores files that aren't found, so + # we have to add an extra check: + if not os.path.exists(filename): + raise OSError( + "File %s not found" % filename) self.parser.read(filename) 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) if global_conf is None: global_conf = {} else: global_conf = global_conf.copy() - section = find_config_section( - object_type, self.parser.sections(), name=name) - global_conf.update(self.parser.defaults()) + section = self.find_config_section( + object_type, name=name) + defaults = self.parser.defaults() + global_conf.update(defaults) local_conf = {} + global_additions = {} for option in self.parser.options(section): if option.startswith('set '): name = option[4:].strip() - global_conf[name] = self.parser.get(section, option) + global_additions[name] = global_conf[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) if 'use' in local_conf: use = local_conf.pop('use') - context = loadcontext( - object_type, use, - relative_to=os.path.dirname(self.filename)) - context.global_conf.update(global_conf) + context = self.get_context( + object_type, name=use, global_conf=global_conf) + context.global_conf.update(global_additions) context.local_conf.update(local_conf) # @@: Should loader be overwritten? context.loader = self @@ -296,20 +251,70 @@ class ConfigLoader(_Loader): if not possible: raise LookupError( "No loader given in section %r" % section) - value = import_string(possible[0][1]) + found_protocol, found_expr = possible[0] + del local_conf[found_protocol] + value = import_string(found_expr) context = LoaderContext( - value, object_type, possible[0][0], + value, object_type, found_protocol, global_conf, local_conf, self) 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 from %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): - entry_point, protocol = find_egg_entry_point( - object_type, self.spec, name=name) + if self.absolute_name(name): + return loadcontext(object_type, name, + global_conf=global_conf) + entry_point, protocol = self.find_egg_entry_point( + object_type, name=name) return LoaderContext( entry_point, object_type, @@ -317,6 +322,35 @@ class EggLoader(_Loader): global_conf or {}, {}, self) + 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: + entry = pkg_resources.get_entry_info( + self.spec, + protocol, + name) + if entry is not None: + possible.append((entry.load(), protocol)) + break + if not possible: + # Better exception + raise LookupError( + "Entry point %r not found in egg %r (protocols: %s)" + % (name, self.spec, + ', '.join(_flatten(object_type.egg_protocols)))) + if len(possible) > 1: + raise LookupError( + "Ambiguous entry points for %r in egg %r (protocols: %s)" + % (name, self.spec, ', '.join(_flatten(protocol_list)))) + return possible[0] + class LoaderContext(object): def __init__(self, obj, object_type, protocol, diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt index fa19625..901a406 100644 --- a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -2,6 +2,7 @@ basic_app=fakeapp.apps:make_basic_app other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app [paste.composit_factory1] diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py index f70f39a..759a9da 100644 --- a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -1,3 +1,7 @@ +############################################################ +## Apps +############################################################ + def simple_app(response, environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) return [response] @@ -14,6 +18,10 @@ def basic_app2(environ, start_response): def make_basic_app2(global_conf, **conf): return basic_app2 +############################################################ +## Composits +############################################################ + def make_remote_addr(loader, global_conf, **conf): apps = {} addrs = {} @@ -35,4 +43,3 @@ class RemoteAddrDispatch(object): addr = environ['REMOTE_ADDR'] app = self.map.get(addr) or self.map['0.0.0.0'] return app(environ, start_response) - diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index b161249..c67b038 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -8,6 +8,7 @@ setup( 'paste.app_factory1': """ basic_app=fakeapp.apps:make_basic_app other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app """, 'paste.composit_factory1': """ remote_addr=fakeapp.apps:make_remote_addr diff --git a/tests/fixture.py b/tests/fixture.py index 47bea2e..ee42c53 100644 --- a/tests/fixture.py +++ b/tests/fixture.py @@ -1,2 +1,3 @@ import os from pkg_resources import * +require('FakeApp') diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini new file mode 100644 index 0000000..85c66a1 --- /dev/null +++ b/tests/sample_configs/test_config.ini @@ -0,0 +1,26 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:test1] +use = egg:FakeApp#configed +setting1 = foo +setting2 = bar + +[app:test2] +use = egg:FakeApp#configed +set def1 = test2 +set another = TEST +local conf = something + +[app:test3] +use = test2 +set def1 = test3 +another = something more + across several + lines + +[app:test_foreign_config] +use = config:test_config_included.ini +set glob = override +another = FOO diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini new file mode 100644 index 0000000..aa24a61 --- /dev/null +++ b/tests/sample_configs/test_config_included.ini @@ -0,0 +1,10 @@ +[DEFAULT] +def2 = from include +def3 = c + +[app:main] +# Equivalent to the egg reference, but just for kicks... +paste.app_factory1 = fakeapp.configapps:SimpleApp.make_app +set glob = orig +bob = your uncle +another = BAR diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py index 92405ed..05b61e3 100644 --- a/tests/test_basic_app.py +++ b/tests/test_basic_app.py @@ -1,6 +1,5 @@ from paste.deploy import loadapp, loadfilter from fixture import * -require('FakeApp') import fakeapp.apps here = os.path.dirname(__file__) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..11d121e --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,55 @@ +import os +from paste.deploy import loadapp, loadfilter +from fixture import * +import fakeapp.configapps as fc + +ini_file = 'config:sample_configs/test_config.ini' +here = os.path.dirname(__file__) + +def test_config_egg(): + app = loadapp('egg:FakeApp#configed') + assert isinstance(app, fc.SimpleApp) + +def test_config1(): + app = loadapp(ini_file, relative_to=here, name='test1') + assert app.local_conf == { + 'setting1': 'foo', 'setting2': 'bar'} + assert app.global_conf == { + 'def1': 'a', 'def2': 'b'} + +def test_config2(): + app = loadapp(ini_file, relative_to=here, name='test2') + assert app.local_conf == { + 'local conf': 'something'} + assert app.global_conf == { + 'def1': 'test2', + 'def2': 'b', + 'another': 'TEST'} + # Run this to make sure the global-conf-modified test2 + # didn't mess up the general global conf + test_config1() + +def test_config3(): + app = loadapp(ini_file, relative_to=here, name='test3') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'local conf': 'something', + 'another': 'something more\nacross several\nlines'} + assert app.global_conf == { + 'def1': 'test3', + 'def2': 'b', + 'another': 'TEST'} + test_config2() + +def test_foreign_config(): + app = loadapp(ini_file, relative_to=here, name='test_foreign_config') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'another': 'FOO', + 'bob': 'your uncle'} + assert app.global_conf == { + 'def1': 'a', + 'def2': 'from include', + 'def3': 'c', + 'glob': 'override'} + -- cgit v1.2.1 From 3c3d1a70f4b90d08504e0585ce77ce3b5ae357d3 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 21 Aug 2005 23:34:08 +0000 Subject: Added file forgotten in last commit; added interface file for documentation --- paste/deploy/interfaces.py | 166 +++++++++++++++++++++ .../FakeApp.egg/fakeapp/configapps.py | 14 ++ 2 files changed, 180 insertions(+) create mode 100644 paste/deploy/interfaces.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/configapps.py diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py new file mode 100644 index 0000000..efe5dd0 --- /dev/null +++ b/paste/deploy/interfaces.py @@ -0,0 +1,166 @@ +############################################################ +## Functions +############################################################ + +def loadapp(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadapp``. + + Load the specified URI as a WSGI application (returning IWSGIApp). + The ``name`` can be in the URI (typically as ``#name``). If it is + and ``name`` is given, the keyword argument overrides the URI. + + If the URI contains a relative filename, then ``relative_to`` is + used (if ``relative_to`` is not provided, then it is an error). + + ``global_conf`` is used to load the configuration (additions + override the values). ``global_conf`` is copied before modifying. + """ + +def loadfilter(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadfilter``. + + Like ``loadapp()``, except returns in IFilter object. + """ + +def loadserver(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadserver``. + + Like ``loadapp()``, except returns in IServer object. + """ + +############################################################ +## Factories +############################################################ + +class IPasteAppFactory1: + + """ + This is the spec for the ``paste.app_factory1`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a WSGI application (IWSGIAPP) given the global + configuration and the local configuration passed in as keyword + arguments. All keys are strings, but values in local_conf may + not be valid Python identifiers (if you use **kw you can still + capture these values). + """ + +class IPasteCompositFactory1: + + """ + This is the spec for the ``paste.composit_factory1`` + protocol/entry_point. + + This also produces WSGI applications, like ``paste.app_factory1``, + but is given more access to the context in which it is loaded. + """ + + def __call__(loader, global_conf, **local_conf): + """ + Like IPasteAppFactory1 this returns a WSGI application + (IWSGIApp). The ``loader`` value conforms to the ``ILoader`` + interface, and can be used to load (contextually) more + applications. + """ + +class IPasteFilterFactory1: + + """ + This is the spec for the ``paste.filter_factory1`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a IFilter object. + """ + +class IPasteServerFactory1: + + """ + This is the spec for the ``paste.server_factory1`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a IServer object. + """ + +class ILoader: + + """ + This is an object passed into ``IPasteCompositFactory``. It is + currently implemented in ``paste.deploy.loadwsgi`` by + ``ConfigLoader`` and ``EggLoader``. + """ + + def get_app(name_or_uri, global_conf=None): + """ + Return an IWSGIApp object. If the loader supports named + applications, then you can use a simple name; otherwise + you must use a full URI. + + Any global configuration you pass in will be added; you should + generally pass through the global configuration you received. + """ + + def get_filter(name_or_uri, global_conf=None): + """ + Return an IFilter object, like ``get_app``. + """ + + def get_server(name_or_uri, global_conf=None): + """ + Return an IServer object, like ``get_app``. + """ + +############################################################ +## Objects +############################################################ + +class IWSGIApp: + + """ + This is an application that conforms to `PEP 333 + `_: Python Web Server + Gateway Interface v1.0 + """ + + def __call__(environ, start_response): + """ + Calls ``start_response(status_code, header_list)`` and returns + an iterator for the body of the response. + """ + +class IFilter: + + """ + A filter is a simple case of middleware, where an object + wraps a single WSGI application (IWSGIApp). + """ + + def __call__(wsgi_app): + """ + Returns an IWSGIApp object, typically one that wraps the + ``wsgi_app`` passed in. + """ + +class IServer: + + """ + A simple server interface. + """ + + def __call__(wsgi_app): + """ + Serves the given WSGI application. May serve once, many + times, forever; nothing about how the server works is + specified here. + """ diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py new file mode 100644 index 0000000..ef13182 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py @@ -0,0 +1,14 @@ +class SimpleApp(object): + def __init__(self, global_conf, local_conf, name): + self.global_conf = global_conf + self.local_conf = local_conf + self.name = name + + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['I am: ', name] + + def make_app(cls, global_conf, **conf): + return cls(global_conf, conf, 'basic') + make_app = classmethod(make_app) + -- cgit v1.2.1 From 056d48193b37ab25ee201f6f317fec45066036ef Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 01:04:14 +0000 Subject: Added documentation --- docs/paste-deploy.txt | 384 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 docs/paste-deploy.txt diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt new file mode 100644 index 0000000..151b570 --- /dev/null +++ b/docs/paste-deploy.txt @@ -0,0 +1,384 @@ +Paste Deployment +================ + +:author: Ian Bicking +:revision: $Rev$ +:date: $LastChangedDate$ + +.. contents:: + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +Installation +------------ + +First install `setuptools +`_. + +You can install Paste Deployment by installing `easy_install +`_ and running:: + + $ sudo easy_install.py http://svn.pythonpaste.org/Paste/Deploy/trunk + +If you want to track development, do:: + + $ svn co http://svn.pythonpaste.org/Paste/Deploy/trunk Paste-Deploy + $ cd Paste-Deploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory1 = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory1``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composit Applications +~~~~~~~~~~~~~~~~~~~~~ + +"Composit" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composit:main] + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composit application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ that adds +metadata to a normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory1': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory1``, +``paste.composit_factory1``, ``paste.filter_factory1``, and lastly +``paste.server_factory1``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory1`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composit_factory1`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composits are just slightly more complex:: + + def composit_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composit factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composit:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory1`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.server_factory1`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +An implementation of ``Server`` is left to the user. + -- cgit v1.2.1 From 1ec11085543ffe9388d839797feea064148225ed Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 03:02:54 +0000 Subject: Added to-do; added doc build script --- docs/paste-deploy.txt | 26 ++++++++++++++++++++++++++ docs/rebuild | 17 +++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100755 docs/rebuild diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index 151b570..02b8180 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -382,3 +382,29 @@ An example might look like:: An implementation of ``Server`` is left to the user. +Outstanding Issues +------------------ + +* Should add a ``python:`` scheme for loading objects out of modules + directly. It has to include the protocol somehow...? + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory1`` and ``paste.composit_factory1`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + diff --git a/docs/rebuild b/docs/rebuild new file mode 100755 index 0000000..a8d809e --- /dev/null +++ b/docs/rebuild @@ -0,0 +1,17 @@ +#!/bin/sh + +here=`pwd` +parent=`dirname $here` +echo "Adding $parent to \$PYTHONPATH" +export PYTHONPATH=$parent:$PYTHONPATH + +NORMAL="paste-deploy" + +for NAME in $NORMAL ; do + if [ -e "$NAME.html" -a ! "$NAME.html" -ot "$NAME.txt" ] ; then + echo "$NAME is up to date." + continue + fi + echo "Building $NAME." + rst2html.py --no-toc-backlinks "$NAME.txt" > "$NAME.html" +done -- cgit v1.2.1 From df13acecc2b41b1e091d4a6ff3846b0661599879 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 03:20:22 +0000 Subject: Changed homepage link and added link to svn repository --- setup.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 5450b30..136b421 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,18 @@ from setuptools import setup, find_packages +version = '0.1' + setup( name="Paste-Deploy", - version="0.1", + version=version, description="Load, configure, and compose WSGI applications and servers", long_description="""\ This tool provides code to load WSGI applications and servers from URIs; these URIs can refer to Python Eggs for INI-style configuration -files. -""", +files. + +See also the `Subversion repository Date: Mon, 22 Aug 2005 03:46:22 +0000 Subject: Highlight interfaces.py as part of the doc process --- docs/rebuild | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/rebuild b/docs/rebuild index a8d809e..c160226 100755 --- a/docs/rebuild +++ b/docs/rebuild @@ -15,3 +15,6 @@ for NAME in $NORMAL ; do echo "Building $NAME." rst2html.py --no-toc-backlinks "$NAME.txt" > "$NAME.html" done + +source-highlight -f html ../paste/deploy/interfaces.py +mv ../paste/deploy/interfaces.py.html . -- cgit v1.2.1 From df5509396d6d7c09da27bc1c41f7d27626084124 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 03:48:56 +0000 Subject: Added manifest --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e5deda2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include docs/*.html +include docs/*.txt +exclude docs/rebuild -- cgit v1.2.1 From 78f0b0e9bf015f0975318a650e6af83e9bb102e9 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 04:00:56 +0000 Subject: Changed distro name; ignore egg-info dir; fix long description; update version to 0.2 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 136b421..9cd371d 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ from setuptools import setup, find_packages -version = '0.1' +version = '0.2' setup( - name="Paste-Deploy", + name="PasteDeploy", version=version, description="Load, configure, and compose WSGI applications and servers", long_description="""\ @@ -11,7 +11,7 @@ This tool provides code to load WSGI applications and servers from URIs; these URIs can refer to Python Eggs for INI-style configuration files. -See also the `Subversion repository `_ """ % version, classifiers=["Development Status :: 3 - Alpha", "Intended Audience :: Developers", -- cgit v1.2.1 From 5c6765900efbd3d47b78ac12f3e6f1ee44240b44 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 16:45:04 +0000 Subject: Added filter-app and pipeline sections for configs; added docs; added better intro; added EGG-INFO creator (instead of putting EGG-INFO in svn repository); added news document --- docs/news.txt | 12 +++ docs/paste-deploy.txt | 34 ++++++- docs/rebuild | 2 +- paste/deploy/loadwsgi.py | 113 ++++++++++++++++++--- tests/fake_packages/FakeApp.egg/EGG-INFO | 1 - .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 5 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 25 ++++- tests/fake_packages/FakeApp.egg/setup.py | 3 + tests/fixture.py | 13 +++ tests/sample_configs/test_filter.ini | 10 ++ tests/test_filter.py | 19 ++++ 11 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 docs/news.txt delete mode 120000 tests/fake_packages/FakeApp.egg/EGG-INFO create mode 100644 tests/sample_configs/test_filter.ini create mode 100644 tests/test_filter.py diff --git a/docs/news.txt b/docs/news.txt new file mode 100644 index 0000000..4377eab --- /dev/null +++ b/docs/news.txt @@ -0,0 +1,12 @@ +Paste Deployment News +===================== + +Version 0.2 (svn trunk) +----------------------- + +* Added ``filter-app:`` and ``pipeline:`` sections. + +Version 0.1 +----------- + +Initial version released. It's all new. diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index 02b8180..38b1a86 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -11,11 +11,23 @@ Introduction ------------ Paste Deployment is a system for finding and configuring WSGI -applications and servers. +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. Paste Deployment currently does not require other parts of `Paste `_, and is distributed as a separate package. +To see updates that have been made to Paste Deploy see the `news file +`_. + Installation ------------ @@ -212,6 +224,26 @@ prefixes. You load these with ``loadserver`` and ``loadfilter``. The configuration works just the same; you just get back different kinds of objects. +Filter Composition +~~~~~~~~~~~~~~~~~~ + +Two special section types exist to apply filters more easily to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of these +sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + ``egg:`` URIs ------------- diff --git a/docs/rebuild b/docs/rebuild index c160226..852c3fd 100755 --- a/docs/rebuild +++ b/docs/rebuild @@ -5,7 +5,7 @@ parent=`dirname $here` echo "Adding $parent to \$PYTHONPATH" export PYTHONPATH=$parent:$PYTHONPATH -NORMAL="paste-deploy" +NORMAL="paste-deploy news" for NAME in $NORMAL ; do if [ -e "$NAME.html" -a ! "$NAME.html" -ot "$NAME.txt" ] ; then diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 12e9c5a..0d71233 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -58,7 +58,7 @@ class _ObjectType(object): APP = _ObjectType( 'application', ['paste.app_factory1', 'paste.composit_factory1'], - [['app', 'application'], 'composit']) + [['app', 'application'], 'composit', 'pipeline', 'filter-app']) def APP_invoke(context): if context.protocol == 'paste.composit_factory1': @@ -81,6 +81,34 @@ SERVER = _ObjectType( ['paste.server_factory1'], ['server']) +# Virtual type: (@@: There's clearly something crufty here; +# this probably could be more elegant) +PIPELINE = _ObjectType( + 'pipeline', + [], []) + +def PIPELINE_invoke(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.invoke = PIPELINE_invoke + +# Virtual type: +FILTER_APP = _ObjectType( + 'filter_app', + [], []) + +def FILTER_APP_invoke(context): + next_app = context.next_context.create() + filter = context.filter_context.create() + return filter(next_app) + +FILTER_APP.invoke = FILTER_APP_invoke + ############################################################ ## Loaders ############################################################ @@ -209,12 +237,12 @@ class ConfigLoader(_Loader): 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) if global_conf is None: global_conf = {} else: global_conf = global_conf.copy() - section = self.find_config_section( - object_type, name=name) defaults = self.parser.defaults() global_conf.update(defaults) local_conf = {} @@ -229,15 +257,36 @@ class ConfigLoader(_Loader): # @@: It's a global option (?), so skip it continue local_conf[option] = self.parser.get(section, option) + if section.startswith('filter-app:'): + return self._filter_app_context( + object_type, section, name=name, + global_conf=global_conf, local_conf=local_conf, + global_additions=global_additions) + if section.startswith('pipeline:'): + return self._pipeline_app_context( + object_type, section, name=name, + global_conf=global_conf, local_conf=local_conf, + global_additions=global_additions) if 'use' in local_conf: - 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) - # @@: Should loader be overwritten? - context.loader = self - return context + return self._context_from_use( + object_type, local_conf, global_conf, global_additions) + else: + return self._context_from_explicit( + object_type, local_conf, global_conf, global_additions) + + def _context_from_use(self, object_type, local_conf, global_conf, + global_additions): + 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) + # @@: Should loader be overwritten? + context.loader = self + return context + + def _context_from_explicit(self, object_type, local_conf, global_conf, + global_addition): possible = [] for protocol_options in object_type.egg_protocols: for protocol in protocol_options: @@ -259,6 +308,46 @@ class ConfigLoader(_Loader): 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) + else: + context.filter_context = self._context_from_explicit( + FILTER, local_conf, global_conf, global_additions) + 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" + % (', '.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 @@ -278,7 +367,7 @@ class ConfigLoader(_Loader): break if not possible: raise LookupError( - "No section %r (prefixed by %s) found in config %s from %s" + "No section %r (prefixed by %s) found in config %s" % (name, ' or '.join(map(repr, _flatten(object_type.config_prefixes))), self.filename)) diff --git a/tests/fake_packages/FakeApp.egg/EGG-INFO b/tests/fake_packages/FakeApp.egg/EGG-INFO deleted file mode 120000 index 1942929..0000000 --- a/tests/fake_packages/FakeApp.egg/EGG-INFO +++ /dev/null @@ -1 +0,0 @@ -FakeApp.egg-info \ No newline at end of file diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt index 901a406..0f2be56 100644 --- a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -5,6 +5,11 @@ configed=fakeapp.configapps:SimpleApp.make_app +[paste.filter_factory1] + + caps=fakeapp.apps:make_cap_filter + + [paste.composit_factory1] remote_addr=fakeapp.apps:make_remote_addr diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py index 759a9da..f25b4a9 100644 --- a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -4,7 +4,7 @@ def simple_app(response, environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) - return [response] + return ['This is ', response] def basic_app(environ, start_response): return simple_app('basic app', environ, start_response) @@ -43,3 +43,26 @@ class RemoteAddrDispatch(object): addr = environ['REMOTE_ADDR'] app = self.map.get(addr) or self.map['0.0.0.0'] return app(environ, start_response) + +############################################################ +## Filters +############################################################ + +def make_cap_filter(global_conf, method_to_call='upper'): + def filter(app): + return CapFilter(app, method_to_call) + return filter + +class CapFilter(object): + + def __init__(self, app, method_to_call): + self.app = app + self.method_to_call = method_to_call + + def __call__(self, environ, start_response): + app_iter = self.app(environ, start_response) + for item in app_iter: + yield getattr(item, self.method_to_call)() + if hasattr(app_iter, 'close'): + app_iter.close() + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index c67b038..68506dc 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -13,5 +13,8 @@ setup( 'paste.composit_factory1': """ remote_addr=fakeapp.apps:make_remote_addr """, + 'paste.filter_factory1': """ + caps=fakeapp.apps:make_cap_filter + """ }, ) diff --git a/tests/fixture.py b/tests/fixture.py index ee42c53..b2cc75f 100644 --- a/tests/fixture.py +++ b/tests/fixture.py @@ -1,3 +1,16 @@ import os +import shutil from pkg_resources import * + +test_dir = os.path.dirname(__file__) +egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'EGG-INFO') +info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'FakeApp.egg-info') +if not os.path.exists(egg_info_dir): + try: + os.symlink(info_dir, egg_info_dir) + except: + shutil.copytree(info_dir, egg_info_dir) + require('FakeApp') diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini new file mode 100644 index 0000000..a44302a --- /dev/null +++ b/tests/sample_configs/test_filter.ini @@ -0,0 +1,10 @@ +[app:normal] +use = egg:FakeApp#basic_app + +[pipeline:piped] +pipeline = egg:FakeApp#caps normal + +[filter-app:filt] +use = egg:FakeApp#caps +method_to_call = lower +next = normal diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..7475736 --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,19 @@ +from paste.deploy import loadapp, loadfilter +from fixture import * +import fakeapp.apps + +here = os.path.dirname(__file__) + +def test_filter_app(): + app = loadapp('config:sample_configs/test_filter.ini#filt', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + +def test_pipeline(): + app = loadapp('config:sample_configs/test_filter.ini#piped', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' -- cgit v1.2.1 -- cgit v1.2.1 From 7b1f6f058149e7d3b735aea2828832767ccd91a6 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 17:34:04 +0000 Subject: Removed spurious modules which I had intended to port from previous experiment, but now I think I won't --- paste/deploy/pipeline.py | 18 ------------------ paste/deploy/servers.py | 7 ------- paste/deploy/urlmap.py | 12 ------------ 3 files changed, 37 deletions(-) delete mode 100644 paste/deploy/pipeline.py delete mode 100644 paste/deploy/servers.py delete mode 100644 paste/deploy/urlmap.py diff --git a/paste/deploy/pipeline.py b/paste/deploy/pipeline.py deleted file mode 100644 index c33a23a..0000000 --- a/paste/deploy/pipeline.py +++ /dev/null @@ -1,18 +0,0 @@ -from paste import wsgi_deploy - -def make_pipeline(context): - pipeline = filter( - None, context.app_config.get('pipeline', '').split()) - if not pipeline: - raise IndexError( - "You must give a pipeline setting") - filters = pipeline[:-1] - filters.reverse() - app_name = pipeline[-1] - deploy = context.deployment_config - app = deploy.make_app(app_name) - for filter_name in filters: - wsgi_filter = deploy.make_filter(filter_name) - app = wsgi_filter(app) - return app - diff --git a/paste/deploy/servers.py b/paste/deploy/servers.py deleted file mode 100644 index 2230c16..0000000 --- a/paste/deploy/servers.py +++ /dev/null @@ -1,7 +0,0 @@ -def make_wsgiutils(host, port): - from wsgiutils import wsgiServer - def server(app): - server = wsgiServer.WSGIServer( - (host, int(port)), {'': app}) - server.serve_forever() - return server diff --git a/paste/deploy/urlmap.py b/paste/deploy/urlmap.py deleted file mode 100644 index 26525b2..0000000 --- a/paste/deploy/urlmap.py +++ /dev/null @@ -1,12 +0,0 @@ -from paste import wsgi_deploy -from paste import urlmap - -def make_urlmap(context): - mapper = urlmap.URLMap() - for key, value in context.app_config.items(): - if (key.startswith('/') or key.startswith('http://') - or key.startswith('https://')): - app = context.deployment_config.make_app(value) - mapper[key] = app - return mapper - -- cgit v1.2.1 From 8d2f559f5b40851440fbaecb288bb273379e7c1f Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 18:07:41 +0000 Subject: New protocols, paste.server_runner1 and paste.filter_app_factory1 --- docs/news.txt | 8 +++++- docs/paste-deploy.txt | 21 ++++++++++++++ paste/deploy/interfaces.py | 32 ++++++++++++++++++++++ paste/deploy/loadwsgi.py | 32 ++++++++++++++++++++-- .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 5 ++++ tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 5 ++-- tests/fake_packages/FakeApp.egg/setup.py | 5 +++- tests/sample_configs/test_filter.ini | 9 ++++++ tests/test_filter.py | 14 ++++++++++ 9 files changed, 125 insertions(+), 6 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 4377eab..f07aede 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -4,7 +4,13 @@ Paste Deployment News Version 0.2 (svn trunk) ----------------------- -* Added ``filter-app:`` and ``pipeline:`` sections. +* Added ``filter-app:`` and ``pipeline:`` sections. `Docs + `__. + +* Added ``paste.filter_app_factory1`` (`doc + `__) and + ``paste.server_runner1`` (`doc + `__) protocols. Version 0.1 ----------- diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index 38b1a86..5e64dd0 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -394,6 +394,21 @@ variable is set, creating a really simple authentication filter:: '403 Forbidden', [('Content-type', 'text/html')]) return ['You are forbidden to view this resource'] +``paste.filter_app_factory1`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory1``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + ``paste.server_factory1`` ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -414,6 +429,12 @@ An example might look like:: An implementation of ``Server`` is left to the user. +``paste.server_runner1`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory1``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + Outstanding Issues ------------------ diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index efe5dd0..5f649cb 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -81,6 +81,21 @@ class IPasteFilterFactory1: Returns a IFilter object. """ +class IPasteFilterAppFactory1: + + """ + This is the spec for the ``paste.filter_app_factory1`` + protocol/entry_point. + """ + + def __call__(wsgi_app, global_conf, **local_conf): + """ + Returns a WSGI application that wraps ``wsgi_app``. + + Note that paste.deploy creates a wrapper for these + objects that implement the IFilter interface. + """ + class IPasteServerFactory1: """ @@ -93,6 +108,23 @@ class IPasteServerFactory1: Returns a IServer object. """ +class IPasteServerRunner1: + + """ + This is the spec for the ``paste.server_runner1`` + protocol/entry_point. + """ + + def __call__(wsgi_app, global_conf, **local_conf): + """ + Serves the given WSGI application. May serve once, many + times, forever; nothing about how the server works is + specified here. + + Note that paste.deploy creates a wrapper for these + objects that implement the IServer interface. + """ + class ILoader: """ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 0d71233..9a970b9 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -73,14 +73,42 @@ APP.invoke = APP_invoke FILTER = _ObjectType( 'filter', - ['paste.filter_factory1'], + [['paste.filter_factory1', 'paste.filter_app_factory1']], ['filter']) +def FILTER_invoke(context): + if context.protocol == 'paste.filter_factory1': + return context.object(context.global_conf, **context.local_conf) + elif context.protocol == 'paste.filter_app_factory1': + def filter_wrapper(wsgi_app): + # This should be an object, so it has a nicer __repr__ + return context.object(wsgi_app, context.global_conf, + **context.local_conf) + return filter_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol + +FILTER.invoke = FILTER_invoke + SERVER = _ObjectType( 'server', - ['paste.server_factory1'], + [['paste.server_factory1', 'paste.server_runner1']], ['server']) +def SERVER_invoke(context): + if context.protocol == 'paste.server_factory1': + return context.object(context.global_conf, **context.local_conf) + elif context.protocol == 'paste.server_runner1': + def server_wrapper(wsgi_app): + # This should be an object, so it has a nicer __repr__ + return context.object(wsgi_app, context.global_conf, + **context.local_conf) + return server_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol + +SERVER.invoke = SERVER_invoke + # Virtual type: (@@: There's clearly something crufty here; # this probably could be more elegant) PIPELINE = _ObjectType( diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt index 0f2be56..8ecaf21 100644 --- a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -15,3 +15,8 @@ remote_addr=fakeapp.apps:make_remote_addr +[paste.filter_app_factory1] + + caps2=fakeapp.apps:CapFilter + + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py index f25b4a9..7280210 100644 --- a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -50,14 +50,15 @@ class RemoteAddrDispatch(object): def make_cap_filter(global_conf, method_to_call='upper'): def filter(app): - return CapFilter(app, method_to_call) + return CapFilter(app, global_conf, method_to_call) return filter class CapFilter(object): - def __init__(self, app, method_to_call): + def __init__(self, app, global_conf, method_to_call='upper'): self.app = app self.method_to_call = method_to_call + self.global_conf = global_conf def __call__(self, environ, start_response): app_iter = self.app(environ, start_response) diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index 68506dc..febdf9f 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -15,6 +15,9 @@ setup( """, 'paste.filter_factory1': """ caps=fakeapp.apps:make_cap_filter - """ + """, + 'paste.filter_app_factory1': """ + caps2=fakeapp.apps:CapFilter + """, }, ) diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini index a44302a..f948b97 100644 --- a/tests/sample_configs/test_filter.ini +++ b/tests/sample_configs/test_filter.ini @@ -8,3 +8,12 @@ pipeline = egg:FakeApp#caps normal use = egg:FakeApp#caps method_to_call = lower next = normal + +[pipeline:piped2] +pipeline = egg:FakeApp#caps2 normal + +[filter-app:filt2] +use = egg:FakeApp#caps2 +method_to_call = lower +next = normal + diff --git a/tests/test_filter.py b/tests/test_filter.py index 7475736..5740223 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -17,3 +17,17 @@ def test_pipeline(): assert isinstance(app, fakeapp.apps.CapFilter) assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'upper' + +def test_filter_app2(): + app = loadapp('config:sample_configs/test_filter.ini#filt2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + +def test_pipeline2(): + app = loadapp('config:sample_configs/test_filter.ini#piped2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' -- cgit v1.2.1 From 2f46fd3925a56490de67a07a7674bd2921803ca2 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 22:46:28 +0000 Subject: Converters for handling the string keys we get --- paste/deploy/converters.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 paste/deploy/converters.py diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py new file mode 100644 index 0000000..f329174 --- /dev/null +++ b/paste/deploy/converters.py @@ -0,0 +1,24 @@ +def asbool(obj): + if isinstance(obj, (str, unicode)): + obj = obj.strip().lower() + if obj in ['true', 'yes', 'on', 'y', 't', '1']: + return True + elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + return False + else: + raise ValueError( + "String is not true/false: %r" % obj) + return bool(obj) + +def aslist(obj, sep=None, strip=True): + if isinstance(obj, (str, unicode)): + 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] -- cgit v1.2.1 From 98743455e80615911730d72766bfddda7532dd8d Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 22 Aug 2005 22:47:00 +0000 Subject: Converters news note --- docs/news.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index f07aede..d8eecd7 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -12,6 +12,9 @@ Version 0.2 (svn trunk) ``paste.server_runner1`` (`doc `__) protocols. +* Added ``paste.deploy.converters`` module for handling the + string values that are common with this system. + Version 0.1 ----------- -- cgit v1.2.1 From f8cd35289cc87e1529efced52d5e9cbc3c423399 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 25 Aug 2005 19:37:03 +0000 Subject: Added filter-with option; fixed some error messages; changed protocol names --- docs/news.txt | 5 ++ docs/paste-deploy.txt | 51 +++++++++++------ paste/deploy/interfaces.py | 28 ++++----- paste/deploy/loadwsgi.py | 66 +++++++++++++++------- tests/conftest.py | 4 -- .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 14 ++--- tests/fake_packages/FakeApp.egg/setup.py | 8 +-- tests/sample_configs/test_config_included.ini | 2 +- tests/sample_configs/test_filter.ini | 3 + tests/test_filter.py | 6 ++ 10 files changed, 118 insertions(+), 69 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index d8eecd7..8ea663c 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -4,6 +4,11 @@ Paste Deployment News Version 0.2 (svn trunk) ----------------------- +* Added a ``filter-with`` setting to applications. + +* Removed the ``1`` from all the protocol names (e.g., + ``paste.app_factory1`` is not ``paste.app_factory``). + * Added ``filter-app:`` and ``pipeline:`` sections. `Docs `__. diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index 5e64dd0..e7852cb 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -132,10 +132,10 @@ The other way to define an application is to point exactly to some Python code:: [app:myapp] - paste.app_factory1 = myapp.modulename:app_factory + paste.app_factory = myapp.modulename:app_factory You must give an explicit *protocol* (in this case -``paste.app_factory1``), and the value is something to import. In +``paste.app_factory``), and the value is something to import. In this case the module ``myapp.modulename`` is loaded, and the ``app_factory`` object retrieved from it. @@ -227,9 +227,21 @@ of objects. Filter Composition ~~~~~~~~~~~~~~~~~~ -Two special section types exist to apply filters more easily to your -applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of these -sections define applications, and so can be used wherever an +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an application is needed. ``filter-app`` defines a filter (just like you would in a @@ -244,6 +256,9 @@ ended by an application, like:: [pipeline:main] pipeline = filter1 egg:FilterEgg#filter2 filter3 app + [filter:filter1] + ... + ``egg:`` URIs ------------- @@ -286,7 +301,7 @@ argument to ``setup()`` like:: name='MyApp', ... entry_points={ - 'paste.app_factory1': [ + 'paste.app_factory': [ 'main=myapp.mymodule:app_factory', 'ob2=myapp.mymodule:ob_factory'], }, @@ -309,12 +324,12 @@ This lets you point to factories (that obey the specific protocols we mentioned). But that's not much use unless you can create factories for your applications. -There's a few protocols: ``paste.app_factory1``, -``paste.composit_factory1``, ``paste.filter_factory1``, and lastly -``paste.server_factory1``. Each of these expects a callable (like a +There's a few protocols: ``paste.app_factory``, +``paste.composit_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a function, method, or class). -``paste.app_factory1`` +``paste.app_factory`` ~~~~~~~~~~~~~~~~~~~~~~ The application is the most common. You define one like:: @@ -325,7 +340,7 @@ The application is the most common. You define one like:: The ``global_config`` is a dictionary, and local configuration is passed as keyword arguments. The function returns a WSGI application. -``paste.composit_factory1`` +``paste.composit_factory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Composits are just slightly more complex:: @@ -364,7 +379,7 @@ Then we use it like:: [app:myapp] use = egg:MyApp -``paste.filter_factory1`` +``paste.filter_factory`` ~~~~~~~~~~~~~~~~~~~~~~~~~ Filter factories are just like app factories (same signature), except @@ -394,10 +409,10 @@ variable is set, creating a really simple authentication filter:: '403 Forbidden', [('Content-type', 'text/html')]) return ['You are forbidden to view this resource'] -``paste.filter_app_factory1`` +``paste.filter_app_factory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This is very similar to ``paste.filter_factory1``, except that it also +This is very similar to ``paste.filter_factory``, except that it also takes a ``wsgi_app`` argument, and returns a WSGI application. So if you changed the above example to:: @@ -409,7 +424,7 @@ Then ``AuthFilter`` would serve as a filter_app_factory (``req_usernames`` is a required local configuration key in this case). -``paste.server_factory1`` +``paste.server_factory`` ~~~~~~~~~~~~~~~~~~~~~~~~~ This takes the same signature as applications and filters, but returns @@ -429,10 +444,10 @@ An example might look like:: An implementation of ``Server`` is left to the user. -``paste.server_runner1`` +``paste.server_runner`` ~~~~~~~~~~~~~~~~~~~~~~~~ -Like ``paste.server_factory1``, except ``wsgi_app`` is passed as the +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the first argument, and the server should run immediately. Outstanding Issues @@ -444,7 +459,7 @@ Outstanding Issues * Should there be a "default" protocol for each type of object? Since there's currently only one protocol, it seems like it makes sense (in the future there could be multiple). Except that - ``paste.app_factory1`` and ``paste.composit_factory1`` overlap + ``paste.app_factory`` and ``paste.composit_factory`` overlap considerably. * ConfigParser's INI parsing is kind of annoying. I'd like it both diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index 5f649cb..63e37b2 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -35,10 +35,10 @@ def loadserver(uri, name=None, relative_to=None, global_conf=None): ## Factories ############################################################ -class IPasteAppFactory1: +class IPasteAppFactory: """ - This is the spec for the ``paste.app_factory1`` + This is the spec for the ``paste.app_factory`` protocol/entry_point. """ @@ -51,28 +51,28 @@ class IPasteAppFactory1: capture these values). """ -class IPasteCompositFactory1: +class IPasteCompositFactory: """ - This is the spec for the ``paste.composit_factory1`` + This is the spec for the ``paste.composit_factory`` protocol/entry_point. - This also produces WSGI applications, like ``paste.app_factory1``, + This also produces WSGI applications, like ``paste.app_factory``, but is given more access to the context in which it is loaded. """ def __call__(loader, global_conf, **local_conf): """ - Like IPasteAppFactory1 this returns a WSGI application + Like IPasteAppFactory this returns a WSGI application (IWSGIApp). The ``loader`` value conforms to the ``ILoader`` interface, and can be used to load (contextually) more applications. """ -class IPasteFilterFactory1: +class IPasteFilterFactory: """ - This is the spec for the ``paste.filter_factory1`` + This is the spec for the ``paste.filter_factory`` protocol/entry_point. """ @@ -81,10 +81,10 @@ class IPasteFilterFactory1: Returns a IFilter object. """ -class IPasteFilterAppFactory1: +class IPasteFilterAppFactory: """ - This is the spec for the ``paste.filter_app_factory1`` + This is the spec for the ``paste.filter_app_factory`` protocol/entry_point. """ @@ -96,10 +96,10 @@ class IPasteFilterAppFactory1: objects that implement the IFilter interface. """ -class IPasteServerFactory1: +class IPasteServerFactory: """ - This is the spec for the ``paste.server_factory1`` + This is the spec for the ``paste.server_factory`` protocol/entry_point. """ @@ -108,10 +108,10 @@ class IPasteServerFactory1: Returns a IServer object. """ -class IPasteServerRunner1: +class IPasteServerRunner: """ - This is the spec for the ``paste.server_runner1`` + This is the spec for the ``paste.server_runner`` protocol/entry_point. """ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 9a970b9..9e892d7 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -57,14 +57,14 @@ class _ObjectType(object): APP = _ObjectType( 'application', - ['paste.app_factory1', 'paste.composit_factory1'], + ['paste.app_factory', 'paste.composit_factory'], [['app', 'application'], 'composit', 'pipeline', 'filter-app']) def APP_invoke(context): - if context.protocol == 'paste.composit_factory1': + if context.protocol == 'paste.composit_factory': return context.object(context.loader, context.global_conf, **context.local_conf) - elif context.protocol == 'paste.app_factory1': + elif context.protocol == 'paste.app_factory': return context.object(context.global_conf, **context.local_conf) else: assert 0, "Protocol %r unknown" % context.protocol @@ -73,13 +73,13 @@ APP.invoke = APP_invoke FILTER = _ObjectType( 'filter', - [['paste.filter_factory1', 'paste.filter_app_factory1']], + [['paste.filter_factory', 'paste.filter_app_factory']], ['filter']) def FILTER_invoke(context): - if context.protocol == 'paste.filter_factory1': + if context.protocol == 'paste.filter_factory': return context.object(context.global_conf, **context.local_conf) - elif context.protocol == 'paste.filter_app_factory1': + elif context.protocol == 'paste.filter_app_factory': def filter_wrapper(wsgi_app): # This should be an object, so it has a nicer __repr__ return context.object(wsgi_app, context.global_conf, @@ -92,13 +92,13 @@ FILTER.invoke = FILTER_invoke SERVER = _ObjectType( 'server', - [['paste.server_factory1', 'paste.server_runner1']], + [['paste.server_factory', 'paste.server_runner']], ['server']) def SERVER_invoke(context): - if context.protocol == 'paste.server_factory1': + if context.protocol == 'paste.server_factory': return context.object(context.global_conf, **context.local_conf) - elif context.protocol == 'paste.server_runner1': + elif context.protocol == 'paste.server_runner': def server_wrapper(wsgi_app): # This should be an object, so it has a nicer __repr__ return context.object(wsgi_app, context.global_conf, @@ -137,6 +137,16 @@ def FILTER_APP_invoke(context): FILTER_APP.invoke = FILTER_APP_invoke +FILTER_WITH = _ObjectType( + 'filtered_app', [], []) + +def FILTER_WITH_invoke(context): + filter = context.filter_context.create() + app = APP_invoke(context) + return filter(app) + +FILTER_WITH.invoke = FILTER_WITH_invoke + ############################################################ ## Loaders ############################################################ @@ -285,25 +295,37 @@ class ConfigLoader(_Loader): # @@: It's a global option (?), so skip it continue local_conf[option] = self.parser.get(section, option) + if object_type is APP and 'filter-with' in local_conf: + filter_with = local_conf.pop('filter-with') + else: + filter_with = None if section.startswith('filter-app:'): - return self._filter_app_context( + context = self._filter_app_context( object_type, section, name=name, global_conf=global_conf, local_conf=local_conf, global_additions=global_additions) - if section.startswith('pipeline:'): - return self._pipeline_app_context( + 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) - if 'use' in local_conf: - return self._context_from_use( - object_type, local_conf, global_conf, global_additions) + elif 'use' in local_conf: + context = self._context_from_use( + object_type, local_conf, global_conf, global_additions, + section) else: - return self._context_from_explicit( - object_type, local_conf, global_conf, global_additions) + context = self._context_from_explicit( + object_type, local_conf, global_conf, global_additions, + section) + if filter_with is not None: + filter_context = self.filter_context( + name=filter_with, global_conf=global_conf) + context.object_type = FILTER_WITH + context.filter_context = filter_context + return context def _context_from_use(self, object_type, local_conf, global_conf, - global_additions): + global_additions, section): use = local_conf.pop('use') context = self.get_context( object_type, name=use, global_conf=global_conf) @@ -314,7 +336,7 @@ class ConfigLoader(_Loader): return context def _context_from_explicit(self, object_type, local_conf, global_conf, - global_addition): + global_addition, section): possible = [] for protocol_options in object_type.egg_protocols: for protocol in protocol_options: @@ -349,10 +371,12 @@ class ConfigLoader(_Loader): APP, next_name, global_conf) if 'use' in local_conf: context.filter_context = self._context_from_use( - FILTER, local_conf, global_conf, global_additions) + FILTER, local_conf, global_conf, global_additions, + section) else: context.filter_context = self._context_from_explicit( - FILTER, local_conf, global_conf, global_additions) + FILTER, local_conf, global_conf, global_additions, + section) return context def _pipeline_app_context(self, object_type, section, name, diff --git a/tests/conftest.py b/tests/conftest.py index 0cc7d9b..d48c2d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,3 @@ import pkg_resources sys.path.insert(0, base) #pkg_resources.require('Paste-Deploy') -# This is where applications we test go; these applications -# are only used for testing, they aren't "real". -sys.path.append(os.path.join(here, 'fake_packages')) - diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt index 8ecaf21..9bfc986 100644 --- a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -1,22 +1,22 @@ -[paste.app_factory1] +[paste.app_factory] basic_app=fakeapp.apps:make_basic_app other=fakeapp.apps:make_basic_app2 configed=fakeapp.configapps:SimpleApp.make_app -[paste.filter_factory1] +[paste.composit_factory] - caps=fakeapp.apps:make_cap_filter + remote_addr=fakeapp.apps:make_remote_addr -[paste.composit_factory1] +[paste.filter_app_factory] - remote_addr=fakeapp.apps:make_remote_addr + caps2=fakeapp.apps:CapFilter -[paste.filter_app_factory1] +[paste.filter_factory] - caps2=fakeapp.apps:CapFilter + caps=fakeapp.apps:make_cap_filter diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py index febdf9f..854483e 100644 --- a/tests/fake_packages/FakeApp.egg/setup.py +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -5,18 +5,18 @@ setup( version="1.0", packages=find_packages(), entry_points={ - 'paste.app_factory1': """ + 'paste.app_factory': """ basic_app=fakeapp.apps:make_basic_app other=fakeapp.apps:make_basic_app2 configed=fakeapp.configapps:SimpleApp.make_app """, - 'paste.composit_factory1': """ + 'paste.composit_factory': """ remote_addr=fakeapp.apps:make_remote_addr """, - 'paste.filter_factory1': """ + 'paste.filter_factory': """ caps=fakeapp.apps:make_cap_filter """, - 'paste.filter_app_factory1': """ + 'paste.filter_app_factory': """ caps2=fakeapp.apps:CapFilter """, }, diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini index aa24a61..cc0da7a 100644 --- a/tests/sample_configs/test_config_included.ini +++ b/tests/sample_configs/test_config_included.ini @@ -4,7 +4,7 @@ def3 = c [app:main] # Equivalent to the egg reference, but just for kicks... -paste.app_factory1 = fakeapp.configapps:SimpleApp.make_app +paste.app_factory = fakeapp.configapps:SimpleApp.make_app set glob = orig bob = your uncle another = BAR diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini index f948b97..bfad8dc 100644 --- a/tests/sample_configs/test_filter.ini +++ b/tests/sample_configs/test_filter.ini @@ -17,3 +17,6 @@ use = egg:FakeApp#caps2 method_to_call = lower next = normal +[app:inv] +use = egg:FakeApp#basic_app +filter-with = egg:FakeApp#caps diff --git a/tests/test_filter.py b/tests/test_filter.py index 5740223..6829ada 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -31,3 +31,9 @@ def test_pipeline2(): assert isinstance(app, fakeapp.apps.CapFilter) assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'upper' + +def test_filter_app_inverted(): + app = loadapp('config:sample_configs/test_filter.ini#inv', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app -- cgit v1.2.1 From c10f5564fdf7f336602c81b9977d5107e61e13de Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 25 Aug 2005 22:50:16 +0000 Subject: New setuptools tag command --- paste/deploy/tag.py | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 paste/deploy/tag.py diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py new file mode 100644 index 0000000..f865a81 --- /dev/null +++ b/paste/deploy/tag.py @@ -0,0 +1,159 @@ +from setuptools import Command +from distutils.errors import DistutilsOptionError +from pkg_resources import * +import subprocess +import re +import sys +import os +import shutil +import tempfile + +class tag(Command): + + description = "Tag for release" + + user_options = [ + ('version=', 'v', "Specify version"), + ('message=', 'm', "Specify a log message"), + ('build=', 'b', "Specify directory to build tag files in"), + ] + + version = None + message = None + build = None + + def initialize_options(self): + pass + + def finalize_options(self): + if self.version is None: + raise DistutilsOptionError( + "You must specify a version") + if self.message is None: + self.message = "Tagging %s version" % self.version + + _svn_url_re = re.compile(r'\bURL: (.*)') + _setup_version_re = re.compile(r'(version\s+=\s+)([^ \n\r,)]*)') + _egg_info_re = re.compile(r'^[egg_info]$') + + def run(self): + ei_cmd = self.get_finalized_command("egg_info") + path_item = normalize_path(ei_cmd.egg_base) + metadata = PathMetadata( + path_item, normalize_path(ei_cmd.egg_info) + ) + proc = subprocess.Popen( + ['svn', 'info', path_item], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate() + if stderr: + print 'Error from svn:' + print stderr + match = self._svn_url_re.search(stdout) + if not match: + print 'svn output did not contain "URL: ...":' + print stdout + assert 0 + svn_url = match.group(1) + if not svn_url.endswith('/trunk'): + print 'svn URL must end with "/trunk" (current: %r)' % svn_url + assert 0 + package_url = svn_url.rsplit('/', 1)[0] + tag_url = package_url + '/tags/' + self.version + # @@: Should check svn status + command = ['svn', 'cp', '--message', self.message, + svn_url, tag_url] + print ' '.join(command) + proc = subprocess.Popen(command) + proc.communicate() + tmpdir = tempfile.mkdtemp(prefix='tag_checkout_') + command = ['svn', 'co', '--quiet', tag_url, tmpdir] + print ' '.join(command) + subprocess.Popen(command).communicate() + self.update_setup_py(tmpdir) + self.update_setup_cfg(tmpdir) + print ' '.join(command) + subprocess.Popen(command).communicate() + command = ['svn', 'commit', '--message', + 'Auto-update of version strings', tmpdir] + print ' '.join(command) + subprocess.Popen(command).communicate() + print 'Removing %s' % tmpdir + shutil.rmtree(tmpdir) + + def update_setup_py(self, tmpdir): + setup_py = os.path.join(tmpdir, 'setup.py') + if not os.path.exists(setup_py): + print 'setup.py file cannot be found at %s' % setup_py + return + f = open(setup_py) + content = f.read() + f.close() + match = self._setup_version_re.search(content) + if not match: + print 'Cannot find version info in %s' % setup_py + else: + new_content = ( + content[:match.start()] + + match.group(1) + + repr(self.version) + + content[match.end():]) + if new_content == content: + print 'Version string up-to-date (edit trunk yourself)' + else: + f = open(setup_py, 'w') + f.write(new_content) + f.close() + print '%s version updated' % setup_py + command = [sys.executable, setup_py, 'egg_info'] + + def update_setup_cfg(self, tmpdir): + setup_cfg = os.path.join(tmpdir, 'setup.cfg') + if not os.path.exists(setup_cfg): + print 'setup.cfg file cannot be found at %s' % setup_cfg + return + f = open(setup_cfg) + content = f.readlines() + f.close() + new_content = [] + egg_info_content = [] + while content: + line = content.pop(0) + if line.strip() != '[egg_info]': + new_content.append(line) + else: + egg_info_content.append(line) + inner_line = None + while content: + inner_line = content.pop(0) + if inner_line.strip().startswith('['): + break + if inner_line.strip().startswith('tag_build'): + continue + elif inner_line.strip().startswith('tag_svn_revision'): + continue + if line.strip(): + egg_info_content.append(line) + if len(egg_info_content) == 1: + egg_info_content = [] + else: + egg_info_content.append('\n') + new_content.extend(egg_info_content) + if inner_line: + new_content.append(inner_line) + content = ''.join(content) + if not content: + command = ['svn', 'rm', setup_cfg] + print ' '.join(command) + subprocess.Popen(command).communicate() + return + if content != new_content: + f = open(setup_cfg, 'w') + f.write(new_content) + f.close() + print '%s updated' % setup_cfg + + + + -- cgit v1.2.1 From 33f85664e5074c81b17e08252a6d0e2cf7578b4e Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 16:16:12 +0000 Subject: More work on the tag command --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 9cd371d..4803c85 100644 --- a/setup.py +++ b/setup.py @@ -29,4 +29,8 @@ See also the `Subversion repository Date: Fri, 26 Aug 2005 16:18:17 +0000 Subject: Improvements of tag command --- paste/deploy/tag.py | 157 ++++++++++++++++++++++++---------------------------- 1 file changed, 72 insertions(+), 85 deletions(-) diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index f865a81..a916eae 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -1,5 +1,8 @@ from setuptools import Command -from distutils.errors import DistutilsOptionError +from setuptools.command import setopt +from ConfigParser import NoSectionError +from distutils.errors import * +from distutils import log from pkg_resources import * import subprocess import re @@ -31,7 +34,12 @@ class tag(Command): "You must specify a version") if self.message is None: self.message = "Tagging %s version" % self.version - + if self.build is None: + self.build = tempfile.mkdtemp(prefix='tag_checkout_') + if os.path.exists(self.build): + raise DistutilsOptionError( + "The build directory %s already exists" % self.build) + _svn_url_re = re.compile(r'\bURL: (.*)') _setup_version_re = re.compile(r'(version\s+=\s+)([^ \n\r,)]*)') _egg_info_re = re.compile(r'^[egg_info]$') @@ -42,57 +50,36 @@ class tag(Command): metadata = PathMetadata( path_item, normalize_path(ei_cmd.egg_info) ) - proc = subprocess.Popen( - ['svn', 'info', path_item], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = proc.communicate() - if stderr: - print 'Error from svn:' - print stderr + stdout = run_command(['svn', 'info', path_item]) match = self._svn_url_re.search(stdout) if not match: - print 'svn output did not contain "URL: ...":' - print stdout - assert 0 + raise DistutilsExecError( + 'svn output did not contain "URL: ...":\n' + stdout) svn_url = match.group(1) if not svn_url.endswith('/trunk'): - print 'svn URL must end with "/trunk" (current: %r)' % svn_url - assert 0 + raise DistutilsExecError( + 'svn URL must end with "/trunk" (current: %r)' % svn_url) package_url = svn_url.rsplit('/', 1)[0] tag_url = package_url + '/tags/' + self.version - # @@: Should check svn status - command = ['svn', 'cp', '--message', self.message, - svn_url, tag_url] - print ' '.join(command) - proc = subprocess.Popen(command) - proc.communicate() - tmpdir = tempfile.mkdtemp(prefix='tag_checkout_') - command = ['svn', 'co', '--quiet', tag_url, tmpdir] - print ' '.join(command) - subprocess.Popen(command).communicate() - self.update_setup_py(tmpdir) - self.update_setup_cfg(tmpdir) - print ' '.join(command) - subprocess.Popen(command).communicate() - command = ['svn', 'commit', '--message', - 'Auto-update of version strings', tmpdir] - print ' '.join(command) - subprocess.Popen(command).communicate() - print 'Removing %s' % tmpdir - shutil.rmtree(tmpdir) + run_command(['svn', 'cp', '--message', self.message, + svn_url, tag_url]) + run_command(['svn', 'co', '--quiet', tag_url, self.build]) + self.update_setup_py() + self.update_setup_cfg() + run_command(['svn', 'commit', '--message', + 'Auto-update of version strings', self.build]) - def update_setup_py(self, tmpdir): - setup_py = os.path.join(tmpdir, 'setup.py') + def update_setup_py(self): + setup_py = os.path.join(self.build, 'setup.py') if not os.path.exists(setup_py): - print 'setup.py file cannot be found at %s' % setup_py + log.warn('setup.py file cannot be found at %s' % setup_py) return f = open(setup_py) content = f.read() f.close() match = self._setup_version_re.search(content) if not match: - print 'Cannot find version info in %s' % setup_py + log.warn('Cannot find version info in %s' % setup_py) else: new_content = ( content[:match.start()] @@ -100,60 +87,60 @@ class tag(Command): + repr(self.version) + content[match.end():]) if new_content == content: - print 'Version string up-to-date (edit trunk yourself)' + log.info('Version string up-to-date (edit trunk yourself)') else: f = open(setup_py, 'w') f.write(new_content) f.close() - print '%s version updated' % setup_py + log.info('%s version updated' % setup_py) command = [sys.executable, setup_py, 'egg_info'] - def update_setup_cfg(self, tmpdir): - setup_cfg = os.path.join(tmpdir, 'setup.cfg') + def update_setup_cfg(self): + setup_cfg = os.path.join(self.build, 'setup.cfg') if not os.path.exists(setup_cfg): - print 'setup.cfg file cannot be found at %s' % setup_cfg + log.warn('setup.cfg file cannot be found at %s' % setup_cfg) return + try: + setopt.edit_config( + setup_cfg, + {'egg_info': {'tag_build': None, + 'tag_svn_revision': None}}) + except NoSectionError: + # No [egg_info]; that's okay + pass f = open(setup_cfg) - content = f.readlines() + content = f.read() f.close() - new_content = [] - egg_info_content = [] - while content: - line = content.pop(0) - if line.strip() != '[egg_info]': - new_content.append(line) - else: - egg_info_content.append(line) - inner_line = None - while content: - inner_line = content.pop(0) - if inner_line.strip().startswith('['): - break - if inner_line.strip().startswith('tag_build'): - continue - elif inner_line.strip().startswith('tag_svn_revision'): - continue - if line.strip(): - egg_info_content.append(line) - if len(egg_info_content) == 1: - egg_info_content = [] - else: - egg_info_content.append('\n') - new_content.extend(egg_info_content) - if inner_line: - new_content.append(inner_line) - content = ''.join(content) - if not content: - command = ['svn', 'rm', setup_cfg] - print ' '.join(command) - subprocess.Popen(command).communicate() - return - if content != new_content: - f = open(setup_cfg, 'w') - f.write(new_content) - f.close() - print '%s updated' % setup_cfg - + if not content.strip(): + log.info('%s empty; deleting' % setup_cfg) + run_command(['svn', 'rm', '--force', setup_cfg]) - - +def run_command(command_list, stdin=None): + log.info('Running %s', format_command(command_list)) + proc = subprocess.Popen(command_list, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate(stdin) + if stderr: + log.warn('Ouput from %s:\n%s', + format_command(command_list), + stderr) + returncode = proc.returncode + if returncode: + error = 'Exit code %s from %s' % ( + returncode, format_command(command_list)) + if stderr: + error += '; stderr output:\n' + stderr + raise Distutils.ExecError(error) + return stdout + +def format_command(lst): + return ' '.join(map(quote_command_item, lst)) + +def quote_command_item(item): + if ' ' in item: # @@: Obvious I should check more characters + for char in ('\\', '"', "'", '$'): + item = item.replace(char, '\\' + char) + item = '"%s"' % item + return item + -- cgit v1.2.1 From bd3b46d11aba9412e64f2ac7c5cc6b0bf3dea6ed Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 16:30:10 +0000 Subject: trunk will be 0.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4803c85..bbbf4cd 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.2' +version = '0.3' setup( name="PasteDeploy", -- cgit v1.2.1 From 08e80e6e52e15a4c07c75bcd15ef56132e37a8f2 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 16:32:57 +0000 Subject: New version sections --- docs/news.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 8ea663c..6ae0d45 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,10 @@ Paste Deployment News ===================== -Version 0.2 (svn trunk) ------------------------ +Version 0.2 +----------- + +Released 26 August 2004 * Added a ``filter-with`` setting to applications. @@ -23,4 +25,6 @@ Version 0.2 (svn trunk) Version 0.1 ----------- +Released 22 August 2004 + Initial version released. It's all new. -- cgit v1.2.1 From bc2775db87e3e22009f04a6a343c77f1d22bb044 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 16:35:03 +0000 Subject: Installation instruction changes --- docs/paste-deploy.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index e7852cb..f91dec4 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -37,7 +37,7 @@ First install `setuptools You can install Paste Deployment by installing `easy_install `_ and running:: - $ sudo easy_install.py http://svn.pythonpaste.org/Paste/Deploy/trunk + $ sudo easy_install.py PasteDeploy If you want to track development, do:: @@ -48,6 +48,9 @@ If you want to track development, do:: This will install the package globally, but will load the files in the checkout. +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + Basic Usage ----------- -- cgit v1.2.1 From b23cc22b62aebe1ccbb741d189ac84bc6ca30f6e Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 18:26:16 +0000 Subject: Added new variable setting to make globals local --- docs/news.txt | 9 +++++++++ paste/deploy/loadwsgi.py | 6 ++++++ tests/sample_configs/test_config.ini | 6 ++++++ tests/test_config.py | 10 ++++++++++ 4 files changed, 31 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index 6ae0d45..c4e6a42 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,15 @@ Paste Deployment News ===================== +Version 0.3 (svn trunk) +----------------------- + +* Allow variable setting like:: + + get local_var = global_var_name + + To bring in global variables to the local scope. + Version 0.2 ----------- diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 9e892d7..f20d03d 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -285,16 +285,22 @@ class ConfigLoader(_Loader): global_conf.update(defaults) 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 is APP and 'filter-with' in local_conf: filter_with = local_conf.pop('filter-with') else: diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini index 85c66a1..6459df1 100644 --- a/tests/sample_configs/test_config.ini +++ b/tests/sample_configs/test_config.ini @@ -24,3 +24,9 @@ another = something more use = config:test_config_included.ini set glob = override another = FOO + +[app:test_get] +use = egg:FakeApp#configed +set def2 = TEST +get def1 = def1 +get foo = def2 diff --git a/tests/test_config.py b/tests/test_config.py index 11d121e..72e864e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -53,3 +53,13 @@ def test_foreign_config(): 'def3': 'c', 'glob': 'override'} +def test_config_get(): + app = loadapp(ini_file, relative_to=here, name='test_get') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'def1': 'a', + 'foo': 'TEST'} + assert app.global_conf == { + 'def1': 'a', + 'def2': 'TEST'} + -- cgit v1.2.1 From ee1355bc128674a2cec8e03d478e9c1592974c92 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 26 Aug 2005 22:53:02 +0000 Subject: (re)Added a configuration middleware --- paste/deploy/__init__.py | 1 + paste/deploy/config.py | 163 +++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 paste/deploy/config.py diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index 5c58ac1..5c83a6d 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1,2 +1,3 @@ from loadwsgi import loadapp, loadfilter, loadserver +from config import CONFIG diff --git a/paste/deploy/config.py b/paste/deploy/config.py new file mode 100644 index 0000000..4b6a2d7 --- /dev/null +++ b/paste/deploy/config.py @@ -0,0 +1,163 @@ +from paste.util.threadinglocal import local +import threading +# Loaded lazily +wsgilib = None + +__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware'] + +config_local = local() + +def local_dict(): + try: + return config_local.wsgi_dict + except AttributeError: + config_local.wsgi_dict = result = {} + return result + +class DispatchingConfig(object): + + """ + 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 local_dict().has_key(self._local_key): + 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 not conf: + 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 not conf: + raise TypeError( + "No configuration has been registered for this process " + "or thread") + return conf[key] + +CONFIG = DispatchingConfig() + +class ConfigMiddleware(object): + + """ + 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: + from paste import wsgilib + 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 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) + 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) + diff --git a/setup.py b/setup.py index bbbf4cd..73a445a 100644 --- a/setup.py +++ b/setup.py @@ -26,11 +26,13 @@ See also the `Subversion repository Date: Sat, 27 Aug 2005 09:25:19 +0000 Subject: More notes --- docs/paste-deploy.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt index f91dec4..ceb8a42 100644 --- a/docs/paste-deploy.txt +++ b/docs/paste-deploy.txt @@ -479,3 +479,13 @@ Outstanding Issues code isn't usable with Python strings without a thin wrapper to translate objects into their proper types. +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + -- cgit v1.2.1 From d5e3d7bea6178d25f9691a0bd2460b0c2ad4ee4d Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 29 Aug 2005 18:29:10 +0000 Subject: Better error message --- paste/deploy/loadwsgi.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index f20d03d..c00a72d 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -488,10 +488,16 @@ class EggLoader(_Loader): break if not possible: # Better exception + dist = pkg_resources.get_distribution(self.spec) raise LookupError( - "Entry point %r not found in egg %r (protocols: %s)" + "Entry point %r not found in egg %r (dir: %s; protocols: %s; " + "entry_points: %s)" % (name, self.spec, - ', '.join(_flatten(object_type.egg_protocols)))) + dist.location, + ', '.join(_flatten(object_type.egg_protocols)), + ', '.join(_flatten([ + (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)" -- cgit v1.2.1 From 9ec0b1400fa7b84158462bd737e3a3eb33a946cd Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 6 Sep 2005 23:29:56 +0000 Subject: Allow ini files to be used as executables, with paster as the #! line runner; doesn't yet do anything with those files --- tests/sample_configs/executable.ini | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 tests/sample_configs/executable.ini diff --git a/tests/sample_configs/executable.ini b/tests/sample_configs/executable.ini new file mode 100755 index 0000000..3b75fe9 --- /dev/null +++ b/tests/sample_configs/executable.ini @@ -0,0 +1,10 @@ +#!/usr/bin/env paster +[exe] +sys.path = /fake/path/ + /another/fake/path ../fake_packages/ + +[server] +use = egg:PasteScript#cgi + +[app] +use = egg:FakeApp#basic_app -- cgit v1.2.1 From 8d1ed92d0d0179b1eb27117cad0f7546b9b09efc Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 8 Sep 2005 17:41:19 +0000 Subject: Moved paste_deploy create template into PasteDeploy --- .../paster_templates/paste_deploy/docs/config.txt | 1 + .../paste_deploy/docs/devel_config.ini | 25 ++++++++++++++++++++++ .../paste_deploy/docs/example_deploy.ini | 3 +++ setup.py | 13 ++++++----- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/config.txt create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini diff --git a/paste/deploy/paster_templates/paste_deploy/docs/config.txt b/paste/deploy/paster_templates/paste_deploy/docs/config.txt new file mode 100644 index 0000000..cf6543d --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/config.txt @@ -0,0 +1 @@ +# Description of all the configuration parameters diff --git a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini new file mode 100644 index 0000000..65e4aff --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini @@ -0,0 +1,25 @@ +[DEFAULT] +client_name = test_client +email_errors = ${author_email|empty} +config_root = . + +[app:main] +use = egg:PasteWebKit +app_name = ${package} +package_name = ${package} +# @@: This should be moved into a SQLObject template: +database = postgres://pgsql@/${package} +debug = true +database_module = ${package}.db +get client_name = client_name +get email_errors = email_errors + +[app:test] +use = main +# Disable emailing of errors: +email_errors = + +# @@: This should be moved into a SQLObject template: +database = postgres://pgsql@/${package}_test +# @@: sqlobject-admin can't pick this up from the use :( +database_module = ${package}.db diff --git a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini new file mode 100644 index 0000000..b497a40 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini @@ -0,0 +1,3 @@ +[config:live] +#database = postgres://pgsql@/${package} + diff --git a/setup.py b/setup.py index 73a445a..b9aa9c5 100644 --- a/setup.py +++ b/setup.py @@ -28,11 +28,14 @@ See also the `Subversion repository Date: Fri, 9 Sep 2005 18:06:13 +0000 Subject: Forgot to add this module in the last commit --- paste/deploy/paster_templates.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 paste/deploy/paster_templates.py diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py new file mode 100644 index 0000000..f533519 --- /dev/null +++ b/paste/deploy/paster_templates.py @@ -0,0 +1,9 @@ +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'] + -- cgit v1.2.1 From 2cac462d66274da932be883db58b4b8cb4deec36 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 9 Sep 2005 18:38:05 +0000 Subject: Fixed error-checking bug --- paste/deploy/tag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index a916eae..6fa904f 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -34,11 +34,11 @@ class tag(Command): "You must specify a version") if self.message is None: self.message = "Tagging %s version" % self.version - if self.build is None: - self.build = tempfile.mkdtemp(prefix='tag_checkout_') - if os.path.exists(self.build): + if self.build and os.path.exists(self.build): raise DistutilsOptionError( "The build directory %s already exists" % self.build) + if self.build is None: + self.build = tempfile.mkdtemp(prefix='tag_checkout_') _svn_url_re = re.compile(r'\bURL: (.*)') _setup_version_re = re.compile(r'(version\s+=\s+)([^ \n\r,)]*)') -- cgit v1.2.1 From 8aabfd4ffc01fcc015498d1601e01ed3c63f38dd Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 9 Sep 2005 22:33:40 +0000 Subject: Typo --- paste/deploy/tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index 6fa904f..04fbc75 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -131,7 +131,7 @@ def run_command(command_list, stdin=None): returncode, format_command(command_list)) if stderr: error += '; stderr output:\n' + stderr - raise Distutils.ExecError(error) + raise DistutilsExecError(error) return stdout def format_command(lst): -- cgit v1.2.1 From 2c74a0f230e6681489e5f9821e34a20f85f871b1 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 14 Sep 2005 22:42:13 +0000 Subject: Added _tmpl to files that should be substituted --- .../paste_deploy/docs/devel_config.ini | 25 ---------------------- .../paste_deploy/docs/devel_config.ini_tmpl | 25 ++++++++++++++++++++++ .../paste_deploy/docs/example_deploy.ini | 3 --- .../paste_deploy/docs/example_deploy.ini_tmpl | 3 +++ 4 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl delete mode 100644 paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl diff --git a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini deleted file mode 100644 index 65e4aff..0000000 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini +++ /dev/null @@ -1,25 +0,0 @@ -[DEFAULT] -client_name = test_client -email_errors = ${author_email|empty} -config_root = . - -[app:main] -use = egg:PasteWebKit -app_name = ${package} -package_name = ${package} -# @@: This should be moved into a SQLObject template: -database = postgres://pgsql@/${package} -debug = true -database_module = ${package}.db -get client_name = client_name -get email_errors = email_errors - -[app:test] -use = main -# Disable emailing of errors: -email_errors = - -# @@: This should be moved into a SQLObject template: -database = postgres://pgsql@/${package}_test -# @@: sqlobject-admin can't pick this up from the use :( -database_module = ${package}.db 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 new file mode 100644 index 0000000..65e4aff --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -0,0 +1,25 @@ +[DEFAULT] +client_name = test_client +email_errors = ${author_email|empty} +config_root = . + +[app:main] +use = egg:PasteWebKit +app_name = ${package} +package_name = ${package} +# @@: This should be moved into a SQLObject template: +database = postgres://pgsql@/${package} +debug = true +database_module = ${package}.db +get client_name = client_name +get email_errors = email_errors + +[app:test] +use = main +# Disable emailing of errors: +email_errors = + +# @@: This should be moved into a SQLObject template: +database = postgres://pgsql@/${package}_test +# @@: sqlobject-admin can't pick this up from the use :( +database_module = ${package}.db diff --git a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini deleted file mode 100644 index b497a40..0000000 --- a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini +++ /dev/null @@ -1,3 +0,0 @@ -[config:live] -#database = postgres://pgsql@/${package} - diff --git a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl new file mode 100644 index 0000000..b497a40 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl @@ -0,0 +1,3 @@ +[config:live] +#database = postgres://pgsql@/${package} + -- cgit v1.2.1 From b10f52169df959984d2ad92c721e5ea34f029a65 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 15 Sep 2005 05:07:45 +0000 Subject: Don't require subprocess module to be loaded (since pre-Python 2.4 it can't be) --- paste/deploy/tag.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index 04fbc75..40e507c 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -4,7 +4,11 @@ from ConfigParser import NoSectionError from distutils.errors import * from distutils import log from pkg_resources import * -import subprocess +try: + import subprocess +except ImportError: + # pre-Python 2.4 + pass import re import sys import os -- cgit v1.2.1 From 63f9b872bc42f3269d0a8fd2224e45652bfb1c5a Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 16 Sep 2005 18:48:26 +0000 Subject: Removed database_module setting --- paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 3 --- 1 file changed, 3 deletions(-) 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 index 65e4aff..5fef219 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -10,7 +10,6 @@ package_name = ${package} # @@: This should be moved into a SQLObject template: database = postgres://pgsql@/${package} debug = true -database_module = ${package}.db get client_name = client_name get email_errors = email_errors @@ -21,5 +20,3 @@ email_errors = # @@: This should be moved into a SQLObject template: database = postgres://pgsql@/${package}_test -# @@: sqlobject-admin can't pick this up from the use :( -database_module = ${package}.db -- cgit v1.2.1 From 0bb3376fa08ee7d5dc7f15c7b31a12d8817f873b Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 18 Sep 2005 23:18:21 +0000 Subject: Made basic_package required --- paste/deploy/paster_templates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py index f533519..61a5ad6 100644 --- a/paste/deploy/paster_templates.py +++ b/paste/deploy/paster_templates.py @@ -7,3 +7,4 @@ class PasteDeploy(Template): egg_plugins = ['PasteDeploy'] + required_templates = ['PasteScript#basic_package'] -- cgit v1.2.1 From 49c98ebb0135cc7bf6976559d565332e3b692349 Mon Sep 17 00:00:00 2001 From: rflosi Date: Mon, 19 Sep 2005 20:15:04 +0000 Subject: Added server section --- .../deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 5fef219..4d2b2ad 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -20,3 +20,9 @@ email_errors = # @@: This should be moved into a SQLObject template: database = postgres://pgsql@/${package}_test + +[server:main] +use = egg:PasteScript#wsgiutils +# Change to 0.0.0.0 to make public: +host = 127.0.0.1 +port = 8080 -- cgit v1.2.1 From e97461d5bcc0e01aa2faa10885a2adc53a73509c Mon Sep 17 00:00:00 2001 From: rflosi Date: Tue, 20 Sep 2005 17:57:10 +0000 Subject: Specially process require keys --- paste/deploy/loadwsgi.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index c00a72d..0295f69 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -305,6 +305,10 @@ class ConfigLoader(_Loader): 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, -- cgit v1.2.1 From 338bb011e8a8c5fa86568b694b5dd3025fb3ad97 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 21 Sep 2005 07:44:38 +0000 Subject: Check out the directory when you are through --- paste/deploy/tag.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index 40e507c..00cbb8a 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -72,6 +72,7 @@ class tag(Command): self.update_setup_cfg() run_command(['svn', 'commit', '--message', 'Auto-update of version strings', self.build]) + self.checkout_tag(tag_url) def update_setup_py(self): setup_py = os.path.join(self.build, 'setup.py') @@ -118,6 +119,24 @@ class tag(Command): if not content.strip(): log.info('%s empty; deleting' % setup_cfg) run_command(['svn', 'rm', '--force', setup_cfg]) + + def checkout_tag(self, tag_url): + # Put it in the parent directory: + ext = '' + while 1: + base = os.path.join(os.path.dirname(os.getcwd()), + '%s-%s%s' % (self.distribution.metadata.name, self.version, ext)) + if os.path.exists(base): + log.warn('Directory %s exists' % base) + if ext: + ext += 1 + else: + ext = 0 + else: + break + run_command(['svn', 'checkout', tag_url, base]) + log.info('Checked out tag into %s' % base) + def run_command(command_list, stdin=None): log.info('Running %s', format_command(command_list)) -- cgit v1.2.1 From 331c51a5aa0b86f02e141821dcea64cf979f0f9e Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 21 Sep 2005 20:35:56 +0000 Subject: Added better dependency information; fixed package_data; put requirements into the config middleware --- paste/deploy/config.py | 14 +++++++++++--- setup.py | 12 ++++++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 4b6a2d7..b6fc546 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -1,15 +1,21 @@ -from paste.util.threadinglocal import local import threading # Loaded lazily wsgilib = None +local = None __all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware'] -config_local = local() - def local_dict(): + global config_local, local try: return config_local.wsgi_dict + except NameError: + import pkg_resources + pkg_resources.require('Paste') + from paste.util.threadinglocal import local + config_local = local() + config_local.wsgi_dict = result = {} + return result except AttributeError: config_local.wsgi_dict = result = {} return result @@ -134,6 +140,8 @@ class ConfigMiddleware(object): def __call__(self, environ, start_response): global wsgilib if wsgilib is None: + import pkg_resources + pkg_resources.require('Paste') from paste import wsgilib conf = environ['paste.config'] = self.config.copy() app_iter = None diff --git a/setup.py b/setup.py index b9aa9c5..ac3ddf9 100644 --- a/setup.py +++ b/setup.py @@ -27,13 +27,21 @@ See also the `Subversion repository Date: Wed, 21 Sep 2005 21:31:23 +0000 Subject: Fixes suggested by pyflakes --- paste/deploy/loadwsgi.py | 6 +++--- paste/deploy/tag.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 0295f69..48ebf4b 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -229,11 +229,11 @@ class _Loader(object): def get_filter(self, name=None, global_conf=None): return self.filter_context( - name=naame, global_conf=global_conf).create() + name=name, global_conf=global_conf).create() def get_server(self, name=None, global_conf=None): return self.server_context( - name=naame, global_conf=global_conf).create() + name=name, global_conf=global_conf).create() def app_context(self, name=None, global_conf=None): return self.get_context( @@ -505,7 +505,7 @@ class EggLoader(_Loader): if len(possible) > 1: raise LookupError( "Ambiguous entry points for %r in egg %r (protocols: %s)" - % (name, self.spec, ', '.join(_flatten(protocol_list)))) + % (name, self.spec, ', '.join(_flatten(protocol_options)))) return possible[0] class LoaderContext(object): diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py index 00cbb8a..bb01d3a 100644 --- a/paste/deploy/tag.py +++ b/paste/deploy/tag.py @@ -12,7 +12,6 @@ except ImportError: import re import sys import os -import shutil import tempfile class tag(Command): -- cgit v1.2.1 From 05a1e4e2706255d344956cafd8a0736af6ab7ab6 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 25 Sep 2005 01:07:41 +0000 Subject: Allow variable interpolation, including %(here)s --- docs/news.txt | 5 +++++ paste/deploy/loadwsgi.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index c4e6a42..9026145 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -10,6 +10,11 @@ Version 0.3 (svn trunk) To bring in global variables to the local scope. +* Allow interpolation in files, like ``%(here)s``. Anything in the + ``[DEFAULTS]`` section will be available to substitute into a value, + as will variables in the same section. Also, the special value + ``here`` will be the directory the configuration file is located in. + Version 0.2 ----------- diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 48ebf4b..d5a1118 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,7 +1,7 @@ import os import re import urllib -from ConfigParser import RawConfigParser +from ConfigParser import ConfigParser import pkg_resources __all__ = ['loadapp', 'loadserver', 'loadfilter'] @@ -260,7 +260,7 @@ class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename - self.parser = RawConfigParser() + self.parser = ConfigParser() # Don't lower-case keys: self.parser.optionxform = str # Stupid ConfigParser ignores files that aren't found, so @@ -269,6 +269,7 @@ class ConfigLoader(_Loader): raise OSError( "File %s not found" % filename) self.parser.read(filename) + self.parser._defaults['here'] = os.path.dirname(filename) def get_context(self, object_type, name=None, global_conf=None): if self.absolute_name(name): -- cgit v1.2.1 From cf0a61b51ee698838a773748e040037cf6e1a6d7 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 25 Sep 2005 01:36:57 +0000 Subject: Don't override here if it is already set; make sure here is an absolute location --- paste/deploy/loadwsgi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index d5a1118..2518a4f 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -269,7 +269,8 @@ class ConfigLoader(_Loader): raise OSError( "File %s not found" % filename) self.parser.read(filename) - self.parser._defaults['here'] = os.path.dirname(filename) + self.parser._defaults.setdefault( + 'here', os.path.dirname(os.path.abspath(filename))) def get_context(self, object_type, name=None, global_conf=None): if self.absolute_name(name): -- cgit v1.2.1 From 2dddae4e26f4fb95e83217dfa9b5a6ede5e6a6c0 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 26 Sep 2005 18:14:26 +0000 Subject: Changed to special dir, instead of curdir --- paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4d2b2ad..05a80fb 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -1,7 +1,7 @@ [DEFAULT] client_name = test_client email_errors = ${author_email|empty} -config_root = . +config_root = [app:main] use = egg:PasteWebKit -- cgit v1.2.1 From 9fac7099015d36b36dbaa673a5f393ae3c2e2439 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 26 Sep 2005 18:14:49 +0000 Subject: Made a parser subclass that gives better error messages --- paste/deploy/loadwsgi.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 2518a4f..5e743ea 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -36,6 +36,23 @@ def _flatten(lst): result.extend(_flatten(item)) return result +class NicerConfigParser(ConfigParser): + + def __init__(self, filename, *args, **kw): + ConfigParser.__init__(self, *args, **kw) + self.filename = filename + + def _interpolate(self, section, option, rawval, vars): + try: + return ConfigParser._interpolate( + self, section, option, rawval, vars) + except Exception, e: + args = list(e.args) + args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( + self.filename, section, option, rawval) + e.args = tuple(args) + raise + ############################################################ ## Object types ############################################################ @@ -260,7 +277,7 @@ class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename - self.parser = ConfigParser() + self.parser = ConfigParser(self.filename) # Don't lower-case keys: self.parser.optionxform = str # Stupid ConfigParser ignores files that aren't found, so -- cgit v1.2.1 From 95575119b0c45b74b7d3ef2a6ea523d81a115006 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 26 Sep 2005 21:05:41 +0000 Subject: Typo --- paste/deploy/loadwsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 5e743ea..c7e95a3 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -277,7 +277,7 @@ class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename - self.parser = ConfigParser(self.filename) + self.parser = NicerConfigParser(self.filename) # Don't lower-case keys: self.parser.optionxform = str # Stupid ConfigParser ignores files that aren't found, so -- cgit v1.2.1 From f4227149c1d06c14fc4c6a531a9eeb78b58c05ba Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 27 Sep 2005 06:56:13 +0000 Subject: Moved docs around --- docs/index.txt | 491 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/paste-deploy.txt | 491 -------------------------------------------------- docs/rebuild | 20 -- setup.cfg | 17 ++ 4 files changed, 508 insertions(+), 511 deletions(-) create mode 100644 docs/index.txt delete mode 100644 docs/paste-deploy.txt delete mode 100755 docs/rebuild diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..ceb8a42 --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,491 @@ +Paste Deployment +================ + +:author: Ian Bicking +:revision: $Rev$ +:date: $LastChangedDate$ + +.. contents:: + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +To see updates that have been made to Paste Deploy see the `news file +`_. + +Installation +------------ + +First install `setuptools +`_. + +You can install Paste Deployment by installing `easy_install +`_ and running:: + + $ sudo easy_install.py PasteDeploy + +If you want to track development, do:: + + $ svn co http://svn.pythonpaste.org/Paste/Deploy/trunk Paste-Deploy + $ cd Paste-Deploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. + +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composit Applications +~~~~~~~~~~~~~~~~~~~~~ + +"Composit" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composit:main] + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composit application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +Filter Composition +~~~~~~~~~~~~~~~~~~ + +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + + [filter:filter1] + ... + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ that adds +metadata to a normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory``, +``paste.composit_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composit_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composits are just slightly more complex:: + + def composit_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composit factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composit:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.filter_app_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + +``paste.server_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +An implementation of ``Server`` is left to the user. + +``paste.server_runner`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + +Outstanding Issues +------------------ + +* Should add a ``python:`` scheme for loading objects out of modules + directly. It has to include the protocol somehow...? + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory`` and ``paste.composit_factory`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + diff --git a/docs/paste-deploy.txt b/docs/paste-deploy.txt deleted file mode 100644 index ceb8a42..0000000 --- a/docs/paste-deploy.txt +++ /dev/null @@ -1,491 +0,0 @@ -Paste Deployment -================ - -:author: Ian Bicking -:revision: $Rev$ -:date: $LastChangedDate$ - -.. contents:: - -Introduction ------------- - -Paste Deployment is a system for finding and configuring WSGI -applications and servers. For WSGI application consumers it provides -a single, simple function (``loadapp``) for loading a WSGI application -from a configuration file or a Python Egg. For WSGI application -providers it only asks for a single, simple entry point to your -application, so that application users don't need to be exposed to the -implementation details of your application. - -The result is something a system administrator can install and manage -without knowing any Python, or the details of the WSGI application or -its container. - -Paste Deployment currently does not require other parts of `Paste -`_, and is distributed as a separate package. - -To see updates that have been made to Paste Deploy see the `news file -`_. - -Installation ------------- - -First install `setuptools -`_. - -You can install Paste Deployment by installing `easy_install -`_ and running:: - - $ sudo easy_install.py PasteDeploy - -If you want to track development, do:: - - $ svn co http://svn.pythonpaste.org/Paste/Deploy/trunk Paste-Deploy - $ cd Paste-Deploy - $ sudo python setup.py develop - -This will install the package globally, but will load the files in the -checkout. - -For downloads and other information see the `Cheese Shop PasteDeploy -page `_. - -Basic Usage ------------ - -The basic way you'll use Paste Deployment is to load `WSGI -`_ applications. Many -Python frameworks now support WSGI, so applications written for these -frameworks should be usable. - -The primary function is ``paste.deploy.loadapp``. This loads an -application given a URI. You can use it like:: - - from paste.deploy import loadapp - wsgi_app = loadapp('config:/path/to/config.ini') - -There's two URI formats currently supported: ``config:`` and ``egg:``. - -``config:`` URIs ----------------- - -URIs that being with ``config:`` refer to configuration files. These -filenames can be relative if you pass the ``relative_to`` keyword -argument to ``loadapp()``. - -.. note:: - - Filenames are never considered relative to the current working - directory, as that is a unpredictable location. Generally when - a URI has a context it will be seen as relative to that context; - for example, if you have a ``config:`` URI inside another - configuration file, the path is considered relative to the - directory that contains that configuration file. - -Config Format -~~~~~~~~~~~~~ - -Configuration files are in the INI format. This is a simple format -that looks like:: - - [section_name] - key = value - another key = a long value - that extends over multiple lines - -All values are strings (no quoting is necessary). The keys and -section names are case-sensitive, and may contain punctuation and -spaces (though both keys and values are stripped of leading and -trailing whitespace). Lines can be continued with leading whitespace. - -Lines beginning with ``#`` (preferred) or ``;`` are considered -comments. - -Applications -~~~~~~~~~~~~ - -You can define multiple applications in a single file; each -application goes in its own section. Even if you have just one -application, you must put it in a section. - -Each section name defining an application should be prefixed with -``app:``. The "main" section (when just defining one application) -would go in ``[app:main]`` or just ``[app]``. - -There's two ways to indicate the Python code for the application. The -first is to refer to another URI or name:: - - [app:myapp] - use = config:another_config_file.ini#app_name - - # or any URI: - [app:myotherapp] - use = egg:MyApp - - # or even another section: - [app:mylastapp] - use = myotherapp - -It would seem at first that this was pointless; just a way to point to -another location. However, in addition to loading the application -from that location, you can also add or change the configuration. - -The other way to define an application is to point exactly to some -Python code:: - - [app:myapp] - paste.app_factory = myapp.modulename:app_factory - -You must give an explicit *protocol* (in this case -``paste.app_factory``), and the value is something to import. In -this case the module ``myapp.modulename`` is loaded, and the -``app_factory`` object retrieved from it. - -See `Defining Factories`_ for more about the protocols. - -Configuration -~~~~~~~~~~~~~ - -Configuration is done through keys besides ``use`` (or the protocol -names). Any other keys found in the section will be passed as keyword -arguments to the factory. This might look like:: - - [app:blog] - use = egg:MyBlog - database = mysql://localhost/blogdb - blogname = This Is My Blog! - -You can override these in other sections, like:: - - [app:otherblog] - use = blog - blogname = The other face of my blog - -This way some settings could be defined in a generic configuration -file (if you have ``use = config:other_config_file``) or you can -publish multiple (more specialized) applications just by adding a -section. - -Global Configuration -~~~~~~~~~~~~~~~~~~~~ - -Often many applications share the same configuration. While you can -do that a bit by using other config sections and overriding values, -often you want that done for a bunch of disparate configuration -values. And typically applications can't take "extra" configuration -parameters; with global configuration you do something equivalent to -"if this application wants to know the admin email, this is it". - -Applications are passed the global configuration separately, so they -must specifically pull values out of it; typically the global -configuration serves as the basis for defaults when no local -configuration is passed in. - -Global configuration to apply to every application defined in a file -should go in a special section named ``[DEFAULT]``. You can override -global configuration locally like:: - - [DEFAULT] - admin_email = webmaster@example.com - - [app:main] - use = ... - set admin_email = bob@example.com - -That is, by using ``set`` in front of the key. - -Composit Applications -~~~~~~~~~~~~~~~~~~~~~ - -"Composit" applications are things that act like applications, but -are made up of other applications. One example would be a URL mapper, -where you mount applications at different URL paths. This might look -like:: - - [composit:main] - / = mainapp - /files = staticapp - - [app:mainapp] - use = egg:MyApp - - [app:staticapp] - use = egg:Paste#static - document_root = /path/to/docroot - -The composit application "main" is just like any other application -from the outside (you load it with ``loadapp`` for instance), but it -has access to other applications defined in the configuration file. - -Other Objects -~~~~~~~~~~~~~ - -In addition to sections with ``app:``, you can define filters and -servers in a configuration file, with ``server:`` and ``filter:`` -prefixes. You load these with ``loadserver`` and ``loadfilter``. The -configuration works just the same; you just get back different kinds -of objects. - -Filter Composition -~~~~~~~~~~~~~~~~~~ - -There are several ways to apply filters to applications. It mostly -depends on how many filters, and in what order you want to apply them. - -The first way is to use the ``filter-with`` setting, like:: - - [app:main] - use = egg:MyEgg - filter-with = printdebug - - [filter:printdebug] - use = egg:Paste#printdebug - -Also, two special section types exist to apply filters to your -applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of -these sections define applications, and so can be used wherever an -application is needed. - -``filter-app`` defines a filter (just like you would in a -``[filter:...]`` section), and then a special key ``next`` which -points to the application to apply the filter to. - -``pipeline:`` is used when you need apply a number of filters. It -takes *one* configuration key ``pipeline`` (plus any global -configuration overrides you want). ``pipeline`` is a list of filters -ended by an application, like:: - - [pipeline:main] - pipeline = filter1 egg:FilterEgg#filter2 filter3 app - - [filter:filter1] - ... - -``egg:`` URIs -------------- - -`Python Eggs `_ -are a distribution and installation format produced by `setuptools -`_ that adds -metadata to a normal Python package (among other things). - -You don't need to understand a whole lot about Eggs to use them. If -you have a `distutils -`_ -``setup.py`` script, just change:: - - from distutils.core import setup - -to:: - - from setuptools import setup - -Now when you install the package it will be installed as an egg. - -The first important part about an Egg is that it has a -*specification*. This is formed from the name of your distribution -(the ``name`` keyword argument to ``setup()``), and you can specify a -specific version. So you can have an egg named ``MyApp``, or -``MyApp==0.1`` to specify a specific version. - -The second is *entry points*. These are references to Python objects -in your packages that are named and have a specific protocol. -"Protocol" here is just a way of saying that we will call them with -certain arguments, and expect a specific return value. We'll talk -more about the protocols later_. - -.. _later: `Defining Factories`_ - -The important part here is how we define entry points. You'll add an -argument to ``setup()`` like:: - - setup( - name='MyApp', - ... - entry_points={ - 'paste.app_factory': [ - 'main=myapp.mymodule:app_factory', - 'ob2=myapp.mymodule:ob_factory'], - }, - ) - -This defines two applications named ``main`` and ``ob2``. You can -then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, -since ``main`` is the default) and ``egg:MyApp#ob2``. - -The values are instructions for importing the objects. ``main`` is -located in the ``myapp.mymodule`` module, in an object named -``app_factory``. - -There's no way to add configuration to objects imported as Eggs. - -Defining Factories ------------------- - -This lets you point to factories (that obey the specific protocols we -mentioned). But that's not much use unless you can create factories -for your applications. - -There's a few protocols: ``paste.app_factory``, -``paste.composit_factory``, ``paste.filter_factory``, and lastly -``paste.server_factory``. Each of these expects a callable (like a -function, method, or class). - -``paste.app_factory`` -~~~~~~~~~~~~~~~~~~~~~~ - -The application is the most common. You define one like:: - - def app_factory(global_config, **local_conf): - return wsgi_app - -The ``global_config`` is a dictionary, and local configuration is -passed as keyword arguments. The function returns a WSGI application. - -``paste.composit_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Composits are just slightly more complex:: - - def composit_factory(loader, global_config, **local_conf): - return wsgi_app - -The ``loader`` argument is an object that has a couple interesting -methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI -application with the given name. ``get_filter`` and ``get_server`` -work the same way. - -A more interesting example might be a composit factory that does -something. For instance, consider a "pipeline" application:: - - def pipeline_factory(loader, global_config, pipeline): - # space-separated list of filter and app names: - pipeline = pipeline.split() - filters = [loader.get_filter(n) for n in pipeline[:-1]] - app = loader.get_app(pipeline[-1]) - filters.reverse() # apply in reverse order! - for filter in filters: - app = filter(app) - return app - -Then we use it like:: - - [composit:main] - use = - pipeline = egg:Paste#printdebug session myapp - - [filter:session] - use = egg:Paste#session - store = memory - - [app:myapp] - use = egg:MyApp - -``paste.filter_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Filter factories are just like app factories (same signature), except -they return filters. Filters are callables that take a WSGI -application as the only argument, and return a "filtered" version of -that application. - -Here's an example of a filter that checks that the ``REMOTE_USER`` CGI -variable is set, creating a really simple authentication filter:: - - def auth_filter_factory(global_conf, req_usernames): - # space-separated list of usernames: - req_usernames = req_usernames.split() - def filter(app): - return AuthFilter(app, req_usernames) - return filter - - class AuthFilter(object): - def __init__(self, app, req_usernames): - self.app = app - self.req_usernames = req_usernames - - def __call__(self, environ, start_response): - if environ.get('REMOTE_USER') in self.req_usernames: - return self.app(environ, start_response) - start_response( - '403 Forbidden', [('Content-type', 'text/html')]) - return ['You are forbidden to view this resource'] - -``paste.filter_app_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is very similar to ``paste.filter_factory``, except that it also -takes a ``wsgi_app`` argument, and returns a WSGI application. So if -you changed the above example to:: - - class AuthFilter(object): - def __init__(self, app, global_conf, req_usernames): - .... - -Then ``AuthFilter`` would serve as a filter_app_factory -(``req_usernames`` is a required local configuration key in this -case). - -``paste.server_factory`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -This takes the same signature as applications and filters, but returns -a server. - -A server is a callable that takes a single argument, a WSGI -application. It then serves the application. - -An example might look like:: - - def server_factory(global_conf, host, port): - port = int(port) - def serve(app): - s = Server(app, host=host, port=port) - s.serve_forever() - return serve - -An implementation of ``Server`` is left to the user. - -``paste.server_runner`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -Like ``paste.server_factory``, except ``wsgi_app`` is passed as the -first argument, and the server should run immediately. - -Outstanding Issues ------------------- - -* Should add a ``python:`` scheme for loading objects out of modules - directly. It has to include the protocol somehow...? - -* Should there be a "default" protocol for each type of object? Since - there's currently only one protocol, it seems like it makes sense - (in the future there could be multiple). Except that - ``paste.app_factory`` and ``paste.composit_factory`` overlap - considerably. - -* ConfigParser's INI parsing is kind of annoying. I'd like it both - more constrained and less constrained. Some parts are sloppy (like - the way it interprets ``[DEFAULT]``). - -* ``config:`` URLs should be potentially relative to other locations, - e.g., ``config:$docroot/...``. Maybe using variables from - ``global_conf``? - -* Should other variables have access to ``global_conf``? - -* Should objects be Python-syntax, instead of always strings? Lots of - code isn't usable with Python strings without a thin wrapper to - translate objects into their proper types. - -* Some short-form for a filter/app, where the filter refers to the - "next app". Maybe like:: - - [app-filter:app_name] - use = egg:... - next = next_app - - [app:next_app] - ... - diff --git a/docs/rebuild b/docs/rebuild deleted file mode 100755 index 852c3fd..0000000 --- a/docs/rebuild +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -here=`pwd` -parent=`dirname $here` -echo "Adding $parent to \$PYTHONPATH" -export PYTHONPATH=$parent:$PYTHONPATH - -NORMAL="paste-deploy news" - -for NAME in $NORMAL ; do - if [ -e "$NAME.html" -a ! "$NAME.html" -ot "$NAME.txt" ] ; then - echo "$NAME is up to date." - continue - fi - echo "Building $NAME." - rst2html.py --no-toc-backlinks "$NAME.txt" > "$NAME.html" -done - -source-highlight -f html ../paste/deploy/interfaces.py -mv ../paste/deploy/interfaces.py.html . diff --git a/setup.cfg b/setup.cfg index 01bb954..ea2d143 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,20 @@ [egg_info] tag_build = dev tag_svn_revision = true + +[pudge] +theme = lesscode.org +docs = docs/index.txt docs/news.txt +dest = docs/html +modules = paste +title = Paste Core +organization = Python Paste +organization_url = http://pythonpaste.org/ +trac_url = http://pythonpaste.org/trac/ +blog_url = http://pythonpaste.org/news/ +mailing_list_url = http://pythonpaste.org/community/mailing-list.html + +[publish] +doc-dir=docs/html +doc-dest=scp://ianb@webwareforpython.org/home/paste/htdocs/new/deploy +make-dirs=1 -- cgit v1.2.1 From 9fffa32745767befc2a2c31dd0eeba0412de6f74 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 28 Sep 2005 08:42:32 +0000 Subject: Changed pudge template --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ea2d143..70306c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ tag_build = dev tag_svn_revision = true [pudge] -theme = lesscode.org +theme = pythonpaste.org docs = docs/index.txt docs/news.txt dest = docs/html modules = paste -- cgit v1.2.1 From 796ab597686001d1deb1476d58d7cebfd2c21f87 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 28 Sep 2005 20:20:39 +0000 Subject: Make sure the new paste is loaded --- paste/deploy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index b6fc546..2768b87 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -11,7 +11,7 @@ def local_dict(): return config_local.wsgi_dict except NameError: import pkg_resources - pkg_resources.require('Paste') + pkg_resources.require('Paste>=0.1') from paste.util.threadinglocal import local config_local = local() config_local.wsgi_dict = result = {} -- cgit v1.2.1 -- cgit v1.2.1 From c32ef542591f1066f18b866c81a5063093b658fd Mon Sep 17 00:00:00 2001 From: ianb Date: Sat, 1 Oct 2005 08:49:35 +0000 Subject: Changed file location --- setup.cfg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 70306c7..c4afda9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,8 +13,11 @@ organization_url = http://pythonpaste.org/ trac_url = http://pythonpaste.org/trac/ blog_url = http://pythonpaste.org/news/ mailing_list_url = http://pythonpaste.org/community/mailing-list.html +settings = no_about=true + link1=/deploy/ paste.deploy + link2=/script/ paster script [publish] doc-dir=docs/html -doc-dest=scp://ianb@webwareforpython.org/home/paste/htdocs/new/deploy +doc-dest=scp://ianb@webwareforpython.org/home/paste/htdocs/deploy make-dirs=1 -- cgit v1.2.1 From 244d513002a73614045c90e4e53da008768998af Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 3 Oct 2005 04:51:33 +0000 Subject: Updated metadata --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ac3ddf9..00849d8 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.3' +version = '0.4' setup( name="PasteDeploy", @@ -13,7 +13,7 @@ files. See also the `Subversion repository `_ """ % version, - classifiers=["Development Status :: 3 - Alpha", + classifiers=["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Python Software Foundation License", "Programming Language :: Python", @@ -24,7 +24,7 @@ See also the `Subversion repository Date: Mon, 3 Oct 2005 05:30:17 +0000 Subject: updated site generation --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index c4afda9..285e2fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ mailing_list_url = http://pythonpaste.org/community/mailing-list.html settings = no_about=true link1=/deploy/ paste.deploy link2=/script/ paster script + link3=/download/ Download + extra_credits=Hosting courtesy of Tummy.com [publish] doc-dir=docs/html -- cgit v1.2.1 From 0fb982f84894af402e4c5d01421d141cf5ddd0d3 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 3 Oct 2005 21:28:30 +0000 Subject: Fix bug where an empty config, and no current config, were being confused --- paste/deploy/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 2768b87..23b811d 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -95,7 +95,7 @@ class DispatchingConfig(object): def __getattr__(self, attr): conf = self.current_conf() - if not conf: + if conf is None: raise AttributeError( "No configuration has been registered for this process " "or thread") @@ -113,7 +113,7 @@ class DispatchingConfig(object): def __getitem__(self, key): # I thought __getattr__ would catch this, but apparently not conf = self.current_conf() - if not conf: + if conf is None: raise TypeError( "No configuration has been registered for this process " "or thread") -- cgit v1.2.1 From 60fe04d39a541f80a19cb66027deb5c33b3bf669 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 4 Oct 2005 16:41:15 +0000 Subject: Fixed error message --- paste/deploy/loadwsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index c7e95a3..36ee3e9 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -49,7 +49,7 @@ class NicerConfigParser(ConfigParser): except Exception, e: args = list(e.args) args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( - self.filename, section, option, rawval) + self.filename, section, option, rawval, e) e.args = tuple(args) raise -- cgit v1.2.1 From a32883ef6b70397141dd610c2897c409e3d256c8 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 4 Oct 2005 17:10:23 +0000 Subject: Remove subversion link --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 00849d8..e52db08 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,6 @@ setup( This tool provides code to load WSGI applications and servers from URIs; these URIs can refer to Python Eggs for INI-style configuration files. - -See also the `Subversion repository `_ """ % version, classifiers=["Development Status :: 4 - Beta", "Intended Audience :: Developers", -- cgit v1.2.1 From de1239bbe65b651e97b9d567e9d759fd2a1fdbf3 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 4 Oct 2005 18:47:35 +0000 Subject: Fix metadata --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e52db08..dd1e5bd 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( This tool provides code to load WSGI applications and servers from URIs; these URIs can refer to Python Eggs for INI-style configuration files. -""" % version, +""", classifiers=["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Python Software Foundation License", -- cgit v1.2.1 From d1c3c487e3ca364b4c9ad2e5c7c2243de5851d0c Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 4 Oct 2005 18:59:59 +0000 Subject: Copy the one bit of code we share with Paste core over (or should it use svn:externals?) --- paste/deploy/__init__.py | 7 ++++++- paste/deploy/config.py | 4 +--- paste/deploy/util/__init__.py | 1 + paste/deploy/util/threadinglocal.py | 39 +++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 paste/deploy/util/__init__.py create mode 100644 paste/deploy/util/threadinglocal.py diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index 5c83a6d..1677202 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1,3 +1,8 @@ from loadwsgi import loadapp, loadfilter, loadserver -from config import CONFIG +try: + from config import CONFIG +except ImportError: + # @@: Or should we require Paste? Or should we put threadlocal + # into this package too? + pass diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 23b811d..055a1c2 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -10,9 +10,7 @@ def local_dict(): try: return config_local.wsgi_dict except NameError: - import pkg_resources - pkg_resources.require('Paste>=0.1') - from paste.util.threadinglocal import local + from paste.deploy.util.threadinglocal import local config_local = local() config_local.wsgi_dict = result = {} return result diff --git a/paste/deploy/util/__init__.py b/paste/deploy/util/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/paste/deploy/util/__init__.py @@ -0,0 +1 @@ +# diff --git a/paste/deploy/util/threadinglocal.py b/paste/deploy/util/threadinglocal.py new file mode 100644 index 0000000..57afa17 --- /dev/null +++ b/paste/deploy/util/threadinglocal.py @@ -0,0 +1,39 @@ +# (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 threading +except ImportError: + # No threads, so "thread local" means process-global + class local(object): + pass +else: + try: + local = threading.local + except AttributeError: + # Added in 2.4, but now we'll have to define it ourselves + import thread + class local(object): + + def __init__(self): + self.__dict__['__objs'] = {} + + def __getattr__(self, attr, g=thread.get_ident): + try: + return self.__dict__['__objs'][g()][attr] + except KeyError: + raise AttributeError( + "No variable %s defined for the thread %s" + % (attr, g())) + + def __setattr__(self, attr, value, g=thread.get_ident): + self.__dict__['__objs'].setdefault(g(), {})[attr] = value + + def __delattr__(self, attr, g=thread.get_ident): + try: + del self.__dict__['__objs'][g()][attr] + except KeyError: + raise AttributeError( + "No variable %s defined for thread %s" + % (attr, g())) + diff --git a/setup.py b/setup.py index dd1e5bd..9bd9f8d 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ files. }, zip_safe=False, extras_require={ - 'Config': ['Paste'], + 'Config': [], 'Paste': ['Paste'], }, entry_points=""" -- cgit v1.2.1 From 9a37ca6cc4bae8ae1c57623e879df750a2bc1a1b Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 5 Oct 2005 00:59:14 +0000 Subject: Dispatch __setitem__ and __contains__ specially --- paste/deploy/config.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 055a1c2..912eeb0 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -117,6 +117,15 @@ class DispatchingConfig(object): "or thread") return conf[key] + def __contains__(self, key): + # I thought __getattr__ would catch this, but apparently not + return self.has_key(key) + + 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(object): -- cgit v1.2.1 From 161288912cd9d18a53db4f53b6441d09ec69f32a Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 5 Oct 2005 04:30:34 +0000 Subject: Fixed typo/missing line --- docs/index.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.txt b/docs/index.txt index ceb8a42..a67daf2 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -204,6 +204,7 @@ where you mount applications at different URL paths. This might look like:: [composit:main] + use = egg:Paste#urlmap / = mainapp /files = staticapp -- cgit v1.2.1 From c4d342ef84a253e7401cc0df6db7c79da5386f88 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 7 Oct 2005 01:24:55 +0000 Subject: fix docstring --- paste/deploy/interfaces.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index 63e37b2..8a03bdb 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -46,8 +46,10 @@ class IPasteAppFactory: """ Returns a WSGI application (IWSGIAPP) given the global configuration and the local configuration passed in as keyword - arguments. All keys are strings, but values in local_conf may - not be valid Python identifiers (if you use **kw you can still + arguments. + + All keys are strings, but values in local_conf may not be + valid Python identifiers (if you use ``**kw`` you can still capture these values). """ -- cgit v1.2.1 From 8b891a0c49fded9d8c2b3d4d051adc7320b54129 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 7 Oct 2005 19:59:48 +0000 Subject: Allow filters to have a filter-with configuration key. Clean up object stuff a bit (but just a bit) --- paste/deploy/loadwsgi.py | 197 ++++++++++++++---------- tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 4 +- tests/sample_configs/test_filter_with.ini | 11 ++ tests/test_config.py | 16 +- tests/test_filter.py | 7 + 5 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 tests/sample_configs/test_filter_with.ini diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 36ee3e9..e622407 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -3,6 +3,7 @@ import re import urllib from ConfigParser import ConfigParser import pkg_resources +from UserDict import UserDict __all__ = ['loadapp', 'loadserver', 'loadfilter'] @@ -59,110 +60,119 @@ class NicerConfigParser(ConfigParser): class _ObjectType(object): - def __init__(self, name, egg_protocols, config_prefixes): - self.name = name - self.egg_protocols = map(_aslist, _aslist(egg_protocols)) - self.config_prefixes = map(_aslist, _aslist(config_prefixes)) + name = None + egg_protocols = None + config_prefixes = None + + def __init__(self): + # Normalize these variables: + self.egg_protocols = map(_aslist, _aslist(self.egg_protocols)) + self.config_prefixes = map(_aslist, _aslist(self.config_prefixes)) def __repr__(self): return '<%s protocols=%r prefixes=%r>' % ( - self.name, self.egg_protocols, self.config_prefixees) + self.name, self.egg_protocols, self.config_prefixes) def invoke(self, context): assert context.protocol in _flatten(self.egg_protocols) return context.object(context.global_conf, **context.local_conf) -APP = _ObjectType( - 'application', - ['paste.app_factory', 'paste.composit_factory'], - [['app', 'application'], 'composit', 'pipeline', 'filter-app']) +class _App(_ObjectType): -def APP_invoke(context): - if context.protocol == 'paste.composit_factory': - return context.object(context.loader, context.global_conf, - **context.local_conf) - elif context.protocol == 'paste.app_factory': - return context.object(context.global_conf, **context.local_conf) - else: - assert 0, "Protocol %r unknown" % context.protocol + name = 'application' + egg_protocols = ['paste.app_factory', 'paste.composit_factory'] + config_prefixes = [['app', 'application'], ['composite', 'composit'], + 'pipeline', 'filter-app'] -APP.invoke = APP_invoke + def invoke(self, context): + if context.protocol == 'paste.composit_factory': + return context.object(context.loader, context.global_conf, + **context.local_conf) + elif context.protocol == 'paste.app_factory': + return context.object(context.global_conf, **context.local_conf) + else: + assert 0, "Protocol %r unknown" % context.protocol -FILTER = _ObjectType( - 'filter', - [['paste.filter_factory', 'paste.filter_app_factory']], - ['filter']) +APP = _App() -def FILTER_invoke(context): - if context.protocol == 'paste.filter_factory': - return 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 context.object(wsgi_app, context.global_conf, - **context.local_conf) - return filter_wrapper - else: - assert 0, "Protocol %r unknown" % context.protocol +class _Filter(_ObjectType): + name = 'filter' + egg_protocols = [['paste.filter_factory', 'paste.filter_app_factory']] + config_prefixes = ['filter'] -FILTER.invoke = FILTER_invoke + def invoke(self, context): + if context.protocol == 'paste.filter_factory': + return 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 context.object(wsgi_app, context.global_conf, + **context.local_conf) + return filter_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol -SERVER = _ObjectType( - 'server', - [['paste.server_factory', 'paste.server_runner']], - ['server']) +FILTER = _Filter() -def SERVER_invoke(context): - if context.protocol == 'paste.server_factory': - return 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 context.object(wsgi_app, context.global_conf, - **context.local_conf) - return server_wrapper - else: - assert 0, "Protocol %r unknown" % context.protocol +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 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 context.object(wsgi_app, context.global_conf, + **context.local_conf) + return server_wrapper + else: + assert 0, "Protocol %r unknown" % context.protocol -SERVER.invoke = SERVER_invoke +SERVER = _Server() # Virtual type: (@@: There's clearly something crufty here; # this probably could be more elegant) -PIPELINE = _ObjectType( - 'pipeline', - [], []) +class _PipeLine(_ObjectType): + name = 'pipeline' -def PIPELINE_invoke(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 + 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.invoke = PIPELINE_invoke +PIPELINE = _PipeLine() -# Virtual type: -FILTER_APP = _ObjectType( - 'filter_app', - [], []) +class _FilterApp(_ObjectType): + name = 'filter_app' -def FILTER_APP_invoke(context): - next_app = context.next_context.create() - filter = context.filter_context.create() - return filter(next_app) + def invoke(self, context): + next_app = context.next_context.create() + filter = context.filter_context.create() + return filter(next_app) -FILTER_APP.invoke = FILTER_APP_invoke +FILTER_APP = _FilterApp() -FILTER_WITH = _ObjectType( - 'filtered_app', [], []) +class _FilterWith(_App): + name = 'filtered_with' -def FILTER_WITH_invoke(context): - filter = context.filter_context.create() - app = APP_invoke(context) - return filter(app) + 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.invoke = FILTER_WITH_invoke +FILTER_WITH = _FilterWith() ############################################################ ## Loaders @@ -177,6 +187,12 @@ def loadfilter(uri, name=None, **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 + _loaders = {} def loadobj(object_type, uri, name=None, relative_to=None, @@ -320,7 +336,7 @@ class ConfigLoader(_Loader): 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 is APP and 'filter-with' in local_conf: + if object_type in (APP, FILTER) and 'filter-with' in local_conf: filter_with = local_conf.pop('filter-with') else: filter_with = None @@ -347,10 +363,16 @@ class ConfigLoader(_Loader): object_type, local_conf, global_conf, global_additions, section) if filter_with is not None: - filter_context = self.filter_context( + filter_with_context = LoaderContext( + obj=None, + object_type=FILTER_WITH, + protocol=None, + global_conf=None, local_conf=None, + loader=self) + filter_with_context.filter_context = self.filter_context( name=filter_with, global_conf=global_conf) - context.object_type = FILTER_WITH - context.filter_context = filter_context + filter_with_context.next_context = context + return filter_with_context return context def _context_from_use(self, object_type, local_conf, global_conf, @@ -540,3 +562,16 @@ class LoaderContext(object): def create(self): return self.object_type.invoke(self) + + def config(self): + conf = AttrDict(self.global_conf) + AttrDict.update(self.local_conf) + AttrDict.local_conf = local_conf + AttrDict.global_conf = global_conf + return AttrDict + +class AttrDict(dict): + """ + A dictionary that can be assigned to. + """ + pass diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py index 7280210..cae7eba 100644 --- a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -49,9 +49,9 @@ class RemoteAddrDispatch(object): ############################################################ def make_cap_filter(global_conf, method_to_call='upper'): - def filter(app): + def cap_filter(app): return CapFilter(app, global_conf, method_to_call) - return filter + return cap_filter class CapFilter(object): diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini new file mode 100644 index 0000000..ec6592b --- /dev/null +++ b/tests/sample_configs/test_filter_with.ini @@ -0,0 +1,11 @@ +[app:main] +use = egg:FakeApp#basic_app +filter-with = filter1 + +[filter:filter1] +use = egg:FakeApp#caps +filter-with = filter2 + +[filter:filter2] +use = egg:FakeApp#caps + diff --git a/tests/test_config.py b/tests/test_config.py index 72e864e..75c102e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ import fakeapp.configapps as fc ini_file = 'config:sample_configs/test_config.ini' here = os.path.dirname(__file__) +config_path = os.path.join(here, 'sample_configs') def test_config_egg(): app = loadapp('egg:FakeApp#configed') @@ -15,7 +16,8 @@ def test_config1(): assert app.local_conf == { 'setting1': 'foo', 'setting2': 'bar'} assert app.global_conf == { - 'def1': 'a', 'def2': 'b'} + 'def1': 'a', 'def2': 'b', + 'here': config_path} def test_config2(): app = loadapp(ini_file, relative_to=here, name='test2') @@ -24,7 +26,8 @@ def test_config2(): assert app.global_conf == { 'def1': 'test2', 'def2': 'b', - 'another': 'TEST'} + 'another': 'TEST', + 'here': config_path} # Run this to make sure the global-conf-modified test2 # didn't mess up the general global conf test_config1() @@ -38,7 +41,8 @@ def test_config3(): assert app.global_conf == { 'def1': 'test3', 'def2': 'b', - 'another': 'TEST'} + 'another': 'TEST', + 'here': config_path} test_config2() def test_foreign_config(): @@ -51,7 +55,8 @@ def test_foreign_config(): 'def1': 'a', 'def2': 'from include', 'def3': 'c', - 'glob': 'override'} + 'glob': 'override', + 'here': config_path} def test_config_get(): app = loadapp(ini_file, relative_to=here, name='test_get') @@ -61,5 +66,6 @@ def test_config_get(): 'foo': 'TEST'} assert app.global_conf == { 'def1': 'a', - 'def2': 'TEST'} + 'def2': 'TEST', + 'here': config_path} diff --git a/tests/test_filter.py b/tests/test_filter.py index 6829ada..77ee2ee 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -37,3 +37,10 @@ def test_filter_app_inverted(): relative_to=here) assert isinstance(app, fakeapp.apps.CapFilter) assert app.app is fakeapp.apps.basic_app + +def test_filter_with_filter_with(): + app = loadapp('config:sample_configs/test_filter_with.ini', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert isinstance(app.app, fakeapp.apps.CapFilter) + assert app.app.app is fakeapp.apps.basic_app -- cgit v1.2.1 From c3f95b66835105a173430ca3836b85d53279da60 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 7 Oct 2005 20:16:15 +0000 Subject: Added tests of appconfig, docs, news --- docs/index.txt | 23 +++++++++++++++++------ docs/news.txt | 14 +++++++++++++- paste/deploy/__init__.py | 2 +- paste/deploy/loadwsgi.py | 18 ++++++++++-------- tests/test_basic_app.py | 2 +- tests/test_config.py | 16 +++++++++++++++- 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index a67daf2..c790b42 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -195,15 +195,15 @@ global configuration locally like:: That is, by using ``set`` in front of the key. -Composit Applications -~~~~~~~~~~~~~~~~~~~~~ +Composite Applications +~~~~~~~~~~~~~~~~~~~~~~ -"Composit" applications are things that act like applications, but +"Composite" applications are things that act like applications, but are made up of other applications. One example would be a URL mapper, where you mount applications at different URL paths. This might look like:: - [composit:main] + [composite:main] use = egg:Paste#urlmap / = mainapp /files = staticapp @@ -215,7 +215,7 @@ like:: use = egg:Paste#static document_root = /path/to/docroot -The composit application "main" is just like any other application +The composite application "main" is just like any other application from the outside (you load it with ``loadapp`` for instance), but it has access to other applications defined in the configuration file. @@ -242,6 +242,7 @@ The first way is to use the ``filter-with`` setting, like:: [filter:printdebug] use = egg:Paste#printdebug + # and you could have another filter-with here, and so on... Also, two special section types exist to apply filters to your applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of @@ -263,6 +264,16 @@ ended by an application, like:: [filter:filter1] ... +Getting Configuration +~~~~~~~~~~~~~~~~~~~~~ + +If you want to get the configuration without creating the application, +you can use the ``appconfig(uri)`` function, which is just like the +``loadapp()`` function except it returns the configuration that would +be used, as a dictionary. Both global and local configuration is +combined into a single dictionary, but you can look at just one or the +other with the attributes ``.local_conf`` and ``.global_conf``. + ``egg:`` URIs ------------- @@ -329,7 +340,7 @@ mentioned). But that's not much use unless you can create factories for your applications. There's a few protocols: ``paste.app_factory``, -``paste.composit_factory``, ``paste.filter_factory``, and lastly +``paste.composite_factory``, ``paste.filter_factory``, and lastly ``paste.server_factory``. Each of these expects a callable (like a function, method, or class). diff --git a/docs/news.txt b/docs/news.txt index 9026145..d05e51b 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,9 +1,21 @@ Paste Deployment News ===================== -Version 0.3 (svn trunk) +Version 0.4 (svn trunk) ----------------------- +* Allow filters to have ``filter-with`` values, just like + applications. + +* Renamed ``composit`` to ``composite`` (old names still work, but + aren't documented). + +* Added ``appconfig()`` to load along with ``loadapp()``, but return + the configuration without invoking the application. + +Version 0.3 +----------- + * Allow variable setting like:: get local_var = global_var_name diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index 1677202..abf833f 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1,4 +1,4 @@ -from loadwsgi import loadapp, loadfilter, loadserver +from loadwsgi import * try: from config import CONFIG except ImportError: diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index e622407..67b213e 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -5,7 +5,7 @@ from ConfigParser import ConfigParser import pkg_resources from UserDict import UserDict -__all__ = ['loadapp', 'loadserver', 'loadfilter'] +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] ############################################################ ## Utility functions @@ -80,12 +80,14 @@ class _ObjectType(object): class _App(_ObjectType): name = 'application' - egg_protocols = ['paste.app_factory', 'paste.composit_factory'] + 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 == 'paste.composit_factory': + if context.protocol in ('paste.composit_factory', + 'paste.composite_factory'): return context.object(context.loader, context.global_conf, **context.local_conf) elif context.protocol == 'paste.app_factory': @@ -191,7 +193,7 @@ 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 + return context.config() _loaders = {} @@ -565,10 +567,10 @@ class LoaderContext(object): def config(self): conf = AttrDict(self.global_conf) - AttrDict.update(self.local_conf) - AttrDict.local_conf = local_conf - AttrDict.global_conf = global_conf - return AttrDict + conf.update(self.local_conf) + conf.local_conf = self.local_conf + conf.global_conf = self.global_conf + return conf class AttrDict(dict): """ diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py index 05b61e3..11d1f40 100644 --- a/tests/test_basic_app.py +++ b/tests/test_basic_app.py @@ -1,4 +1,4 @@ -from paste.deploy import loadapp, loadfilter +from paste.deploy import loadapp, loadfilter, appconfig from fixture import * import fakeapp.apps diff --git a/tests/test_config.py b/tests/test_config.py index 75c102e..1761f17 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,5 @@ import os -from paste.deploy import loadapp, loadfilter +from paste.deploy import loadapp, loadfilter, appconfig from fixture import * import fakeapp.configapps as fc @@ -69,3 +69,17 @@ def test_config_get(): 'def2': 'TEST', 'here': config_path} +def test_appconfig(): + conf = appconfig(ini_file, relative_to=here, name='test_get') + assert conf == { + 'def1': 'a', + 'def2': 'TEST', + 'here': config_path, + 'foo': 'TEST'} + assert conf.local_conf == { + 'def1': 'a', + 'foo': 'TEST'} + assert conf.global_conf == { + 'def1': 'a', + 'def2': 'TEST', + 'here': config_path} -- cgit v1.2.1 From ae839336ca3e54364a498b6b1cc5d319fd4b896d Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 14 Nov 2005 00:37:08 +0000 Subject: Removed tag distutils command (this has been moved to buildutils) --- paste/deploy/tag.py | 168 ---------------------------------------------------- setup.py | 3 - 2 files changed, 171 deletions(-) delete mode 100644 paste/deploy/tag.py diff --git a/paste/deploy/tag.py b/paste/deploy/tag.py deleted file mode 100644 index bb01d3a..0000000 --- a/paste/deploy/tag.py +++ /dev/null @@ -1,168 +0,0 @@ -from setuptools import Command -from setuptools.command import setopt -from ConfigParser import NoSectionError -from distutils.errors import * -from distutils import log -from pkg_resources import * -try: - import subprocess -except ImportError: - # pre-Python 2.4 - pass -import re -import sys -import os -import tempfile - -class tag(Command): - - description = "Tag for release" - - user_options = [ - ('version=', 'v', "Specify version"), - ('message=', 'm', "Specify a log message"), - ('build=', 'b', "Specify directory to build tag files in"), - ] - - version = None - message = None - build = None - - def initialize_options(self): - pass - - def finalize_options(self): - if self.version is None: - raise DistutilsOptionError( - "You must specify a version") - if self.message is None: - self.message = "Tagging %s version" % self.version - if self.build and os.path.exists(self.build): - raise DistutilsOptionError( - "The build directory %s already exists" % self.build) - if self.build is None: - self.build = tempfile.mkdtemp(prefix='tag_checkout_') - - _svn_url_re = re.compile(r'\bURL: (.*)') - _setup_version_re = re.compile(r'(version\s+=\s+)([^ \n\r,)]*)') - _egg_info_re = re.compile(r'^[egg_info]$') - - def run(self): - ei_cmd = self.get_finalized_command("egg_info") - path_item = normalize_path(ei_cmd.egg_base) - metadata = PathMetadata( - path_item, normalize_path(ei_cmd.egg_info) - ) - stdout = run_command(['svn', 'info', path_item]) - match = self._svn_url_re.search(stdout) - if not match: - raise DistutilsExecError( - 'svn output did not contain "URL: ...":\n' + stdout) - svn_url = match.group(1) - if not svn_url.endswith('/trunk'): - raise DistutilsExecError( - 'svn URL must end with "/trunk" (current: %r)' % svn_url) - package_url = svn_url.rsplit('/', 1)[0] - tag_url = package_url + '/tags/' + self.version - run_command(['svn', 'cp', '--message', self.message, - svn_url, tag_url]) - run_command(['svn', 'co', '--quiet', tag_url, self.build]) - self.update_setup_py() - self.update_setup_cfg() - run_command(['svn', 'commit', '--message', - 'Auto-update of version strings', self.build]) - self.checkout_tag(tag_url) - - def update_setup_py(self): - setup_py = os.path.join(self.build, 'setup.py') - if not os.path.exists(setup_py): - log.warn('setup.py file cannot be found at %s' % setup_py) - return - f = open(setup_py) - content = f.read() - f.close() - match = self._setup_version_re.search(content) - if not match: - log.warn('Cannot find version info in %s' % setup_py) - else: - new_content = ( - content[:match.start()] - + match.group(1) - + repr(self.version) - + content[match.end():]) - if new_content == content: - log.info('Version string up-to-date (edit trunk yourself)') - else: - f = open(setup_py, 'w') - f.write(new_content) - f.close() - log.info('%s version updated' % setup_py) - command = [sys.executable, setup_py, 'egg_info'] - - def update_setup_cfg(self): - setup_cfg = os.path.join(self.build, 'setup.cfg') - if not os.path.exists(setup_cfg): - log.warn('setup.cfg file cannot be found at %s' % setup_cfg) - return - try: - setopt.edit_config( - setup_cfg, - {'egg_info': {'tag_build': None, - 'tag_svn_revision': None}}) - except NoSectionError: - # No [egg_info]; that's okay - pass - f = open(setup_cfg) - content = f.read() - f.close() - if not content.strip(): - log.info('%s empty; deleting' % setup_cfg) - run_command(['svn', 'rm', '--force', setup_cfg]) - - def checkout_tag(self, tag_url): - # Put it in the parent directory: - ext = '' - while 1: - base = os.path.join(os.path.dirname(os.getcwd()), - '%s-%s%s' % (self.distribution.metadata.name, self.version, ext)) - if os.path.exists(base): - log.warn('Directory %s exists' % base) - if ext: - ext += 1 - else: - ext = 0 - else: - break - run_command(['svn', 'checkout', tag_url, base]) - log.info('Checked out tag into %s' % base) - - -def run_command(command_list, stdin=None): - log.info('Running %s', format_command(command_list)) - proc = subprocess.Popen(command_list, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = proc.communicate(stdin) - if stderr: - log.warn('Ouput from %s:\n%s', - format_command(command_list), - stderr) - returncode = proc.returncode - if returncode: - error = 'Exit code %s from %s' % ( - returncode, format_command(command_list)) - if stderr: - error += '; stderr output:\n' + stderr - raise DistutilsExecError(error) - return stdout - -def format_command(lst): - return ' '.join(map(quote_command_item, lst)) - -def quote_command_item(item): - if ' ' in item: # @@: Obvious I should check more characters - for char in ('\\', '"', "'", '$'): - item = item.replace(char, '\\' + char) - item = '"%s"' % item - return item - diff --git a/setup.py b/setup.py index 9bd9f8d..a5f47b1 100644 --- a/setup.py +++ b/setup.py @@ -35,9 +35,6 @@ files. 'Paste': ['Paste'], }, entry_points=""" - [distutils.commands] - tag = paste.deploy.tag:tag - [paste.filter_app_factory] config = paste.deploy.config:make_config_filter [Config] -- cgit v1.2.1 From 3d4b97e311721dcbd47bb1b4016a0afe4897f487 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 14 Nov 2005 02:57:32 +0000 Subject: Bunch of minor changes suggested by pyflakes --- paste/deploy/loadwsgi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 67b213e..2513a95 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -3,7 +3,6 @@ import re import urllib from ConfigParser import ConfigParser import pkg_resources -from UserDict import UserDict __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] -- cgit v1.2.1 From 9dbe4de2f5dc9b69ad912413e3dd0cfffea0ffe4 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 18 Nov 2005 20:30:35 +0000 Subject: Fix a configuration name --- paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 05a80fb..ff550e3 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -1,6 +1,6 @@ [DEFAULT] client_name = test_client -email_errors = ${author_email|empty} +error_email = ${author_email|empty} config_root = [app:main] -- cgit v1.2.1 From 0ed3aaf978694c511fb44bd5871a93897367842b Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 21 Nov 2005 08:12:45 +0000 Subject: Added pkg_resources.declare_namespace --- paste/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paste/__init__.py b/paste/__init__.py index 792d600..5e2ef1d 100644 --- a/paste/__init__.py +++ b/paste/__init__.py @@ -1 +1,2 @@ -# +import pkg_resources +pkg_resources.declare_namespace('paste') -- cgit v1.2.1 From 8628edc8512c0a0d80ab69b3430a1b20a2f34a1e Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 9 Dec 2005 23:54:49 +0000 Subject: Trimming down the paste_deploy template --- paste/deploy/loadwsgi.py | 1 + .../paster_templates/paste_deploy/docs/config.txt | 1 - .../paste_deploy/docs/devel_config.ini_tmpl | 28 ++++++++-------------- .../paste_deploy/docs/example_deploy.ini_tmpl | 3 --- 4 files changed, 11 insertions(+), 22 deletions(-) delete mode 100644 paste/deploy/paster_templates/paste_deploy/docs/config.txt delete mode 100644 paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 2513a95..608103c 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -525,6 +525,7 @@ class EggLoader(_Loader): 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, diff --git a/paste/deploy/paster_templates/paste_deploy/docs/config.txt b/paste/deploy/paster_templates/paste_deploy/docs/config.txt deleted file mode 100644 index cf6543d..0000000 --- a/paste/deploy/paster_templates/paste_deploy/docs/config.txt +++ /dev/null @@ -1 +0,0 @@ -# Description of all the configuration parameters 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 index ff550e3..ecb241d 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -1,25 +1,17 @@ -[DEFAULT] -client_name = test_client -error_email = ${author_email|empty} -config_root = +[filter-app:main] +# This puts the interactive debugger in place: +use = egg:Paste#evalerror +next = devel -[app:main] -use = egg:PasteWebKit -app_name = ${package} -package_name = ${package} -# @@: This should be moved into a SQLObject template: -database = postgres://pgsql@/${package} +[app:devel] +# This application is meant for interactive development +use = egg:${project} debug = true -get client_name = client_name -get email_errors = email_errors [app:test] -use = main -# Disable emailing of errors: -email_errors = - -# @@: This should be moved into a SQLObject template: -database = postgres://pgsql@/${package}_test +# While this version of the configuration is for non-iteractive +# tests (unit tests) +use = devel [server:main] use = egg:PasteScript#wsgiutils diff --git a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl b/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl deleted file mode 100644 index b497a40..0000000 --- a/paste/deploy/paster_templates/paste_deploy/docs/example_deploy.ini_tmpl +++ /dev/null @@ -1,3 +0,0 @@ -[config:live] -#database = postgres://pgsql@/${package} - -- cgit v1.2.1 From b4e6febf868cf307ca8c444e449232033d4f1505 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 19 Dec 2005 08:14:11 +0000 Subject: Added some more metadata to the thing returned by appconfig --- paste/deploy/loadwsgi.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 608103c..f30c03e 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -506,14 +506,16 @@ class EggLoader(_Loader): if self.absolute_name(name): return loadcontext(object_type, name, global_conf=global_conf) - entry_point, protocol = self.find_egg_entry_point( + 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) + self, + distribution=pkg_resources.get_distribution(self.spec), + entry_point_name=ep_name) def find_egg_entry_point(self, object_type, name=None): """ @@ -531,7 +533,7 @@ class EggLoader(_Loader): protocol, name) if entry is not None: - possible.append((entry.load(), protocol)) + possible.append((entry.load(), protocol, entry.name)) break if not possible: # Better exception @@ -554,13 +556,19 @@ class EggLoader(_Loader): class LoaderContext(object): def __init__(self, obj, object_type, protocol, - global_conf, local_conf, loader): + 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) @@ -570,6 +578,7 @@ class LoaderContext(object): 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): -- cgit v1.2.1 From df8a0adb879c9d3d94eb18705226f6194074301e Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 9 Jan 2006 05:48:40 +0000 Subject: Updated setup.py description --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a5f47b1..96d57f3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,14 @@ setup( long_description="""\ This tool provides code to load WSGI applications and servers from URIs; these URIs can refer to Python Eggs for INI-style configuration -files. +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Subversion repository +`_. + +For the latest changes see the `news file +`_. """, classifiers=["Development Status :: 4 - Beta", "Intended Audience :: Developers", -- cgit v1.2.1 From 9d93c891046a9a7c8059143573ef4e8435037ca2 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 9 Jan 2006 06:08:17 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 96d57f3..5930bf7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.4' +version = '0.5' setup( name="PasteDeploy", -- cgit v1.2.1 From 3f15b6bc02750eef0c0945a0d28aab474599a32d Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 9 Jan 2006 06:11:18 +0000 Subject: Fixed def --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5930bf7..a5f0484 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ files. `Paste Script `_ provides commands to serve applications based on this configuration file. The latest version is available in a `Subversion repository -`_. +`_. For the latest changes see the `news file `_. -- cgit v1.2.1 From 5d21d961544954f036688485eb53c46dbe048d40 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 10 Jan 2006 08:33:46 +0000 Subject: Rename all references to 'composit' to 'composite' --- docs/index.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index c790b42..60e5526 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -355,12 +355,12 @@ The application is the most common. You define one like:: The ``global_config`` is a dictionary, and local configuration is passed as keyword arguments. The function returns a WSGI application. -``paste.composit_factory`` +``paste.composite_factory`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Composits are just slightly more complex:: +Composites are just slightly more complex:: - def composit_factory(loader, global_config, **local_conf): + def composite_factory(loader, global_config, **local_conf): return wsgi_app The ``loader`` argument is an object that has a couple interesting @@ -368,7 +368,7 @@ methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI application with the given name. ``get_filter`` and ``get_server`` work the same way. -A more interesting example might be a composit factory that does +A more interesting example might be a composite factory that does something. For instance, consider a "pipeline" application:: def pipeline_factory(loader, global_config, pipeline): @@ -383,7 +383,7 @@ something. For instance, consider a "pipeline" application:: Then we use it like:: - [composit:main] + [composite:main] use = pipeline = egg:Paste#printdebug session myapp @@ -474,7 +474,7 @@ Outstanding Issues * Should there be a "default" protocol for each type of object? Since there's currently only one protocol, it seems like it makes sense (in the future there could be multiple). Except that - ``paste.app_factory`` and ``paste.composit_factory`` overlap + ``paste.app_factory`` and ``paste.composite_factory`` overlap considerably. * ConfigParser's INI parsing is kind of annoying. I'd like it both -- cgit v1.2.1 From 763f2a2dee755eded76f9347e76a5884d0198c01 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 10 Jan 2006 08:39:52 +0000 Subject: Improved the paster create --template=paste_deploy template, to include an entry point and sample application ready to run --- docs/news.txt | 11 +++++++++-- paste/deploy/paster_templates.py | 21 ++++++++++++++++++++ .../paste_deploy/+package+/sampleapp.py_tmpl | 21 ++++++++++++++++++++ .../paste_deploy/+package+/wsgiapp.py_tmpl | 23 ++++++++++++++++++++++ .../paste_deploy/docs/devel_config.ini_tmpl | 2 ++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl diff --git a/docs/news.txt b/docs/news.txt index d05e51b..d5cf6e9 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,15 @@ Paste Deployment News ===================== -Version 0.4 (svn trunk) ------------------------ +svn trunk +--------- + +* Made the ``paste_deploy`` template (used with ``paster create + --template=paste_deploy``) more useful, with an example application + and entry point. + +Version 0.4 +----------- * Allow filters to have ``filter-with`` values, just like applications. diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py index 61a5ad6..c3de82b 100644 --- a/paste/deploy/paster_templates.py +++ b/paste/deploy/paster_templates.py @@ -1,3 +1,4 @@ +import os from paste.script.templates import Template class PasteDeploy(Template): @@ -8,3 +9,23 @@ class PasteDeploy(Template): 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 new file mode 100644 index 0000000..4db7d13 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -0,0 +1,21 @@ +import cgi +from paste.deploy 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 = [ + '%s\n' % greeting, + '

%s!

\n' % greeting, + '\n', + ] + for key in sorted(environ): + content.append('\n' + % (key, cgi.escape(repr(environ[key])))) + content.append('
%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 new file mode 100644 index 0000000..5d7bced --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,23 @@ +import sampleapp +from paste.deploy.config import ConfigMiddleware + +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 index ecb241d..865f453 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -7,6 +7,8 @@ next = 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 -- cgit v1.2.1 From a5836a12e821586a550b112af46112a6cbb87554 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 10 Jan 2006 09:10:23 +0000 Subject: Removed python2.4isms --- .../paster_templates/paste_deploy/+package+/sampleapp.py_tmpl | 6 ++++-- .../deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl index 4db7d13..ae55f97 100644 --- a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -12,9 +12,11 @@ def application(environ, start_response): '

%s!

\n' % greeting, '\n', ] - for key in sorted(environ): + items = environ.items() + items.sort() + for key, value in items: content.append('\n' - % (key, cgi.escape(repr(environ[key])))) + % (key, cgi.escape(repr(value)))) content.append('
%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 index 5d7bced..57155fc 100644 --- a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -13,7 +13,7 @@ def make_app( # 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.update(kw) conf['greeting'] = greeting # ConfigMiddleware means that paste.deploy.CONFIG will, # during this request (threadsafe) represent the -- cgit v1.2.1 From d57f7f2b13c581d12a6cadcf61b33310574afe89 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 23 Jan 2006 02:28:52 +0000 Subject: Add __file__ variable in addition to here (which is the containing directory) --- paste/deploy/loadwsgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index f30c03e..a34737f 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -305,6 +305,8 @@ class ConfigLoader(_Loader): self.parser.read(filename) self.parser._defaults.setdefault( 'here', os.path.dirname(os.path.abspath(filename))) + self.parser._defaults.setdefault( + '__file__', os.path.abspath(filename)) def get_context(self, object_type, name=None, global_conf=None): if self.absolute_name(name): -- cgit v1.2.1 From c612e4197b52d0465df10720c468b0ff2b9c14a5 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 23 Jan 2006 02:29:31 +0000 Subject: Added a more introductory section to the paste deploy docs --- docs/index.txt | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 60e5526..f29b60e 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -46,11 +46,124 @@ If you want to track development, do:: $ sudo python setup.py develop This will install the package globally, but will load the files in the -checkout. +checkout. You can also simply install ``PasteDeploy==dev``. For downloads and other information see the `Cheese Shop PasteDeploy page `_. +A complimentary package is `Paste Script `_. To install +that use ``easy_install PasteScript`` (or ``easy_install +PasteScript==dev``). + +From the User Perspective +------------------------- + +In the following sections, the Python API for using Paste Deploy is +given. This isn't what users will be using (but it is useful for +Python developers and useful for setting up tests fixtures). + +The primary interaction with Paste Deploy is through its configuration +files. The primary thing you want to do with a configuration file is +serve it. To learn about serving configuration files, see `the +``paster serve`` command +`_. + +The Config File +~~~~~~~~~~~~~~~ + +A config file has different sections. The only sections Paste Deploy +cares about have prefixes, like ``app:main`` or ``filter:errors`` -- +the part after the ``:`` is the "name" of the section, and the part +before gives the "type". Other sections are ignored. + +The format is a simple `INI format +`_: ``name = value``. You can +extend the value by indenting subsequent lines. ``#`` is a comment. + +Typically you have one or two sections, named "main": an application +section (``[app:main]``) and a server section (``[server:main]``). +``[composite:...]`` signifies something that dispatches to multiple +applications (example below). + +Here's a typical configuration file that also shows off mounting +multiple applications using `paste.urlmap +`_:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +I'll explain each section in detail now:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + +That this is a ``composite`` section means it dispatches the request +to other applications. ``use = egg:Paste#urlmap`` means to use the +composite application named ``urlmap`` from the ``Paste`` package. +``urlmap`` is a particularly common composite application -- it uses a +path prefix to map your request to another application. These are +the applications like "home", "blog" and "config:cms.ini". The last +one just refers to another file ``cms.ini`` in the same directory. + +Next up:: + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + +``egg:Paste#static`` is another simple application, in this case it +just serves up non-dynamic files. It takes one bit of configuration: +``document_root``. You can use variable substitution, which will pull +variables from the section ``[DEFAULT]`` (case sensitive!) with +markers like ``%(var_name)s``. The special variable ``%(here)s`` is +the directory containing the configuration file; you should use that +in lieu of relative filenames (which depend on the current directory, +which can change depending how the server is run). + +Lastly:: + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +The ``[filter-app:blog]`` section means that you want an application +with a filter applied. The application being filtered is indicated +with ``next`` (which refers to the next section). The +``egg:Authentication#auth`` filter doesn't actually exist, but one +could imagine it logs people in and checks permissions. + +That last section is just a reference to an application that you +probably installed with ``easy_install BlogApp``, and one bit of +configuration you passed to it (``database``). + +So, that's most of the features you'll use. + Basic Usage ----------- -- cgit v1.2.1 From 3ebf79b0aa185bb93585ee987e92829dce8cf8fd Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 23 Jan 2006 02:35:54 +0000 Subject: Fix the pudge config a bit --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 285e2fc..780079e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,8 +6,8 @@ tag_svn_revision = true theme = pythonpaste.org docs = docs/index.txt docs/news.txt dest = docs/html -modules = paste -title = Paste Core +modules = paste.deploy +title = Paste Deploy organization = Python Paste organization_url = http://pythonpaste.org/ trac_url = http://pythonpaste.org/trac/ -- cgit v1.2.1 From b45debcaf0cb6efde138ce9d2ed6d32a14e0872e Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 30 Jan 2006 09:22:07 +0000 Subject: Make sure section name defaults to main for appconfig --- paste/deploy/loadwsgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index a34737f..fe0ae88 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -211,6 +211,8 @@ def loadcontext(object_type, uri, name=None, relative_to=None, else: # @@: Ignore fragment or error? uri = uri.split('#', 1)[0] + if name is None: + name = 'main' scheme, path = uri.split(':', 1) scheme = scheme.lower() if scheme not in _loaders: -- cgit v1.2.1 From b773feff98189b408e17ac20643ae1e321dc601d Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 7 Mar 2006 02:02:33 +0000 Subject: News headline update for 0.5 --- docs/index.txt | 4 ++++ docs/news.txt | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index f29b60e..8a2b6eb 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -7,6 +7,10 @@ Paste Deployment .. contents:: +.. comment: + The names used in sections should be more concrete, and it should + be clearer that they are just arbitrary names. + Introduction ------------ diff --git a/docs/news.txt b/docs/news.txt index d5cf6e9..8b43c43 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +Version 0.5 +----------- * Made the ``paste_deploy`` template (used with ``paster create --template=paste_deploy``) more useful, with an example application -- cgit v1.2.1 From 0a286b7526ebae8cc10923fbd31fd030674d9bd1 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 7 Mar 2006 02:03:30 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a5f0484..5096695 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.5' +version = '0.5.1' setup( name="PasteDeploy", -- cgit v1.2.1 From 130b790ee71feef347882552f7cb1012c7722d09 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 13 Mar 2006 22:55:00 +0000 Subject: Make the error messages prettier when you call a function and fail to give an argument, like a required function argument --- paste/deploy/loadwsgi.py | 27 +++++++++++------- paste/deploy/util/fixtypeerror.py | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 paste/deploy/util/fixtypeerror.py diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index fe0ae88..91d26e8 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -3,6 +3,7 @@ import re import urllib from ConfigParser import ConfigParser import pkg_resources +from paste.deploy.util.fixtypeerror import fix_call __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] @@ -74,7 +75,8 @@ class _ObjectType(object): def invoke(self, context): assert context.protocol in _flatten(self.egg_protocols) - return context.object(context.global_conf, **context.local_conf) + return fix_call(context.object, + context.global_conf, **context.local_conf) class _App(_ObjectType): @@ -87,10 +89,11 @@ class _App(_ObjectType): def invoke(self, context): if context.protocol in ('paste.composit_factory', 'paste.composite_factory'): - return context.object(context.loader, context.global_conf, - **context.local_conf) + return fix_call(context.object, + context.loader, context.global_conf, + **context.local_conf) elif context.protocol == 'paste.app_factory': - return context.object(context.global_conf, **context.local_conf) + return fix_call(context.object, context.global_conf, **context.local_conf) else: assert 0, "Protocol %r unknown" % context.protocol @@ -103,12 +106,14 @@ class _Filter(_ObjectType): def invoke(self, context): if context.protocol == 'paste.filter_factory': - return context.object(context.global_conf, **context.local_conf) + 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 context.object(wsgi_app, context.global_conf, - **context.local_conf) + return fix_call(context.object, + wsgi_app, context.global_conf, + **context.local_conf) return filter_wrapper else: assert 0, "Protocol %r unknown" % context.protocol @@ -122,12 +127,14 @@ class _Server(_ObjectType): def invoke(self, context): if context.protocol == 'paste.server_factory': - return context.object(context.global_conf, **context.local_conf) + 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 context.object(wsgi_app, context.global_conf, - **context.local_conf) + return fix_call(context.object, + wsgi_app, context.global_conf, + **context.local_conf) return server_wrapper else: assert 0, "Protocol %r unknown" % context.protocol diff --git a/paste/deploy/util/fixtypeerror.py b/paste/deploy/util/fixtypeerror.py new file mode 100644 index 0000000..d513c6d --- /dev/null +++ b/paste/deploy/util/fixtypeerror.py @@ -0,0 +1,59 @@ +""" +Fixes the vague error message that you get when calling a function +with the wrong arguments. +""" +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 + import inspect + argspec = inspect.formatargspec(*inspect.getargspec(callable)) + args = ', '.join(map(_short_repr, varargs)) + if kwargs and args: + args += ', ' + if kwargs: + kwargs = kwargs.items() + kwargs.sort() + args += ', '.join(['%s=...' % n for n, v in kwargs]) + gotspec = '(%s)' % args + msg = '%s; got %s, wanted %s' % (exc_info[1], gotspec, 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[0], exc_info[1], exc_info[2] + return val -- cgit v1.2.1 From 9f4171027cb00b24206ce384afbf3d20c5d611e5 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 13 Jun 2006 16:13:41 +0000 Subject: Added copyright header to a bunch of files --- paste/deploy/__init__.py | 2 ++ paste/deploy/config.py | 2 ++ paste/deploy/converters.py | 2 ++ paste/deploy/interfaces.py | 2 ++ paste/deploy/loadwsgi.py | 2 ++ paste/deploy/paster_templates.py | 2 ++ paste/deploy/util/__init__.py | 2 ++ paste/deploy/util/fixtypeerror.py | 2 ++ 8 files changed, 16 insertions(+) diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index abf833f..f89218f 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1,3 +1,5 @@ +# (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 loadwsgi import * try: from config import CONFIG diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 912eeb0..696f7fd 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -1,3 +1,5 @@ +# (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 threading # Loaded lazily wsgilib = None diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py index f329174..f0ad349 100644 --- a/paste/deploy/converters.py +++ b/paste/deploy/converters.py @@ -1,3 +1,5 @@ +# (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 def asbool(obj): if isinstance(obj, (str, unicode)): obj = obj.strip().lower() diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index 8a03bdb..3da4bfe 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -1,3 +1,5 @@ +# (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 ############################################################ ## Functions ############################################################ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 91d26e8..405b08a 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,3 +1,5 @@ +# (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 import re import urllib diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py index c3de82b..4434898 100644 --- a/paste/deploy/paster_templates.py +++ b/paste/deploy/paster_templates.py @@ -1,3 +1,5 @@ +# (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 diff --git a/paste/deploy/util/__init__.py b/paste/deploy/util/__init__.py index 792d600..56bf54c 100644 --- a/paste/deploy/util/__init__.py +++ b/paste/deploy/util/__init__.py @@ -1 +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 # diff --git a/paste/deploy/util/fixtypeerror.py b/paste/deploy/util/fixtypeerror.py index d513c6d..b1fa0ec 100644 --- a/paste/deploy/util/fixtypeerror.py +++ b/paste/deploy/util/fixtypeerror.py @@ -1,3 +1,5 @@ +# (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 """ Fixes the vague error message that you get when calling a function with the wrong arguments. -- cgit v1.2.1 From f528ea3872e8a8610609a97d283dbf9b5c7ccde3 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 13 Jun 2006 16:17:49 +0000 Subject: Added copyright note --- docs/index.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.txt b/docs/index.txt index 8a2b6eb..aa74be9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -32,6 +32,9 @@ Paste Deployment currently does not require other parts of `Paste To see updates that have been made to Paste Deploy see the `news file `_. +Paste Deploy is released under the `MIT license +`_. + Installation ------------ -- cgit v1.2.1 From 9cffff8f94f67f62bd645d8b7b70d48a5c561dd7 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 15 Jun 2006 23:02:20 +0000 Subject: Added license text --- docs/license.txt | 20 ++++++++++++++++++++ setup.py | 1 + 2 files changed, 21 insertions(+) create mode 100644 docs/license.txt diff --git a/docs/license.txt b/docs/license.txt new file mode 100644 index 0000000..2541308 --- /dev/null +++ b/docs/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2006 Ian Bicking and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/setup.py b/setup.py index 5096695..ee6f1a2 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ For the latest changes see the `news file author="Ian Bicking", author_email="ianb@colorstudy.com", url="http://pythonpaste.org/deploy/", + license='MIT', namespace_packages=['paste'], packages=find_packages(exclude='tests'), package_data={ -- cgit v1.2.1 From 23601f195d279110fc1f0c894414ef6e5324da90 Mon Sep 17 00:00:00 2001 From: thejimmyg Date: Thu, 29 Jun 2006 09:58:33 +0000 Subject: Changed _loadconfig() so that it works with Windows absolute paths --- paste/deploy/loadwsgi.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 405b08a..14bd6d3 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -2,6 +2,7 @@ # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php import os import re +import sys import urllib from ConfigParser import ConfigParser import pkg_resources @@ -237,7 +238,15 @@ def _loadconfig(object_type, uri, path, name, relative_to, global_conf): # De-Windowsify the paths: path = path.replace('\\', '/') - if not path.startswith('/'): + absolute_path = True + if sys.platform == 'win32': + _absolute_re = re.compile(r'^[a-zA-Z]:') + if not _absolute_re.search(path): + absolute_path = False + else: + if not path.startswith('/'): + absolute_path = False + if not absolute_path: if not relative_to: raise ValueError( "Cannot resolve relative uri %r; no context keyword " -- cgit v1.2.1 From 100a5415de38a9a76cb9a307abd7d5383bb3bc61 Mon Sep 17 00:00:00 2001 From: bbangert Date: Mon, 10 Jul 2006 00:01:40 +0000 Subject: Adding prefix middleware which compensates for cases where the wsgi app is behind a proxy of some sort that isn't moving the prefix into the SCRIPT_NAME in advance. --- paste/deploy/config.py | 18 +++++++++++++++++- setup.py | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 696f7fd..11f3921 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -1,11 +1,12 @@ # (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 threading +import re # Loaded lazily wsgilib = None local = None -__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware'] +__all__ = ['DispatchingConfig', 'CONFIG', 'ConfigMiddleware', 'PrefixMiddleware'] def local_dict(): global config_local, local @@ -178,3 +179,18 @@ def make_config_filter(app, global_conf, **local_conf): conf.update(local_conf) return ConfigMiddleware(app, conf) +class PrefixMiddleware(object): + """Translate a given prefix into a SCRIPT_NAME for the filtered + application.""" + def __init__(self, app, global_conf=None, prefix='/'): + self.app = app + self.prefix = prefix + self.regprefix = re.compile("^%s(.*)$" % self.prefix) + + 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 + return self.app(environ, start_response) diff --git a/setup.py b/setup.py index ee6f1a2..267c25e 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ For the latest changes see the `news file entry_points=""" [paste.filter_app_factory] config = paste.deploy.config:make_config_filter [Config] + prefix = paste.deploy.config:PrefixMiddleware [paste.paster_create_template] paste_deploy=paste.deploy.paster_templates:PasteDeploy -- cgit v1.2.1 From bfd5b886bfc90d02fe6d1d744baf1f41afe441a5 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Thu, 27 Jul 2006 22:22:15 +0000 Subject: updating news --- docs/news.txt | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 8b43c43..0994219 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,15 +1,27 @@ Paste Deployment News ===================== -Version 0.5 ------------ +0.9.6 +----- + +* Added ``PrefixMiddleware`` which compensates for cases where the + wsgi app is behind a proxy of some sort that isn't moving the prefix + into the SCRIPT_NAME in advance. + +* Changed _loadconfig() so that it works with Windows absolute paths. + +* Make the error messages prettier when you call a function and fail +to give an argument, like a required function argument. + +0.5 +--- * Made the ``paste_deploy`` template (used with ``paster create --template=paste_deploy``) more useful, with an example application and entry point. -Version 0.4 ------------ +0.4 +--- * Allow filters to have ``filter-with`` values, just like applications. @@ -20,8 +32,8 @@ Version 0.4 * Added ``appconfig()`` to load along with ``loadapp()``, but return the configuration without invoking the application. -Version 0.3 ------------ +0.3 +--- * Allow variable setting like:: @@ -34,8 +46,8 @@ Version 0.3 as will variables in the same section. Also, the special value ``here`` will be the directory the configuration file is located in. -Version 0.2 ------------ +0.2 +--- Released 26 August 2004 @@ -55,8 +67,8 @@ Released 26 August 2004 * Added ``paste.deploy.converters`` module for handling the string values that are common with this system. -Version 0.1 ------------ +0.1 +--- Released 22 August 2004 -- cgit v1.2.1 From fb46296cc84d0641b98e0710d8be5fbdc768228d Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 27 Jul 2006 22:31:12 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 267c25e..af2fdbf 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.5.1' +version = '0.9.7' setup( name="PasteDeploy", -- cgit v1.2.1 From 3d5f36731626e323e8dbd05bd62510236f08bf92 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 10 Aug 2006 17:41:14 +0000 Subject: Added test for config middleware and exception --- tests/conftest.py | 6 ++---- tests/test_config_middleware.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 tests/test_config_middleware.py diff --git a/tests/conftest.py b/tests/conftest.py index d48c2d6..0ba8f8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,14 +3,12 @@ import sys here = os.path.dirname(__file__) base = os.path.dirname(here) -fake_packages = os.path.join(here, 'fake_packages') -sys.path.append(fake_packages) +sys.path.insert(0, base) # We can only import this after we adjust the paths import pkg_resources # Make absolutely sure we're testing *this* package, not # some other installed package -sys.path.insert(0, base) -#pkg_resources.require('Paste-Deploy') +pkg_resources.require('PasteDeploy') diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py new file mode 100644 index 0000000..0443dca --- /dev/null +++ b/tests/test_config_middleware.py @@ -0,0 +1,18 @@ +from paste.deploy.config import ConfigMiddleware +from paste.fixture import TestApp +from py.test import raises + +class Bug(Exception): pass + +def app_with_exception(environ, start_response): + def cont(): + yield "something" + raise Bug + start_response('200 OK', [('Content-type', 'text/html')]) + return cont() + +def test_error(): + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) + test_app = TestApp(wrapped) + raises(Bug, "test_app.get('/')") + -- cgit v1.2.1 From cc8b6bb3f2e3b8a91e926c7c03fdb67c44a03c44 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Fri, 11 Aug 2006 03:34:12 +0000 Subject: avoid popping the thread's config twice when app_iter is None --- paste/deploy/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 11f3921..766e5d9 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -156,19 +156,21 @@ class ConfigMiddleware(object): conf = environ['paste.config'] = self.config.copy() app_iter = None CONFIG.push_thread_config(conf) + popped_config = False try: app_iter = self.application(environ, start_response) finally: if app_iter is None: # An error occurred... CONFIG.pop_thread_config(conf) + popped_config = True 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) return app_iter - else: + elif not popped_config: def close_config(): CONFIG.pop_thread_config(conf) new_app_iter = wsgilib.add_close(app_iter, close_config) -- cgit v1.2.1 From 324820d046a8a515bb389e7f93cce674cd0a35d4 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Fri, 11 Aug 2006 04:12:54 +0000 Subject: changelog of r5530 --- docs/news.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/news.txt b/docs/news.txt index 0994219..991f2b6 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,12 @@ Paste Deployment News ===================== +0.9.7 +----- + +* Fixed a bug where ``ConfigMiddleware`` would attempt to pop the + thread's config twice. + 0.9.6 ----- @@ -11,7 +17,7 @@ Paste Deployment News * Changed _loadconfig() so that it works with Windows absolute paths. * Make the error messages prettier when you call a function and fail -to give an argument, like a required function argument. + to give an argument, like a required function argument. 0.5 --- -- cgit v1.2.1 From e7902749ba0b577e1138ab5fd8372104b1fdd9c3 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Fri, 11 Aug 2006 19:12:37 +0000 Subject: reverting r5451; woops, isn't needed --- docs/news.txt | 6 ------ paste/deploy/config.py | 4 +--- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 991f2b6..b4602d5 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,12 +1,6 @@ Paste Deployment News ===================== -0.9.7 ------ - -* Fixed a bug where ``ConfigMiddleware`` would attempt to pop the - thread's config twice. - 0.9.6 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 766e5d9..11f3921 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -156,21 +156,19 @@ class ConfigMiddleware(object): conf = environ['paste.config'] = self.config.copy() app_iter = None CONFIG.push_thread_config(conf) - popped_config = False try: app_iter = self.application(environ, start_response) finally: if app_iter is None: # An error occurred... CONFIG.pop_thread_config(conf) - popped_config = True 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) return app_iter - elif not popped_config: + else: def close_config(): CONFIG.pop_thread_config(conf) new_app_iter = wsgilib.add_close(app_iter, close_config) -- cgit v1.2.1 From fa347993dc4531f8972037a8a678095f2df85cf5 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 23 Aug 2006 19:21:33 +0000 Subject: Fixed problems with tests; FakeApp wasn't loading properly, and the __file__ changes made previously didn't update the tests (so they've been broken for a while) --- tests/fixture.py | 7 ++++++- tests/test_config.py | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/fixture.py b/tests/fixture.py index b2cc75f..6c3e99f 100644 --- a/tests/fixture.py +++ b/tests/fixture.py @@ -1,6 +1,6 @@ import os +import sys import shutil -from pkg_resources import * test_dir = os.path.dirname(__file__) egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', @@ -13,4 +13,9 @@ if not os.path.exists(egg_info_dir): except: shutil.copytree(info_dir, egg_info_dir) +sys.path.append(os.path.dirname(egg_info_dir)) + +from pkg_resources import * +working_set.add_entry(os.path.dirname(egg_info_dir)) require('FakeApp') + diff --git a/tests/test_config.py b/tests/test_config.py index 1761f17..7626b25 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,6 +6,7 @@ import fakeapp.configapps as fc ini_file = 'config:sample_configs/test_config.ini' here = os.path.dirname(__file__) config_path = os.path.join(here, 'sample_configs') +config_filename = os.path.join(config_path, 'test_config.ini') def test_config_egg(): app = loadapp('egg:FakeApp#configed') @@ -17,7 +18,8 @@ def test_config1(): 'setting1': 'foo', 'setting2': 'bar'} assert app.global_conf == { 'def1': 'a', 'def2': 'b', - 'here': config_path} + 'here': config_path, + '__file__': config_filename} def test_config2(): app = loadapp(ini_file, relative_to=here, name='test2') @@ -27,7 +29,8 @@ def test_config2(): 'def1': 'test2', 'def2': 'b', 'another': 'TEST', - 'here': config_path} + 'here': config_path, + '__file__': config_filename} # Run this to make sure the global-conf-modified test2 # didn't mess up the general global conf test_config1() @@ -42,7 +45,8 @@ def test_config3(): 'def1': 'test3', 'def2': 'b', 'another': 'TEST', - 'here': config_path} + 'here': config_path, + '__file__': config_filename} test_config2() def test_foreign_config(): @@ -56,7 +60,8 @@ def test_foreign_config(): 'def2': 'from include', 'def3': 'c', 'glob': 'override', - 'here': config_path} + 'here': config_path, + '__file__': os.path.join(config_path, 'test_config_included.ini')} def test_config_get(): app = loadapp(ini_file, relative_to=here, name='test_get') @@ -67,7 +72,8 @@ def test_config_get(): assert app.global_conf == { 'def1': 'a', 'def2': 'TEST', - 'here': config_path} + 'here': config_path, + '__file__': config_filename} def test_appconfig(): conf = appconfig(ini_file, relative_to=here, name='test_get') @@ -75,6 +81,7 @@ def test_appconfig(): 'def1': 'a', 'def2': 'TEST', 'here': config_path, + '__file__': config_filename, 'foo': 'TEST'} assert conf.local_conf == { 'def1': 'a', @@ -82,4 +89,5 @@ def test_appconfig(): assert conf.global_conf == { 'def1': 'a', 'def2': 'TEST', - 'here': config_path} + 'here': config_path, + '__file__': config_filename,} -- cgit v1.2.1 From 0d4c8eec0e99091b242e70acf6cb638fccd2e241 Mon Sep 17 00:00:00 2001 From: bbangert Date: Sun, 3 Sep 2006 03:57:29 +0000 Subject: Adding config module docstring and extended docs for PrefixMiddleware. --- paste/deploy/config.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 11f3921..04a1448 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -1,5 +1,6 @@ # (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 @@ -181,7 +182,42 @@ def make_config_filter(app, global_conf, **local_conf): class PrefixMiddleware(object): """Translate a given prefix into a SCRIPT_NAME for the filtered - application.""" + 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:: PasteIni + + 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. + + """ def __init__(self, app, global_conf=None, prefix='/'): self.app = app self.prefix = prefix -- cgit v1.2.1 From 1af24b4ef5506065c14c60ce562f6800f2a015a4 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 13 Oct 2006 21:50:50 +0000 Subject: Added documentation for all the entry points paste deploy uses --- docs/news.txt | 6 ++++++ paste/__init__.py | 2 +- paste/deploy/epdesc.py | 37 +++++++++++++++++++++++++++++++++++++ setup.py | 9 +++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 paste/deploy/epdesc.py diff --git a/docs/news.txt b/docs/news.txt index b4602d5..ba21c72 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,12 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Added some documentation for the different kinds of entry points + Paste Deploy uses. + 0.9.6 ----- diff --git a/paste/__init__.py b/paste/__init__.py index 5e2ef1d..ece379c 100644 --- a/paste/__init__.py +++ b/paste/__init__.py @@ -1,2 +1,2 @@ import pkg_resources -pkg_resources.declare_namespace('paste') +pkg_resources.declare_namespace(__name__) diff --git a/paste/deploy/epdesc.py b/paste/deploy/epdesc.py new file mode 100644 index 0000000..7c7deae --- /dev/null +++ b/paste/deploy/epdesc.py @@ -0,0 +1,37 @@ +class AppFactoryDescription(object): + description = """ + This gives a factory/function that can create WSGI apps + """ + +class CompositeFactoryDescription(object): + description = """ + This gives a factory/function that can create WSGI apps, and has + access to the application creator so that it can in turn fetch + apps based on name. + """ + +class FilterAppFactoryDescription(object): + description = """ + This gives a factory/function that wraps a WSGI application to + create another WSGI application (typically applying middleware) + """ + +class FilterFactoryDescription(object): + description = """ + This gives a factory/function that return a function that can wrap + a WSGI application and returns another WSGI application. + paste.filter_app_factory is the same thing with less layers. + """ + +class ServerFactoryDescription(object): + description = """ + This gives a factory/function that creates a server, that can be + called with a WSGI application to run indefinitely. + paste.server_runner is the same thing with less layers. + """ + +class ServerRunnerDescription(object): + description = """ + This gives a factory/function that, given a WSGI application and + configuration, will serve the application indefinitely. + """ diff --git a/setup.py b/setup.py index af2fdbf..2e260d1 100644 --- a/setup.py +++ b/setup.py @@ -49,5 +49,14 @@ For the latest changes see the `news file [paste.paster_create_template] paste_deploy=paste.deploy.paster_templates:PasteDeploy + + [paste.entry_point_description] + paste.app_factory = paste.deploy.epdesc:AppFactoryDescription + paste.composit_factory = paste.deploy.epdesc:CompositeFactoryDescription + paste.composite_factory = paste.deploy.epdesc:CompositeFactoryDescription + paste.filter_app_factory = paste.deploy.epdesc:FilterAppFactoryDescription + paste.filter_factory = paste.deploy.epdesc:FilterFactoryDescription + paste.server_factory = paste.deploy.epdesc:ServerFactoryDescription + paste.server_runner = paste.deploy.epdesc:ServerRunnerDescription """, ) -- cgit v1.2.1 From cb98fa8b0bbab716cf1da062369506c3c097149d Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 20 Oct 2006 19:12:40 +0000 Subject: Add docstring to config middleware entry point --- paste/deploy/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 04a1448..9a07660 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -180,6 +180,8 @@ def make_config_filter(app, global_conf, **local_conf): conf.update(local_conf) return ConfigMiddleware(app, conf) +make_config_middleware = ConfigMiddleware.__doc__ + class PrefixMiddleware(object): """Translate a given prefix into a SCRIPT_NAME for the filtered application. -- cgit v1.2.1 From 703a4e59c34982e8271df6f5db8a6b9fca48cded Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 20 Oct 2006 23:03:26 +0000 Subject: Added X-Forwarded-Host translation --- docs/news.txt | 3 +++ paste/deploy/config.py | 22 +++++++++++++++++++++- setup.py | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index ba21c72..6749e36 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -7,6 +7,9 @@ svn trunk * Added some documentation for the different kinds of entry points Paste Deploy uses. +* Added a feature to ``PrefixMiddleware`` that translates the + ``X-Forwarded-Server`` header to ``Host``. + 0.9.6 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 9a07660..608efdf 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -218,11 +218,17 @@ class PrefixMiddleware(object): 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. """ - def __init__(self, app, global_conf=None, prefix='/'): + def __init__(self, app, global_conf=None, prefix='/', + translate_forwarded_server=True): self.app = app self.prefix = prefix + self.translate_forwarded_server = translate_forwarded_server self.regprefix = re.compile("^%s(.*)$" % self.prefix) def __call__(self, environ, start_response): @@ -231,4 +237,18 @@ class PrefixMiddleware(object): if not url: url = '/' environ['PATH_INFO'] = url environ['SCRIPT_NAME'] = self.prefix + if (self.translate_forwarded_server and + 'HTTP_X_FORWARDED_SERVER' in environ): + environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') return self.app(environ, start_response) + +def make_prefix_middleware( + app, global_conf, prefix='/', + translate_forwarded_server=True): + from paste.deploy.converters import asbool + translate_forwarded_server = asbool(translate_forwarded_server) + return PrefixMiddleware( + app, prefix=prefix, + translate_forwarded_server=translate_forwarded_server) + +make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ diff --git a/setup.py b/setup.py index 2e260d1..8c0a98e 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ For the latest changes see the `news file entry_points=""" [paste.filter_app_factory] config = paste.deploy.config:make_config_filter [Config] - prefix = paste.deploy.config:PrefixMiddleware + prefix = paste.deploy.config:make_prefix_middleware [paste.paster_create_template] paste_deploy=paste.deploy.paster_templates:PasteDeploy -- cgit v1.2.1 From ed4a3f8fb36bc98b444e32db2e413bd5bf810bd8 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 22 Oct 2006 20:33:08 +0000 Subject: Update docs for 1.0 --- docs/index.txt | 17 +++++++++++++++++ docs/news.txt | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index aa74be9..8ffb455 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -35,6 +35,23 @@ To see updates that have been made to Paste Deploy see the `news file Paste Deploy is released under the `MIT license `_. +Status +------ + +Paste Deploy has passed version 1.0. Paste Script is an actively +maintained project. As of 1.0, we'll make a strong effort to maintain +backward compatibility (this actually started happening long before +1.0, but now it is explicit). This will include deprecation warnings +when necessary. Major changes will take place under new functions or +with new entry points. + +Note that the most key aspect of Paste Deploy is the entry points it +defines (such as ``paste.app_factory``). Paste Deploy is not the only +consumer of these entry points, and many extensions can best take +place by utilizing the entry points instead of using Paste Deploy +directly. The entry points will not change; if changes are necessary, +new entry points will be defined. + Installation ------------ diff --git a/docs/news.txt b/docs/news.txt index 6749e36..a625a89 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.0 +--- * Added some documentation for the different kinds of entry points Paste Deploy uses. -- cgit v1.2.1 From 1384a5408e8fed6d54f0eedae54bbba1daad024d Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 22 Oct 2006 20:52:57 +0000 Subject: Updated trove classifiers --- setup.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 8c0a98e..a1196ea 100644 --- a/setup.py +++ b/setup.py @@ -18,14 +18,18 @@ The latest version is available in a `Subversion repository For the latest changes see the `news file `_. """, - classifiers=["Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", - "Programming Language :: Python", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Software Development :: Libraries :: Python Modules", - ], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Python Software Foundation License", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", + "Framework :: Paste", + ], keywords='web wsgi application server', author="Ian Bicking", author_email="ianb@colorstudy.com", -- cgit v1.2.1 From 6cdcd4075ddb27b8c79317c8c4b0337915f4615d Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 22 Oct 2006 20:58:38 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a1196ea..91a7992 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '0.9.7' +version = '1.0.1' setup( name="PasteDeploy", -- cgit v1.2.1 From bafbac278384b5b17697886c32118a92e80677d5 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 6 Nov 2006 19:17:22 +0000 Subject: modulefinder change --- paste/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/paste/__init__.py b/paste/__init__.py index ece379c..e46fb43 100644 --- a/paste/__init__.py +++ b/paste/__init__.py @@ -1,2 +1,14 @@ -import pkg_resources -pkg_resources.declare_namespace(__name__) +# (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__) + +import modulefinder +for p in __path__: + modulefinder.AddPackagePath(__name__, p) + -- cgit v1.2.1 From eb4f10276b95cf9cf2522cc1974bc23fbf30fff6 Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 12 Nov 2006 20:05:45 +0000 Subject: Allow global_conf settings passed through to show up as config defaults, available for interpolation --- docs/news.txt | 8 ++++++++ paste/deploy/loadwsgi.py | 8 ++++++++ tests/sample_configs/test_config.ini | 4 ++++ tests/test_config.py | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index a625a89..2360c1e 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,14 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other + loaders) will show up as though they were in ``[DEFAULT]``, so they + can be used in variable interpolation. Note: this won't overwrite + any existing values in ``[DEFAULT]``. + 1.0 --- diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 14bd6d3..11f9c55 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -260,6 +260,8 @@ def _loadconfig(object_type, uri, path, name, relative_to, path = path[2:] path = urllib.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 @@ -328,6 +330,12 @@ class ConfigLoader(_Loader): self.parser._defaults.setdefault( '__file__', os.path.abspath(filename)) + 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, diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini index 6459df1..69bae5a 100644 --- a/tests/sample_configs/test_config.ini +++ b/tests/sample_configs/test_config.ini @@ -30,3 +30,7 @@ use = egg:FakeApp#configed set def2 = TEST get def1 = def1 get foo = def2 + +[app:test_global_conf] +use = egg:FakeApp#configed +test_interp = this:%(inherit)s diff --git a/tests/test_config.py b/tests/test_config.py index 7626b25..cf19cf5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,6 +2,7 @@ import os from paste.deploy import loadapp, loadfilter, appconfig from fixture import * import fakeapp.configapps as fc +from pprint import pprint ini_file = 'config:sample_configs/test_config.ini' here = os.path.dirname(__file__) @@ -55,6 +56,7 @@ def test_foreign_config(): assert app.local_conf == { 'another': 'FOO', 'bob': 'your uncle'} + pprint(app.global_conf) assert app.global_conf == { 'def1': 'a', 'def2': 'from include', @@ -91,3 +93,20 @@ def test_appconfig(): 'def2': 'TEST', 'here': config_path, '__file__': config_filename,} + +def test_global_conf(): + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + pprint(conf) + assert conf == { + 'def1': 'a', + # Note that this gets overwritten: + 'def2': 'b', + 'here': config_path, + 'inherit': 'bazbar', + '__file__': config_filename, + 'test_interp': 'this:bazbar', + } + assert conf.local_conf == { + 'test_interp': 'this:bazbar', + } + -- cgit v1.2.1 From b7a9182c07267fb8108c9ce7da480655192d9873 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 1 Dec 2006 00:12:08 +0000 Subject: Give better error message when scheme is missing --- paste/deploy/loadwsgi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 11f9c55..3894bec 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -223,6 +223,8 @@ def loadcontext(object_type, uri, name=None, relative_to=None, 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: -- cgit v1.2.1 From b7330a4ee6eec6717f26f773041fbe046aceb4ce Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 6 Dec 2006 01:15:28 +0000 Subject: remove blog link --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 780079e..b5d9260 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ title = Paste Deploy organization = Python Paste organization_url = http://pythonpaste.org/ trac_url = http://pythonpaste.org/trac/ -blog_url = http://pythonpaste.org/news/ mailing_list_url = http://pythonpaste.org/community/mailing-list.html settings = no_about=true link1=/deploy/ paste.deploy -- cgit v1.2.1 From 036190051d1ccb30db6bd9a3e2002a65875a75f6 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 14 Dec 2006 22:10:55 +0000 Subject: Added force_port option --- docs/news.txt | 3 +++ paste/deploy/config.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 2360c1e..c8bfe3b 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -9,6 +9,9 @@ svn trunk can be used in variable interpolation. Note: this won't overwrite any existing values in ``[DEFAULT]``. +* Added ``force_port`` option to + ``paste.deploy.config.PrefixMiddleware`` + 1.0 --- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 608efdf..363ee59 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -222,14 +222,23 @@ class PrefixMiddleware(object): 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. + + 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. """ def __init__(self, app, global_conf=None, prefix='/', - translate_forwarded_server=True): + translate_forwarded_server=True, + force_port=None): self.app = app self.prefix = prefix self.translate_forwarded_server = translate_forwarded_server self.regprefix = re.compile("^%s(.*)$" % self.prefix) + self.force_port = force_port def __call__(self, environ, start_response): url = environ['PATH_INFO'] @@ -240,15 +249,29 @@ class PrefixMiddleware(object): if (self.translate_forwarded_server and 'HTTP_X_FORWARDED_SERVER' in environ): environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') + if self.force_port is not None: + host = environ.get('HTTP_HOST', '').split(':', 1)[0] + if self.force_port: + host = '%s:%s' % (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 return self.app(environ, start_response) def make_prefix_middleware( app, global_conf, prefix='/', - translate_forwarded_server=True): + translate_forwarded_server=True, + force_port=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) + translate_forwarded_server=translate_forwarded_server, + force_port=force_port) make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ -- cgit v1.2.1 From d25fdf1c496a4ee6bac8735396f1f0d7175af0dd Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 14 Dec 2006 22:22:07 +0000 Subject: Strip any trailing /'s from prefix in PrefixMiddleware. Specifically the default of '/' will cause breakage --- docs/news.txt | 4 +++- paste/deploy/config.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index c8bfe3b..58beaee 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -10,7 +10,9 @@ svn trunk any existing values in ``[DEFAULT]``. * Added ``force_port`` option to - ``paste.deploy.config.PrefixMiddleware`` + ``paste.deploy.config.PrefixMiddleware``. Also the ``prefix`` + argument is stripped of any trailing ``/``, which can't be valid in + that position. 1.0 --- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 363ee59..bf271a3 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -235,7 +235,7 @@ class PrefixMiddleware(object): translate_forwarded_server=True, force_port=None): self.app = app - self.prefix = prefix + self.prefix = prefix.rstrip('/') self.translate_forwarded_server = translate_forwarded_server self.regprefix = re.compile("^%s(.*)$" % self.prefix) self.force_port = force_port -- cgit v1.2.1 From edbbe06ecadd4a5e7aa67addc2e82d371fc0b7ed Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 18 Dec 2006 01:10:33 +0000 Subject: rename news section --- docs/news.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 58beaee..048dcb2 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.1 +--- * Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other loaders) will show up as though they were in ``[DEFAULT]``, so they -- cgit v1.2.1 From f52df403e6afd2492cb728a5296c20ab2ec11627 Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 18 Dec 2006 01:11:01 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 91a7992..4d57404 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.0.1' +version = '1.1.1' setup( name="PasteDeploy", -- cgit v1.2.1 From 4d31af776b67bdaf81d9e0b8e6d8d419797c78f4 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Thu, 21 Dec 2006 21:57:56 +0000 Subject: restore the popped paste.config to environ when appropriate --- paste/deploy/config.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index bf271a3..dd81195 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -154,6 +154,9 @@ class ConfigMiddleware(object): 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) @@ -163,11 +166,15 @@ class ConfigMiddleware(object): 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(): -- cgit v1.2.1 From d02e07d736beb543de077c8587cf03b6ff3baeb8 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Fri, 5 Jan 2007 03:00:20 +0000 Subject: covert old-style classes to new-style classes --- paste/deploy/interfaces.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index 3da4bfe..29163a5 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -37,7 +37,7 @@ def loadserver(uri, name=None, relative_to=None, global_conf=None): ## Factories ############################################################ -class IPasteAppFactory: +class IPasteAppFactory(object): """ This is the spec for the ``paste.app_factory`` @@ -55,7 +55,7 @@ class IPasteAppFactory: capture these values). """ -class IPasteCompositFactory: +class IPasteCompositFactory(object): """ This is the spec for the ``paste.composit_factory`` @@ -73,7 +73,7 @@ class IPasteCompositFactory: applications. """ -class IPasteFilterFactory: +class IPasteFilterFactory(object): """ This is the spec for the ``paste.filter_factory`` @@ -85,7 +85,7 @@ class IPasteFilterFactory: Returns a IFilter object. """ -class IPasteFilterAppFactory: +class IPasteFilterAppFactory(object): """ This is the spec for the ``paste.filter_app_factory`` @@ -100,7 +100,7 @@ class IPasteFilterAppFactory: objects that implement the IFilter interface. """ -class IPasteServerFactory: +class IPasteServerFactory(object): """ This is the spec for the ``paste.server_factory`` @@ -112,7 +112,7 @@ class IPasteServerFactory: Returns a IServer object. """ -class IPasteServerRunner: +class IPasteServerRunner(object): """ This is the spec for the ``paste.server_runner`` @@ -129,7 +129,7 @@ class IPasteServerRunner: objects that implement the IServer interface. """ -class ILoader: +class ILoader(object): """ This is an object passed into ``IPasteCompositFactory``. It is @@ -161,7 +161,7 @@ class ILoader: ## Objects ############################################################ -class IWSGIApp: +class IWSGIApp(object): """ This is an application that conforms to `PEP 333 @@ -175,7 +175,7 @@ class IWSGIApp: an iterator for the body of the response. """ -class IFilter: +class IFilter(object): """ A filter is a simple case of middleware, where an object @@ -188,7 +188,7 @@ class IFilter: ``wsgi_app`` passed in. """ -class IServer: +class IServer(object): """ A simple server interface. -- cgit v1.2.1 From 0b0f90c179589ba1a44ab120943a5517e4c48235 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 11 Jan 2007 22:10:53 +0000 Subject: use pygments --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b5d9260..fe8d247 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ settings = no_about=true link2=/script/ paster script link3=/download/ Download extra_credits=Hosting courtesy of Tummy.com +highlighter = pygments [publish] doc-dir=docs/html -- cgit v1.2.1 From abc13e2f021bd5951ddb18b4972f11f60b1f5b7e Mon Sep 17 00:00:00 2001 From: pjenvey Date: Thu, 15 Feb 2007 08:22:02 +0000 Subject: updated license year --- docs/license.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/license.txt b/docs/license.txt index 2541308..c810dec 100644 --- a/docs/license.txt +++ b/docs/license.txt @@ -1,4 +1,4 @@ -Copyright (c) 2006 Ian Bicking and Contributors +Copyright (c) 2006-2007 Ian Bicking and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -- cgit v1.2.1 From b5ecf78380db1784bf5179614c6b6fc0f38ee052 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 21 Feb 2007 04:46:33 +0000 Subject: allow PrefixMiddleware to set the scheme --- docs/news.txt | 6 ++++++ paste/deploy/config.py | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 048dcb2..15ef148 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,12 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Added ``scheme`` option to ``PrefixMiddleware``, so you can force a + scheme (E.g., when proxying an HTTPS connection over HTTP). + 1.1 --- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index dd81195..094f72e 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -236,16 +236,19 @@ class PrefixMiddleware(object): 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): + 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'] @@ -268,17 +271,19 @@ class PrefixMiddleware(object): 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): + 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) + force_port=force_port, scheme=scheme) make_prefix_middleware.__doc__ = PrefixMiddleware.__doc__ -- cgit v1.2.1 From 8d8628669fc33e2743e6f5507bc0dd119de798be Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 6 Mar 2007 03:56:19 +0000 Subject: Use include_package_data instead of enumerating package data (fixes #152) --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 4d57404..43884e3 100644 --- a/setup.py +++ b/setup.py @@ -37,10 +37,7 @@ For the latest changes see the `news file license='MIT', namespace_packages=['paste'], packages=find_packages(exclude='tests'), - package_data={ - 'paste.deploy': ['paster_templates/paste_deploy/docs/*_tmpl', - 'paster_templates/paste_deploy/docs/*.txt'], - }, + include_package_data=True, zip_safe=False, extras_require={ 'Config': [], -- cgit v1.2.1 From 58dad40a6dc3b1428dc8dbcb87efdf1532f68f8c Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 1 Apr 2007 18:09:45 +0000 Subject: change default http server in PasteScript template --- paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 865f453..71ea310 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -16,7 +16,7 @@ greeting = Aloha! use = devel [server:main] -use = egg:PasteScript#wsgiutils +use = egg:Page#http # Change to 0.0.0.0 to make public: host = 127.0.0.1 port = 8080 -- cgit v1.2.1 From 66ec468d16f0db9197cfbfc5325409d746c3dbcb Mon Sep 17 00:00:00 2001 From: ianb Date: Sun, 1 Apr 2007 19:11:19 +0000 Subject: doh, typo --- paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 71ea310..0c0ae35 100644 --- a/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl @@ -16,7 +16,7 @@ greeting = Aloha! use = devel [server:main] -use = egg:Page#http +use = egg:Paste#http # Change to 0.0.0.0 to make public: host = 127.0.0.1 port = 8080 -- cgit v1.2.1 From eb90de107b81474b9247ec2256633cd0928b68e3 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Apr 2007 02:41:41 +0000 Subject: added a news item on paste.config --- docs/news.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index 15ef148..4427640 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -7,6 +7,9 @@ svn trunk * Added ``scheme`` option to ``PrefixMiddleware``, so you can force a scheme (E.g., when proxying an HTTPS connection over HTTP). +* Pop proper values into ``environ['paste.config']`` in + ``ConfigMiddleware``. + 1.1 --- -- cgit v1.2.1 From a09656a0f70b28a60875cb389c83596a7244b9df Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Apr 2007 02:42:47 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 43884e3..c85bfe9 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.1.1' +version = '1.3.1' setup( name="PasteDeploy", -- cgit v1.2.1 From 8f6bce1de76c1268af5a71043eac454bf64272cf Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Apr 2007 02:43:53 +0000 Subject: update news version --- docs/news.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 4427640..63c42c3 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.3 +--- * Added ``scheme`` option to ``PrefixMiddleware``, so you can force a scheme (E.g., when proxying an HTTPS connection over HTTP). -- cgit v1.2.1 From 6b3125beb512d1f38871027dc98957abb47f6e3a Mon Sep 17 00:00:00 2001 From: rflosi Date: Tue, 8 May 2007 15:52:38 +0000 Subject: strip filename; use %r in OSError --- paste/deploy/loadwsgi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 3894bec..9e1fd13 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -317,7 +317,7 @@ class _Loader(object): class ConfigLoader(_Loader): def __init__(self, filename): - self.filename = filename + self.filename = filename = filename.strip() self.parser = NicerConfigParser(self.filename) # Don't lower-case keys: self.parser.optionxform = str @@ -325,7 +325,7 @@ class ConfigLoader(_Loader): # we have to add an extra check: if not os.path.exists(filename): raise OSError( - "File %s not found" % filename) + "File %r not found" % filename) self.parser.read(filename) self.parser._defaults.setdefault( 'here', os.path.dirname(os.path.abspath(filename))) -- cgit v1.2.1 From f4d7c110acb14226430a072a6455d498771a710d Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 25 May 2007 02:55:48 +0000 Subject: Fix appconfig and filter-with --- docs/news.txt | 7 +++++++ paste/deploy/loadwsgi.py | 2 +- tests/sample_configs/test_filter_with.ini | 1 + tests/test_config.py | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/news.txt b/docs/news.txt index 63c42c3..5448bf5 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,13 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Fix ``appconfig`` config loading when using a config file with + ``filter-with`` in it (previously you'd get TypeError: iteration + over non-sequence) + 1.3 --- diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 9e1fd13..3bb820f 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -400,7 +400,7 @@ class ConfigLoader(_Loader): obj=None, object_type=FILTER_WITH, protocol=None, - global_conf=None, local_conf=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) diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini index ec6592b..118804f 100644 --- a/tests/sample_configs/test_filter_with.ini +++ b/tests/sample_configs/test_filter_with.ini @@ -1,5 +1,6 @@ [app:main] use = egg:FakeApp#basic_app +example = test filter-with = filter1 [filter:filter1] diff --git a/tests/test_config.py b/tests/test_config.py index cf19cf5..6fffe82 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -94,6 +94,10 @@ def test_appconfig(): 'here': config_path, '__file__': config_filename,} +def test_appconfig_filter_with(): + conf = appconfig('config:test_filter_with.ini', relative_to=config_path) + assert conf['example'] == 'test' + def test_global_conf(): conf = appconfig(ini_file, relative_to=here, name='test_global_conf', global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) pprint(conf) -- cgit v1.2.1 From 726389ea474c4fe522811d8ec5fa6f13034a26c4 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 26 Jun 2007 17:15:42 +0000 Subject: add pudge/publish/distribute commands --- setup.cfg | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/setup.cfg b/setup.cfg index fe8d247..a8f5b23 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,24 +2,31 @@ tag_build = dev tag_svn_revision = true +[global] +command_packages = buildutils.pudge_command, buildutils.publish_command + [pudge] -theme = pythonpaste.org -docs = docs/index.txt docs/news.txt +highlighter = pygments +title = Paste Deploy dest = docs/html +docs = docs/index.txt docs/news.txt +settings = no_about=true + link1=/deploy/ paste.deploy + link2=/script/ paster script + link3=/download/ Download + extra_credits=Hosting courtesy of Tummy.com modules = paste.deploy -title = Paste Deploy +theme = pythonpaste.org +mailing_list_url = http://pythonpaste.org/community/mailing-list.html organization = Python Paste organization_url = http://pythonpaste.org/ trac_url = http://pythonpaste.org/trac/ -mailing_list_url = http://pythonpaste.org/community/mailing-list.html -settings = no_about=true - link1=/deploy/ paste.deploy - link2=/script/ paster script - link3=/download/ Download - extra_credits=Hosting courtesy of Tummy.com -highlighter = pygments [publish] -doc-dir=docs/html -doc-dest=scp://ianb@webwareforpython.org/home/paste/htdocs/deploy -make-dirs=1 +doc-dest = scp://ianb@webwareforpython.org/home/paste/htdocs/deploy +make-dirs = 1 +doc-dir = docs/html + +[aliases] +distribute = register sdist bdist_egg upload pudge publish + -- cgit v1.2.1 From c265f34a1b6dcc93fb1f1dddc41d87179b8f90e2 Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 26 Jun 2007 17:15:58 +0000 Subject: prepare news for release --- docs/news.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 5448bf5..df1ae24 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.3.1 +----- * Fix ``appconfig`` config loading when using a config file with ``filter-with`` in it (previously you'd get TypeError: iteration -- cgit v1.2.1 From 241dc930aff3f6a03922ecf5754e39a35054f25b Mon Sep 17 00:00:00 2001 From: ianb Date: Tue, 26 Jun 2007 17:16:33 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c85bfe9..1a5ae70 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.3.1' +version = '1.3.2' setup( name="PasteDeploy", -- cgit v1.2.1 From fd5f40ba4467ec1d3342ebca0ce344f19859fc1c Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 13 Aug 2007 22:13:42 +0000 Subject: Use X-Forwarded-Host if available --- paste/deploy/config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 094f72e..91c1ab2 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -256,9 +256,11 @@ class PrefixMiddleware(object): if not url: url = '/' environ['PATH_INFO'] = url environ['SCRIPT_NAME'] = self.prefix - if (self.translate_forwarded_server and - 'HTTP_X_FORWARDED_SERVER' in environ): - environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') + if self.translate_forwarded_server: + if 'HTTP_X_FORWARDED_SERVER' in environ: + environ['SERVER_NAME'] = environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') + if 'HTTP_X_FORWARDED_HOST' in environ: + environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST') if self.force_port is not None: host = environ.get('HTTP_HOST', '').split(':', 1)[0] if self.force_port: -- cgit v1.2.1 From cdeb430061444a0aa9b4009f11b8352979e72260 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 15 Aug 2007 16:09:58 +0000 Subject: Improve the error message when there's trailing whitespace in a filename --- paste/deploy/loadwsgi.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 3bb820f..29e880d 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -324,6 +324,11 @@ class ConfigLoader(_Loader): # Stupid ConfigParser ignores files that aren't found, so # we have to add an extra check: if not os.path.exists(filename): + if filename.strip() != filename: + raise OSError( + "File %r not found; trailing whitespace: " + "did you try to use a # on the same line as a filename? " + "(comments must be on their own line)" % filename) raise OSError( "File %r not found" % filename) self.parser.read(filename) -- cgit v1.2.1 From ffe7f2c41fd8b34f6b1c424da79de2cabeeadbe3 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 10 Oct 2007 00:49:36 +0000 Subject: Added paste.deploy.converters.asint (#155) --- docs/news.txt | 5 +++++ paste/deploy/converters.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index df1ae24..e121fbc 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,11 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Added ``paste.deploy.converters.asint()``. + 1.3.1 ----- diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py index f0ad349..98edd8f 100644 --- a/paste/deploy/converters.py +++ b/paste/deploy/converters.py @@ -12,6 +12,13 @@ def asbool(obj): "String is not true/false: %r" % obj) return bool(obj) +def asint(obj): + try: + return int(obj) + except (TypeError, ValueError), e: + raise ValueError( + "Bad integer value: %r" % obj) + def aslist(obj, sep=None, strip=True): if isinstance(obj, (str, unicode)): lst = obj.split(sep) -- cgit v1.2.1 From b7cb3dc6036db7382d53e4f67e9fc04d199bfd43 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 9 Apr 2008 17:01:51 +0000 Subject: Make modulefinder import conditional --- paste/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/paste/__init__.py b/paste/__init__.py index e46fb43..cdb6121 100644 --- a/paste/__init__.py +++ b/paste/__init__.py @@ -8,7 +8,11 @@ except ImportError: from pkgutil import extend_path __path__ = extend_path(__path__, __name__) -import modulefinder -for p in __path__: - modulefinder.AddPackagePath(__name__, p) +try: + import modulefinder +except ImportError: + pass +else: + for p in __path__: + modulefinder.AddPackagePath(__name__, p) -- cgit v1.2.1 From 439617a19bd8ede40576e03df5eba2714e127cfe Mon Sep 17 00:00:00 2001 From: pjenvey Date: Tue, 22 Apr 2008 05:20:46 +0000 Subject: fixed use sections overwriting the config's __file__ value with the use'd filename --- docs/news.txt | 2 ++ paste/deploy/loadwsgi.py | 3 +++ tests/test_config.py | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/news.txt b/docs/news.txt index e121fbc..ad867fc 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -5,6 +5,8 @@ svn trunk --------- * Added ``paste.deploy.converters.asint()``. +* fixed use sections overwriting the config's __file__ value with the + use'd filename. 1.3.1 ----- diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 29e880d..bd2ff75 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -420,6 +420,9 @@ class ConfigLoader(_Loader): 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 return context diff --git a/tests/test_config.py b/tests/test_config.py index 6fffe82..26ba482 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -63,7 +63,7 @@ def test_foreign_config(): 'def3': 'c', 'glob': 'override', 'here': config_path, - '__file__': os.path.join(config_path, 'test_config_included.ini')} + '__file__': os.path.join(config_path, 'test_config.ini')} def test_config_get(): app = loadapp(ini_file, relative_to=here, name='test_get') -- cgit v1.2.1 From 5887c196ccb777ba8ac4c5301631e4d2443a3268 Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 29 May 2008 00:13:58 +0000 Subject: sphinx-ify --- docs/conf.py | 132 ++++++++++++++++++++++++++++++++++++++++++++ docs/index.txt | 8 +++ docs/modules/config.txt | 13 +++++ docs/modules/converters.txt | 11 ++++ docs/modules/loadwsgi.txt | 13 +++++ regen-docs | 9 +++ 6 files changed, 186 insertions(+) create mode 100644 docs/conf.py create mode 100644 docs/modules/config.txt create mode 100644 docs/modules/converters.txt create mode 100644 docs/modules/loadwsgi.txt create mode 100755 regen-docs diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..a2c1de4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Paste Deploy' +copyright = '2008, Ian Bicking' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '1.3' +# The full version, including alpha/beta/rc tags. +release = '1.3.2' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = ['include/contact.txt', 'include/reference_header.txt'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PasteDeploydoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.txt b/docs/index.txt index 8ffb455..0e05b24 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -5,6 +5,14 @@ Paste Deployment :revision: $Rev$ :date: $LastChangedDate$ +Contents: + +.. toctree:: + :maxdepth: 1 + + news + license + .. contents:: .. comment: diff --git a/docs/modules/config.txt b/docs/modules/config.txt new file mode 100644 index 0000000..9d8f894 --- /dev/null +++ b/docs/modules/config.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.config` -- Configuration and Environment middleware +====================================================================== + +.. automodule:: paste.deploy.config + +Module Contents +--------------- + +.. autoclass:: DispatchingConfig +.. autoclass:: ConfigMiddleware +.. autoclass:: PrefixMiddleware + +.. comment: FIXME: do something about CONFIG (manual docs?) diff --git a/docs/modules/converters.txt b/docs/modules/converters.txt new file mode 100644 index 0000000..4948104 --- /dev/null +++ b/docs/modules/converters.txt @@ -0,0 +1,11 @@ +:mod:`paste.deploy.converters` -- Conversion helpers for String Configuration +============================================================================= + +.. automodule:: paste.deploy.converters + +Module Contents +--------------- + +.. autofunction:: asbool +.. autofunction:: asint +.. autofunction:: aslist diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt new file mode 100644 index 0000000..fc3dc4c --- /dev/null +++ b/docs/modules/loadwsgi.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.loadwsgi` -- Load WSGI applications from config files +======================================================================== + +.. automodule:: paste.deploy.loadwsgi + +Module Contents +--------------- + +.. autofunction:: loadapp +.. autofunction:: loadserver +.. autofunction:: loadfilter +.. autofunction;: appconfig + diff --git a/regen-docs b/regen-docs new file mode 100755 index 0000000..f8dad75 --- /dev/null +++ b/regen-docs @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + scp -r _build/* ianb@webwareforpython.org:/home/paste/htdocs/deploy/ +fi -- cgit v1.2.1 From f4efc44c85935430f5e75aa7498c7cfe3069af43 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Sun, 1 Jun 2008 23:37:51 +0000 Subject: stray from normal ConfigParser behavior and interpolate defaults (with themselves). mainly to support using %(here)s --- paste/deploy/loadwsgi.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index bd2ff75..bffa500 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -46,6 +46,17 @@ class NicerConfigParser(ConfigParser): ConfigParser.__init__(self, *args, **kw) self.filename = filename + 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.iteritems(): + defaults[key] = self._interpolate('DEFAULT', key, val, defaults) + return defaults + def _interpolate(self, section, option, rawval, vars): try: return ConfigParser._interpolate( -- cgit v1.2.1 From 066cace4d62d06ffc3c30742889c2e364f7063bc Mon Sep 17 00:00:00 2001 From: pjenvey Date: Thu, 12 Jun 2008 22:56:01 +0000 Subject: changelog for r7382 --- docs/news.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index ad867fc..2056461 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -7,6 +7,8 @@ svn trunk * Added ``paste.deploy.converters.asint()``. * fixed use sections overwriting the config's __file__ value with the use'd filename. +* ``paste.deploy.loadwsgi`` now supports variable expansion in the + DEFAULT section of config files (unlike plain ConfigParser). 1.3.1 ----- -- cgit v1.2.1 From 721c6d4ed7b8a598c893547cb55f2a48ad7dcbcd Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 13 Jun 2008 16:12:13 +0000 Subject: use an ini-language block in docstring --- paste/deploy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 91c1ab2..6981756 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -215,7 +215,7 @@ class PrefixMiddleware(object): To filter your application through a PrefixMiddleware instance, add the following to the '[app:main]' section of your .ini file: - .. code-block:: PasteIni + .. code-block:: ini filter-with = proxy-prefix -- cgit v1.2.1 From 2047e407eca37dd1597de2333d11bfb3ed23c755 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 13 Jun 2008 16:24:19 +0000 Subject: update news for release --- docs/index.txt | 11 ++++++----- docs/news.txt | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 0e05b24..0d3add9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -2,19 +2,20 @@ Paste Deployment ================ :author: Ian Bicking -:revision: $Rev$ -:date: $LastChangedDate$ -Contents: +.. contents:: + +Documents: .. toctree:: :maxdepth: 1 news + modules/loadwsgi + modules/config + modules/converters license -.. contents:: - .. comment: The names used in sections should be more concrete, and it should be clearer that they are just arbitrary names. diff --git a/docs/news.txt b/docs/news.txt index 2056461..59ecc72 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.3.2 +----- * Added ``paste.deploy.converters.asint()``. * fixed use sections overwriting the config's __file__ value with the -- cgit v1.2.1 From 5cc6e456edcdc1a601346949114a3da2dce8260c Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 13 Jun 2008 16:24:41 +0000 Subject: Added template for site links --- docs/_static/paste.css | 15 +++++++++++++++ docs/_templates/layout.html | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 docs/_static/paste.css create mode 100644 docs/_templates/layout.html diff --git a/docs/_static/paste.css b/docs/_static/paste.css new file mode 100644 index 0000000..6705e5d --- /dev/null +++ b/docs/_static/paste.css @@ -0,0 +1,15 @@ +a.invisible-link { + color: #fff; + text-decoration: none; +} + +a.invisible-link:visited { + color: #fff; + text-decoration: none; +} + +a.invisible:link { + color: #fff; + text-decoration: none; +} + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..a6d2a97 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,20 @@ +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block sidebartoc %} +

Python Paste

+ + + +{{ super() }} +{% endblock %} -- cgit v1.2.1 From cfc44fc7f6e22ec279fa7519459858214960b7fa Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 13 Jun 2008 16:25:05 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a5ae70..e3783da 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.3.2' +version = '1.3.3' setup( name="PasteDeploy", -- cgit v1.2.1 From 4237ee81fef4a12dc95139c37f7097c98051f48f Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 20 Jun 2008 17:14:07 +0000 Subject: Translate X-Forwarded-Scheme to wsgi.url_scheme --- docs/news.txt | 7 +++++++ paste/deploy/config.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/news.txt b/docs/news.txt index 59ecc72..76be04d 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,13 @@ Paste Deployment News ===================== +svn trunk +--------- + +* In :class:`paste.deploy.config.PrefixMiddleware` the header + ``X-Forwarded-Scheme`` is now translated to the key + ``environ['wsgi.url_scheme']``. + 1.3.2 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 6981756..35de866 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -228,7 +228,8 @@ class PrefixMiddleware(object): 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. + lost in the proxying. Also ``X-Forwarded-Host`` and + ``X-Forwarded-Scheme`` 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 @@ -261,6 +262,8 @@ class PrefixMiddleware(object): environ['SERVER_NAME'] = environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') if 'HTTP_X_FORWARDED_HOST' in environ: environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST') + if 'HTTP_X_FORWARDED_SCHEME' in environ: + environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_SCHEME') if self.force_port is not None: host = environ.get('HTTP_HOST', '').split(':', 1)[0] if self.force_port: -- cgit v1.2.1 From 36b240b0d48094e0af0d79df87be98f01f0a93c2 Mon Sep 17 00:00:00 2001 From: ianb Date: Fri, 20 Jun 2008 17:15:42 +0000 Subject: Also translate X-Forwarded-Proto to wsgi.url_scheme --- docs/news.txt | 6 +++--- paste/deploy/config.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 76be04d..1f95a57 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -4,9 +4,9 @@ Paste Deployment News svn trunk --------- -* In :class:`paste.deploy.config.PrefixMiddleware` the header - ``X-Forwarded-Scheme`` is now translated to the key - ``environ['wsgi.url_scheme']``. +* In :class:`paste.deploy.config.PrefixMiddleware` the headers + ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated + to the key ``environ['wsgi.url_scheme']``. 1.3.2 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 35de866..7f4c6db 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -228,8 +228,8 @@ class PrefixMiddleware(object): 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`` and - ``X-Forwarded-Scheme`` are translated. + 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 @@ -264,6 +264,8 @@ class PrefixMiddleware(object): environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST') 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: -- cgit v1.2.1 From 4df7b6b918630a8fff312dacfba01a370e6e309b Mon Sep 17 00:00:00 2001 From: ianb Date: Thu, 3 Jul 2008 21:04:59 +0000 Subject: Add X-Forwarded-For translation to REMOTE_ADDR --- docs/news.txt | 3 ++- paste/deploy/config.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/news.txt b/docs/news.txt index 1f95a57..a72ed76 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -6,7 +6,8 @@ svn trunk * In :class:`paste.deploy.config.PrefixMiddleware` the headers ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated - to the key ``environ['wsgi.url_scheme']``. + to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` + is translated to ``environ['REMOTE_ADDR']`` 1.3.2 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 7f4c6db..ddf0ed6 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -262,6 +262,8 @@ class PrefixMiddleware(object): environ['SERVER_NAME'] = environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_SERVER') if 'HTTP_X_FORWARDED_HOST' in environ: environ['HTTP_HOST'] = environ.pop('HTTP_X_FORWARDED_HOST') + if 'HTTP_X_FORWARDED_FOR' in environ: + environ['REMOTE_ADDR'] = environ.pop('HTTP_X_FORWARDED_FOR') if 'HTTP_X_FORWARDED_SCHEME' in environ: environ['wsgi.url_scheme'] = environ.pop('HTTP_X_FORWARDED_SCHEME') elif 'HTTP_X_FORWARDED_PROTO' in environ: -- cgit v1.2.1 From 5df8cc72eda603f93d67ef3d19f94e0db1111dba Mon Sep 17 00:00:00 2001 From: ianb Date: Mon, 26 Jan 2009 18:03:50 +0000 Subject: used the wrong license trove --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3783da..b6e8ee3 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ For the latest changes see the `news file classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", + "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", -- cgit v1.2.1 From 96255f171fdf80dea85f4846094a64c132d56e60 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Feb 2009 21:58:59 +0000 Subject: Handle cases where X-Forwarded-Host has multiple values --- docs/news.txt | 3 +++ paste/deploy/config.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index a72ed76..22f1277 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -9,6 +9,9 @@ svn trunk to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` is translated to ``environ['REMOTE_ADDR']`` +* Also in PrefixMiddleware, if X-Forwarded-Host has multiple + (comma-separated) values, use only the first value. + 1.3.2 ----- diff --git a/paste/deploy/config.py b/paste/deploy/config.py index ddf0ed6..d690ff3 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -259,9 +259,9 @@ class PrefixMiddleware(object): 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') + 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') + 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') if 'HTTP_X_FORWARDED_SCHEME' in environ: -- cgit v1.2.1 From 37a49dc050c22d7565b760c97e973488c36b9fcb Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Feb 2009 22:28:37 +0000 Subject: update news for release --- docs/conf.py | 2 +- docs/news.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a2c1de4..e95109f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ copyright = '2008, Ian Bicking' # The short X.Y version. version = '1.3' # The full version, including alpha/beta/rc tags. -release = '1.3.2' +release = '1.3.3' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/news.txt b/docs/news.txt index 22f1277..923e7b3 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.3.3 +----- * In :class:`paste.deploy.config.PrefixMiddleware` the headers ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated -- cgit v1.2.1 From 9c16ee0bf2ac1934c8a3134c603f5c5658b49962 Mon Sep 17 00:00:00 2001 From: ianb Date: Wed, 11 Feb 2009 22:29:36 +0000 Subject: Auto-update of version strings --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b6e8ee3..d561a1a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '1.3.3' +version = '1.3.4' setup( name="PasteDeploy", -- cgit v1.2.1 From beb10ff91899d3974bcfcbd862466f1e76393abf Mon Sep 17 00:00:00 2001 From: pjenvey Date: Thu, 7 May 2009 06:34:01 +0000 Subject: convert to nose 0.11 --- setup.py | 2 ++ tests/__init__.py | 14 ++++++++++++++ tests/conftest.py | 14 -------------- tests/test_config_middleware.py | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py diff --git a/setup.py b/setup.py index d561a1a..e2516f6 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,8 @@ For the latest changes see the `news file packages=find_packages(exclude='tests'), include_package_data=True, zip_safe=False, + test_suite='nose.collector', + tests_require=['nose>=0.11'], extras_require={ 'Config': [], 'Paste': ['Paste'], diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0ba8f8a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,14 @@ +import os +import sys + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +sys.path.insert(0, base) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +pkg_resources.require('PasteDeploy') + diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 0ba8f8a..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -import os -import sys - -here = os.path.dirname(__file__) -base = os.path.dirname(here) -sys.path.insert(0, base) - -# We can only import this after we adjust the paths -import pkg_resources - -# Make absolutely sure we're testing *this* package, not -# some other installed package -pkg_resources.require('PasteDeploy') - diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py index 0443dca..868e75f 100644 --- a/tests/test_config_middleware.py +++ b/tests/test_config_middleware.py @@ -1,6 +1,6 @@ +from nose.tools import assert_raises from paste.deploy.config import ConfigMiddleware from paste.fixture import TestApp -from py.test import raises class Bug(Exception): pass @@ -14,5 +14,5 @@ def app_with_exception(environ, start_response): def test_error(): wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) test_app = TestApp(wrapped) - raises(Bug, "test_app.get('/')") + assert_raises(Bug, test_app.get, '/') -- cgit v1.2.1 From 2e8c2fddfd649fb67e732e275b343fcb1c24b118 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Tue, 14 Jul 2009 21:32:44 +0000 Subject: use abspath, fixes path handling on Jython on Windows thanks crankycoder --- paste/deploy/loadwsgi.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index bffa500..86aa604 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -2,7 +2,6 @@ # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php import os import re -import sys import urllib from ConfigParser import ConfigParser import pkg_resources @@ -249,17 +248,10 @@ def loadcontext(object_type, uri, name=None, relative_to=None, def _loadconfig(object_type, uri, path, name, relative_to, global_conf): + isabs = os.path.isabs(path) # De-Windowsify the paths: path = path.replace('\\', '/') - absolute_path = True - if sys.platform == 'win32': - _absolute_re = re.compile(r'^[a-zA-Z]:') - if not _absolute_re.search(path): - absolute_path = False - else: - if not path.startswith('/'): - absolute_path = False - if not absolute_path: + if not isabs: if not relative_to: raise ValueError( "Cannot resolve relative uri %r; no context keyword " -- cgit v1.2.1 From 5dbb3e2363837383296b6a7404543e6da2932e42 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Tue, 14 Jul 2009 21:40:27 +0000 Subject: changelog r8024 --- docs/news.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index 923e7b3..96e1793 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,11 @@ Paste Deployment News ===================== +svn trunk +--------- + +* Fix loadconfig path handling on Jython on Windows. + 1.3.3 ----- -- cgit v1.2.1 From 1587cb01d550796899409e44a6c909a05a6d7fc5 Mon Sep 17 00:00:00 2001 From: pjenvey Date: Mon, 5 Oct 2009 00:34:56 +0000 Subject: correct the error message's kwarg name --- paste/deploy/loadwsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 86aa604..97c8dff 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -254,7 +254,7 @@ def _loadconfig(object_type, uri, path, name, relative_to, if not isabs: if not relative_to: raise ValueError( - "Cannot resolve relative uri %r; no context keyword " + "Cannot resolve relative uri %r; no relative_to keyword " "argument given" % uri) relative_to = relative_to.replace('\\', '/') if relative_to.endswith('/'): -- cgit v1.2.1 -- cgit v1.2.1 From 33283636f224c4f1c8ded247efca929f9a3ea55c Mon Sep 17 00:00:00 2001 From: Ian Bicking Date: Wed, 28 Oct 2009 01:41:35 -0500 Subject: moved repo --- docs/index.txt | 4 ++-- setup.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index 0d3add9..c23e270 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -74,8 +74,8 @@ You can install Paste Deployment by installing `easy_install If you want to track development, do:: - $ svn co http://svn.pythonpaste.org/Paste/Deploy/trunk Paste-Deploy - $ cd Paste-Deploy + $ hg clone http://bitbucket.org/ianb/pastedeploy + $ cd pastedeploy $ sudo python setup.py develop This will install the package globally, but will load the files in the diff --git a/setup.py b/setup.py index e2516f6..3ea49fe 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,9 @@ URIs; these URIs can refer to Python Eggs for INI-style configuration files. `Paste Script `_ provides commands to serve applications based on this configuration file. -The latest version is available in a `Subversion repository -`_. +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). For the latest changes see the `news file `_. -- cgit v1.2.1 From 0209b2e9a3af0d6196a5b3749a352aaceee974b0 Mon Sep 17 00:00:00 2001 From: Ian Bicking Date: Thu, 2 Sep 2010 03:47:26 -0500 Subject: raise IOError instead of OSError on file-not-found (http://trac.pythonpaste.org/pythonpaste/ticket/428) --- .hgignore | 4 ++++ paste/deploy/loadwsgi.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .hgignore diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..5139889 --- /dev/null +++ b/.hgignore @@ -0,0 +1,4 @@ +syntax: glob +*.egg-info/ +dist/ +build/ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 97c8dff..bae77ae 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -315,7 +315,7 @@ class _Loader(object): """ if name is None: return False - return self._absolute_re.search(name) + return self._absolute_re.search(name) class ConfigLoader(_Loader): @@ -328,11 +328,11 @@ class ConfigLoader(_Loader): # we have to add an extra check: if not os.path.exists(filename): if filename.strip() != filename: - raise OSError( + raise IOError( "File %r not found; trailing whitespace: " "did you try to use a # on the same line as a filename? " "(comments must be on their own line)" % filename) - raise OSError( + raise IOError( "File %r not found" % filename) self.parser.read(filename) self.parser._defaults.setdefault( -- cgit v1.2.1 From d4d446b6ad6a05229a84c5fe249ef02d55cb60c0 Mon Sep 17 00:00:00 2001 From: Ian Bicking Date: Tue, 14 Sep 2010 11:06:33 -0500 Subject: update news for release --- docs/conf.py | 4 ++-- docs/news.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e95109f..1b256af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,7 @@ master_doc = 'index' # General substitutions. project = 'Paste Deploy' -copyright = '2008, Ian Bicking' +copyright = '2010, Ian Bicking and contributors' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. @@ -42,7 +42,7 @@ copyright = '2008, Ian Bicking' # The short X.Y version. version = '1.3' # The full version, including alpha/beta/rc tags. -release = '1.3.3' +release = '1.3.4' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/news.txt b/docs/news.txt index 96e1793..119e110 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -svn trunk ---------- +1.3.4 +----- * Fix loadconfig path handling on Jython on Windows. -- cgit v1.2.1 -- cgit v1.2.1 From b76c55f8f50506a33084b5f23c4f0d76eb163eb3 Mon Sep 17 00:00:00 2001 From: Ian Bicking Date: Thu, 16 Sep 2010 12:31:17 -0400 Subject: Remove setup.cfg, which wasn't being (usefully) used, and add finddata to remove need for svn/include_package_data --- .hgignore | 1 + setup.cfg | 32 ------------------ setup.py | 9 ++++- tests/finddata.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 33 deletions(-) delete mode 100644 setup.cfg create mode 100644 tests/finddata.py diff --git a/.hgignore b/.hgignore index 5139889..5345d54 100644 --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,4 @@ syntax: glob *.egg-info/ dist/ build/ +docs/_build/ diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a8f5b23..0000000 --- a/setup.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[egg_info] -tag_build = dev -tag_svn_revision = true - -[global] -command_packages = buildutils.pudge_command, buildutils.publish_command - -[pudge] -highlighter = pygments -title = Paste Deploy -dest = docs/html -docs = docs/index.txt docs/news.txt -settings = no_about=true - link1=/deploy/ paste.deploy - link2=/script/ paster script - link3=/download/ Download - extra_credits=Hosting courtesy of Tummy.com -modules = paste.deploy -theme = pythonpaste.org -mailing_list_url = http://pythonpaste.org/community/mailing-list.html -organization = Python Paste -organization_url = http://pythonpaste.org/ -trac_url = http://pythonpaste.org/trac/ - -[publish] -doc-dest = scp://ianb@webwareforpython.org/home/paste/htdocs/deploy -make-dirs = 1 -doc-dir = docs/html - -[aliases] -distribute = register sdist bdist_egg upload pudge publish - diff --git a/setup.py b/setup.py index 3ea49fe..1959175 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,12 @@ from setuptools import setup, find_packages version = '1.3.4' +import os + +here = os.path.dirname(os.path.abspath(__file__)) +finddata_py = os.path.join(here, 'tests', 'finddata.py') +execfile(finddata_py) + setup( name="PasteDeploy", version=version, @@ -38,7 +44,8 @@ For the latest changes see the `news file license='MIT', namespace_packages=['paste'], packages=find_packages(exclude='tests'), - include_package_data=True, + package_data=find_package_data( + exclude_directories=standard_exclude_directories + ('tests',)), zip_safe=False, test_suite='nose.collector', tests_require=['nose>=0.11'], diff --git a/tests/finddata.py b/tests/finddata.py new file mode 100644 index 0000000..4d4fa90 --- /dev/null +++ b/tests/finddata.py @@ -0,0 +1,99 @@ +# (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 +# Note: you may want to copy this into your setup.py file verbatim, as +# you can't import this from another package, when you don't know if +# that package is installed yet. + +import os +import sys +from fnmatch import fnmatchcase +from distutils.util import convert_path + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') +standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', + './dist', 'EGG-INFO', '*.egg-info') + +def find_package_data( + where='.', package='', + exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, + show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + + The dictionary looks like:: + + {'package': [files]} + + Where ``files`` is a list of all the files in that package that + don't match anything in ``exclude``. + + If ``only_in_packages`` is true, then top-level directories that + are not packages won't be included (but directories under packages + will). + + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + + If ``show_ignored`` is true, then all the files that aren't + included in package data are shown on stderr (for debugging + purposes). + + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + + out = {} + stack = [(convert_path(where), '', package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "Directory %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + if (os.path.isfile(os.path.join(fn, '__init__.py')) + and not prefix): + if not package: + new_package = name + else: + new_package = package + '.' + name + stack.append((fn, '', new_package, False)) + else: + stack.append((fn, prefix + name + '/', package, only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "File %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix+name) + return out + +if __name__ == '__main__': + import pprint + pprint.pprint( + find_package_data(show_ignored=True)) -- cgit v1.2.1 -- cgit v1.2.1 From cdd5b17ca1015a56f81311cbeb3d333e57a1ab7f Mon Sep 17 00:00:00 2001 From: Ian Bicking Date: Thu, 28 Oct 2010 11:03:27 -0700 Subject: remove prints from finddata --- docs/news.txt | 5 +++++ tests/finddata.py | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/news.txt b/docs/news.txt index 119e110..a48214a 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,11 @@ Paste Deployment News ===================== +hg tip +------ + +* Was printing extraneous data when calling setup.py + 1.3.4 ----- diff --git a/tests/finddata.py b/tests/finddata.py index 4d4fa90..16057dc 100644 --- a/tests/finddata.py +++ b/tests/finddata.py @@ -93,7 +93,3 @@ def find_package_data( out.setdefault(package, []).append(prefix+name) return out -if __name__ == '__main__': - import pprint - pprint.pprint( - find_package_data(show_ignored=True)) -- cgit v1.2.1 From bce90c1b63ab7bc33de374a6c6a72fffe2af91b2 Mon Sep 17 00:00:00 2001 From: Jason Stitt Date: Thu, 11 Nov 2010 08:49:55 -0600 Subject: Added a new loader using the call: protocol that uses an object path and not an egg --- paste/deploy/loadwsgi.py | 54 ++++++++++++++++++++++++++++++++++++++ tests/sample_configs/test_func.ini | 13 +++++++++ tests/test_func_loader.py | 32 ++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 tests/sample_configs/test_func.ini create mode 100644 tests/test_func_loader.py diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index bae77ae..6b373d0 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -278,6 +278,14 @@ def _loadegg(object_type, uri, spec, name, relative_to, _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 ############################################################ @@ -428,6 +436,20 @@ class ConfigLoader(_Loader): 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' % context_protocol + return context def _context_from_explicit(self, object_type, local_conf, global_conf, @@ -596,6 +618,38 @@ class EggLoader(_Loader): % (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 + try: + self.module_path, self.func_name = self.spec.split(':') + except ValueError: + raise LookupError("Configuration not in format module:function") + self.module_name = self.module_path.split('.', 1)[-1] + + def get_context(self, object_type, name=None, global_conf=None): + module = __import__(self.module_path, {}, {}, [self.module_name], 0) + obj = module + for part in self.func_name.split('.'): + obj = getattr(obj, part) + return LoaderContext( + obj, + object_type, + None, # determine protocol from section type + global_conf or {}, + {}, + self, + ) + + class LoaderContext(object): def __init__(self, obj, object_type, protocol, diff --git a/tests/sample_configs/test_func.ini b/tests/sample_configs/test_func.ini new file mode 100644 index 0000000..a0d28c4 --- /dev/null +++ b/tests/sample_configs/test_func.ini @@ -0,0 +1,13 @@ +[application:main] +use = call:fakeapp.apps:make_basic_app + +[application:other] +use = call:fakeapp.apps:make_basic_app2 + +[composit:remote_addr] +use = call:fakeapp.apps:make_remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 \ No newline at end of file diff --git a/tests/test_func_loader.py b/tests/test_func_loader.py new file mode 100644 index 0000000..a04632d --- /dev/null +++ b/tests/test_func_loader.py @@ -0,0 +1,32 @@ +from paste.deploy import loadapp, loadfilter, appconfig +from fixture import * +import fakeapp.apps + +here = os.path.abspath(os.path.dirname(__file__)) + +def test_main(): + app = loadapp('config:sample_configs/test_func.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/test_func.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/test_func.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/test_func.ini#ignored', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + +def test_other(): + app = loadapp('config:sample_configs/test_func.ini#other', + relative_to=here) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:sample_configs/test_func.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 -- cgit v1.2.1 From e639ba94792f8de7d9f743fceb57b5609d3c48ef Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 02:26:43 +0300 Subject: The threadinglocal module is no longer necessary, so replaced the util package with a util module containing the code from fixtypeerror.py --- .hgignore | 5 + MANIFEST.in | 3 + docs/_static/paste.css | 15 + docs/_templates/layout.html | 20 + docs/conf.py | 132 +++++ docs/index.txt | 649 +++++++++++++++++++++ docs/license.txt | 20 + docs/modules/config.txt | 13 + docs/modules/converters.txt | 11 + docs/modules/loadwsgi.txt | 13 + docs/news.txt | 143 +++++ paste/__init__.py | 18 + paste/deploy/__init__.py | 10 + paste/deploy/config.py | 297 ++++++++++ paste/deploy/converters.py | 33 ++ paste/deploy/epdesc.py | 37 ++ paste/deploy/interfaces.py | 202 +++++++ paste/deploy/loadwsgi.py | 631 ++++++++++++++++++++ paste/deploy/paster_templates.py | 33 ++ .../paste_deploy/+package+/sampleapp.py_tmpl | 23 + .../paste_deploy/+package+/wsgiapp.py_tmpl | 23 + .../paste_deploy/docs/devel_config.ini_tmpl | 22 + paste/deploy/util.py | 58 ++ regen-docs | 9 + setup.py | 73 +++ tests/__init__.py | 14 + .../FakeApp.egg/FakeApp.egg-info/PKG-INFO | 10 + .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 22 + .../FakeApp.egg/FakeApp.egg-info/top_level.txt | 1 + .../fake_packages/FakeApp.egg/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 69 +++ .../FakeApp.egg/fakeapp/configapps.py | 14 + tests/fake_packages/FakeApp.egg/setup.py | 23 + tests/finddata.py | 95 +++ tests/fixture.py | 21 + tests/sample_configs/basic_app.ini | 14 + tests/sample_configs/executable.ini | 10 + tests/sample_configs/test_config.ini | 36 ++ tests/sample_configs/test_config_included.ini | 10 + tests/sample_configs/test_filter.ini | 22 + tests/sample_configs/test_filter_with.ini | 12 + tests/test_basic_app.py | 34 ++ tests/test_config.py | 116 ++++ tests/test_config_middleware.py | 18 + tests/test_filter.py | 46 ++ tests/test_load_package.py | 10 + 46 files changed, 3091 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 docs/_static/paste.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/conf.py create mode 100644 docs/index.txt create mode 100644 docs/license.txt create mode 100644 docs/modules/config.txt create mode 100644 docs/modules/converters.txt create mode 100644 docs/modules/loadwsgi.txt create mode 100644 docs/news.txt create mode 100644 paste/__init__.py create mode 100644 paste/deploy/__init__.py create mode 100644 paste/deploy/config.py create mode 100644 paste/deploy/converters.py create mode 100644 paste/deploy/epdesc.py create mode 100644 paste/deploy/interfaces.py create mode 100644 paste/deploy/loadwsgi.py create mode 100644 paste/deploy/paster_templates.py create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl create mode 100644 paste/deploy/util.py create mode 100755 regen-docs create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/apps.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/configapps.py create mode 100644 tests/fake_packages/FakeApp.egg/setup.py create mode 100644 tests/finddata.py create mode 100644 tests/fixture.py create mode 100644 tests/sample_configs/basic_app.ini create mode 100755 tests/sample_configs/executable.ini create mode 100644 tests/sample_configs/test_config.ini create mode 100644 tests/sample_configs/test_config_included.ini create mode 100644 tests/sample_configs/test_filter.ini create mode 100644 tests/sample_configs/test_filter_with.ini create mode 100644 tests/test_basic_app.py create mode 100644 tests/test_config.py create mode 100644 tests/test_config_middleware.py create mode 100644 tests/test_filter.py create mode 100644 tests/test_load_package.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..5345d54 --- /dev/null +++ b/.hgignore @@ -0,0 +1,5 @@ +syntax: glob +*.egg-info/ +dist/ +build/ +docs/_build/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..e5deda2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include docs/*.html +include docs/*.txt +exclude docs/rebuild diff --git a/docs/_static/paste.css b/docs/_static/paste.css new file mode 100644 index 0000000..6705e5d --- /dev/null +++ b/docs/_static/paste.css @@ -0,0 +1,15 @@ +a.invisible-link { + color: #fff; + text-decoration: none; +} + +a.invisible-link:visited { + color: #fff; + text-decoration: none; +} + +a.invisible:link { + color: #fff; + text-decoration: none; +} + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..a6d2a97 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,20 @@ +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block sidebartoc %} +

Python Paste

+ + + +{{ super() }} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..1b256af --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Paste Deploy' +copyright = '2010, Ian Bicking and contributors' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '1.3' +# The full version, including alpha/beta/rc tags. +release = '1.3.4' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = ['include/contact.txt', 'include/reference_header.txt'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PasteDeploydoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..c23e270 --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,649 @@ +Paste Deployment +================ + +:author: Ian Bicking + +.. contents:: + +Documents: + +.. toctree:: + :maxdepth: 1 + + news + modules/loadwsgi + modules/config + modules/converters + license + +.. comment: + The names used in sections should be more concrete, and it should + be clearer that they are just arbitrary names. + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +To see updates that have been made to Paste Deploy see the `news file +`_. + +Paste Deploy is released under the `MIT license +`_. + +Status +------ + +Paste Deploy has passed version 1.0. Paste Script is an actively +maintained project. As of 1.0, we'll make a strong effort to maintain +backward compatibility (this actually started happening long before +1.0, but now it is explicit). This will include deprecation warnings +when necessary. Major changes will take place under new functions or +with new entry points. + +Note that the most key aspect of Paste Deploy is the entry points it +defines (such as ``paste.app_factory``). Paste Deploy is not the only +consumer of these entry points, and many extensions can best take +place by utilizing the entry points instead of using Paste Deploy +directly. The entry points will not change; if changes are necessary, +new entry points will be defined. + +Installation +------------ + +First install `setuptools +`_. + +You can install Paste Deployment by installing `easy_install +`_ and running:: + + $ sudo easy_install.py PasteDeploy + +If you want to track development, do:: + + $ hg clone http://bitbucket.org/ianb/pastedeploy + $ cd pastedeploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. You can also simply install ``PasteDeploy==dev``. + +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + +A complimentary package is `Paste Script `_. To install +that use ``easy_install PasteScript`` (or ``easy_install +PasteScript==dev``). + +From the User Perspective +------------------------- + +In the following sections, the Python API for using Paste Deploy is +given. This isn't what users will be using (but it is useful for +Python developers and useful for setting up tests fixtures). + +The primary interaction with Paste Deploy is through its configuration +files. The primary thing you want to do with a configuration file is +serve it. To learn about serving configuration files, see `the +``paster serve`` command +`_. + +The Config File +~~~~~~~~~~~~~~~ + +A config file has different sections. The only sections Paste Deploy +cares about have prefixes, like ``app:main`` or ``filter:errors`` -- +the part after the ``:`` is the "name" of the section, and the part +before gives the "type". Other sections are ignored. + +The format is a simple `INI format +`_: ``name = value``. You can +extend the value by indenting subsequent lines. ``#`` is a comment. + +Typically you have one or two sections, named "main": an application +section (``[app:main]``) and a server section (``[server:main]``). +``[composite:...]`` signifies something that dispatches to multiple +applications (example below). + +Here's a typical configuration file that also shows off mounting +multiple applications using `paste.urlmap +`_:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +I'll explain each section in detail now:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + +That this is a ``composite`` section means it dispatches the request +to other applications. ``use = egg:Paste#urlmap`` means to use the +composite application named ``urlmap`` from the ``Paste`` package. +``urlmap`` is a particularly common composite application -- it uses a +path prefix to map your request to another application. These are +the applications like "home", "blog" and "config:cms.ini". The last +one just refers to another file ``cms.ini`` in the same directory. + +Next up:: + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + +``egg:Paste#static`` is another simple application, in this case it +just serves up non-dynamic files. It takes one bit of configuration: +``document_root``. You can use variable substitution, which will pull +variables from the section ``[DEFAULT]`` (case sensitive!) with +markers like ``%(var_name)s``. The special variable ``%(here)s`` is +the directory containing the configuration file; you should use that +in lieu of relative filenames (which depend on the current directory, +which can change depending how the server is run). + +Lastly:: + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +The ``[filter-app:blog]`` section means that you want an application +with a filter applied. The application being filtered is indicated +with ``next`` (which refers to the next section). The +``egg:Authentication#auth`` filter doesn't actually exist, but one +could imagine it logs people in and checks permissions. + +That last section is just a reference to an application that you +probably installed with ``easy_install BlogApp``, and one bit of +configuration you passed to it (``database``). + +So, that's most of the features you'll use. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composite Applications +~~~~~~~~~~~~~~~~~~~~~~ + +"Composite" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composite:main] + use = egg:Paste#urlmap + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composite application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +Filter Composition +~~~~~~~~~~~~~~~~~~ + +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + # and you could have another filter-with here, and so on... + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + + [filter:filter1] + ... + +Getting Configuration +~~~~~~~~~~~~~~~~~~~~~ + +If you want to get the configuration without creating the application, +you can use the ``appconfig(uri)`` function, which is just like the +``loadapp()`` function except it returns the configuration that would +be used, as a dictionary. Both global and local configuration is +combined into a single dictionary, but you can look at just one or the +other with the attributes ``.local_conf`` and ``.global_conf``. + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ that adds +metadata to a normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory``, +``paste.composite_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composite_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composites are just slightly more complex:: + + def composite_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composite factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composite:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.filter_app_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + +``paste.server_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +An implementation of ``Server`` is left to the user. + +``paste.server_runner`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + +Outstanding Issues +------------------ + +* Should add a ``python:`` scheme for loading objects out of modules + directly. It has to include the protocol somehow...? + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory`` and ``paste.composite_factory`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + diff --git a/docs/license.txt b/docs/license.txt new file mode 100644 index 0000000..c810dec --- /dev/null +++ b/docs/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2006-2007 Ian Bicking and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/modules/config.txt b/docs/modules/config.txt new file mode 100644 index 0000000..9d8f894 --- /dev/null +++ b/docs/modules/config.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.config` -- Configuration and Environment middleware +====================================================================== + +.. automodule:: paste.deploy.config + +Module Contents +--------------- + +.. autoclass:: DispatchingConfig +.. autoclass:: ConfigMiddleware +.. autoclass:: PrefixMiddleware + +.. comment: FIXME: do something about CONFIG (manual docs?) diff --git a/docs/modules/converters.txt b/docs/modules/converters.txt new file mode 100644 index 0000000..4948104 --- /dev/null +++ b/docs/modules/converters.txt @@ -0,0 +1,11 @@ +:mod:`paste.deploy.converters` -- Conversion helpers for String Configuration +============================================================================= + +.. automodule:: paste.deploy.converters + +Module Contents +--------------- + +.. autofunction:: asbool +.. autofunction:: asint +.. autofunction:: aslist diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt new file mode 100644 index 0000000..fc3dc4c --- /dev/null +++ b/docs/modules/loadwsgi.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.loadwsgi` -- Load WSGI applications from config files +======================================================================== + +.. automodule:: paste.deploy.loadwsgi + +Module Contents +--------------- + +.. autofunction:: loadapp +.. autofunction:: loadserver +.. autofunction:: loadfilter +.. autofunction;: appconfig + diff --git a/docs/news.txt b/docs/news.txt new file mode 100644 index 0000000..a48214a --- /dev/null +++ b/docs/news.txt @@ -0,0 +1,143 @@ +Paste Deployment News +===================== + +hg tip +------ + +* Was printing extraneous data when calling setup.py + +1.3.4 +----- + +* Fix loadconfig path handling on Jython on Windows. + +1.3.3 +----- + +* In :class:`paste.deploy.config.PrefixMiddleware` the headers + ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated + to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` + is translated to ``environ['REMOTE_ADDR']`` + +* Also in PrefixMiddleware, if X-Forwarded-Host has multiple + (comma-separated) values, use only the first value. + +1.3.2 +----- + +* Added ``paste.deploy.converters.asint()``. +* fixed use sections overwriting the config's __file__ value with the + use'd filename. +* ``paste.deploy.loadwsgi`` now supports variable expansion in the + DEFAULT section of config files (unlike plain ConfigParser). + +1.3.1 +----- + +* Fix ``appconfig`` config loading when using a config file with + ``filter-with`` in it (previously you'd get TypeError: iteration + over non-sequence) + +1.3 +--- + +* Added ``scheme`` option to ``PrefixMiddleware``, so you can force a + scheme (E.g., when proxying an HTTPS connection over HTTP). + +* Pop proper values into ``environ['paste.config']`` in + ``ConfigMiddleware``. + +1.1 +--- + +* Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other + loaders) will show up as though they were in ``[DEFAULT]``, so they + can be used in variable interpolation. Note: this won't overwrite + any existing values in ``[DEFAULT]``. + +* Added ``force_port`` option to + ``paste.deploy.config.PrefixMiddleware``. Also the ``prefix`` + argument is stripped of any trailing ``/``, which can't be valid in + that position. + +1.0 +--- + +* Added some documentation for the different kinds of entry points + Paste Deploy uses. + +* Added a feature to ``PrefixMiddleware`` that translates the + ``X-Forwarded-Server`` header to ``Host``. + +0.9.6 +----- + +* Added ``PrefixMiddleware`` which compensates for cases where the + wsgi app is behind a proxy of some sort that isn't moving the prefix + into the SCRIPT_NAME in advance. + +* Changed _loadconfig() so that it works with Windows absolute paths. + +* Make the error messages prettier when you call a function and fail + to give an argument, like a required function argument. + +0.5 +--- + +* Made the ``paste_deploy`` template (used with ``paster create + --template=paste_deploy``) more useful, with an example application + and entry point. + +0.4 +--- + +* Allow filters to have ``filter-with`` values, just like + applications. + +* Renamed ``composit`` to ``composite`` (old names still work, but + aren't documented). + +* Added ``appconfig()`` to load along with ``loadapp()``, but return + the configuration without invoking the application. + +0.3 +--- + +* Allow variable setting like:: + + get local_var = global_var_name + + To bring in global variables to the local scope. + +* Allow interpolation in files, like ``%(here)s``. Anything in the + ``[DEFAULTS]`` section will be available to substitute into a value, + as will variables in the same section. Also, the special value + ``here`` will be the directory the configuration file is located in. + +0.2 +--- + +Released 26 August 2004 + +* Added a ``filter-with`` setting to applications. + +* Removed the ``1`` from all the protocol names (e.g., + ``paste.app_factory1`` is not ``paste.app_factory``). + +* Added ``filter-app:`` and ``pipeline:`` sections. `Docs + `__. + +* Added ``paste.filter_app_factory1`` (`doc + `__) and + ``paste.server_runner1`` (`doc + `__) protocols. + +* Added ``paste.deploy.converters`` module for handling the + string values that are common with this system. + +0.1 +--- + +Released 22 August 2004 + +Initial version released. It's all new. diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..cdb6121 --- /dev/null +++ b/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/paste/deploy/__init__.py b/paste/deploy/__init__.py new file mode 100644 index 0000000..f89218f --- /dev/null +++ b/paste/deploy/__init__.py @@ -0,0 +1,10 @@ +# (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 loadwsgi import * +try: + from config import CONFIG +except ImportError: + # @@: Or should we require Paste? Or should we put threadlocal + # into this package too? + pass + diff --git a/paste/deploy/config.py b/paste/deploy/config.py new file mode 100644 index 0000000..562a61f --- /dev/null +++ b/paste/deploy/config.py @@ -0,0 +1,297 @@ +# (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(object): + + """ + 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 local_dict().has_key(self._local_key): + 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 self.has_key(key) + + 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(object): + + """ + 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(object): + """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') + 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 = '%s:%s' % (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 new file mode 100644 index 0000000..98edd8f --- /dev/null +++ b/paste/deploy/converters.py @@ -0,0 +1,33 @@ +# (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 +def asbool(obj): + if isinstance(obj, (str, unicode)): + obj = obj.strip().lower() + if obj in ['true', 'yes', 'on', 'y', 't', '1']: + return True + elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + 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), e: + raise ValueError( + "Bad integer value: %r" % obj) + +def aslist(obj, sep=None, strip=True): + if isinstance(obj, (str, unicode)): + 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/epdesc.py b/paste/deploy/epdesc.py new file mode 100644 index 0000000..7c7deae --- /dev/null +++ b/paste/deploy/epdesc.py @@ -0,0 +1,37 @@ +class AppFactoryDescription(object): + description = """ + This gives a factory/function that can create WSGI apps + """ + +class CompositeFactoryDescription(object): + description = """ + This gives a factory/function that can create WSGI apps, and has + access to the application creator so that it can in turn fetch + apps based on name. + """ + +class FilterAppFactoryDescription(object): + description = """ + This gives a factory/function that wraps a WSGI application to + create another WSGI application (typically applying middleware) + """ + +class FilterFactoryDescription(object): + description = """ + This gives a factory/function that return a function that can wrap + a WSGI application and returns another WSGI application. + paste.filter_app_factory is the same thing with less layers. + """ + +class ServerFactoryDescription(object): + description = """ + This gives a factory/function that creates a server, that can be + called with a WSGI application to run indefinitely. + paste.server_runner is the same thing with less layers. + """ + +class ServerRunnerDescription(object): + description = """ + This gives a factory/function that, given a WSGI application and + configuration, will serve the application indefinitely. + """ diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py new file mode 100644 index 0000000..29163a5 --- /dev/null +++ b/paste/deploy/interfaces.py @@ -0,0 +1,202 @@ +# (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 +############################################################ +## Functions +############################################################ + +def loadapp(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadapp``. + + Load the specified URI as a WSGI application (returning IWSGIApp). + The ``name`` can be in the URI (typically as ``#name``). If it is + and ``name`` is given, the keyword argument overrides the URI. + + If the URI contains a relative filename, then ``relative_to`` is + used (if ``relative_to`` is not provided, then it is an error). + + ``global_conf`` is used to load the configuration (additions + override the values). ``global_conf`` is copied before modifying. + """ + +def loadfilter(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadfilter``. + + Like ``loadapp()``, except returns in IFilter object. + """ + +def loadserver(uri, name=None, relative_to=None, global_conf=None): + """ + Provided by ``paste.deploy.loadserver``. + + Like ``loadapp()``, except returns in IServer object. + """ + +############################################################ +## Factories +############################################################ + +class IPasteAppFactory(object): + + """ + This is the spec for the ``paste.app_factory`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a WSGI application (IWSGIAPP) given the global + configuration and the local configuration passed in as keyword + arguments. + + All keys are strings, but values in local_conf may not be + valid Python identifiers (if you use ``**kw`` you can still + capture these values). + """ + +class IPasteCompositFactory(object): + + """ + This is the spec for the ``paste.composit_factory`` + protocol/entry_point. + + This also produces WSGI applications, like ``paste.app_factory``, + but is given more access to the context in which it is loaded. + """ + + def __call__(loader, global_conf, **local_conf): + """ + Like IPasteAppFactory this returns a WSGI application + (IWSGIApp). The ``loader`` value conforms to the ``ILoader`` + interface, and can be used to load (contextually) more + applications. + """ + +class IPasteFilterFactory(object): + + """ + This is the spec for the ``paste.filter_factory`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a IFilter object. + """ + +class IPasteFilterAppFactory(object): + + """ + This is the spec for the ``paste.filter_app_factory`` + protocol/entry_point. + """ + + def __call__(wsgi_app, global_conf, **local_conf): + """ + Returns a WSGI application that wraps ``wsgi_app``. + + Note that paste.deploy creates a wrapper for these + objects that implement the IFilter interface. + """ + +class IPasteServerFactory(object): + + """ + This is the spec for the ``paste.server_factory`` + protocol/entry_point. + """ + + def __call__(global_conf, **local_conf): + """ + Returns a IServer object. + """ + +class IPasteServerRunner(object): + + """ + This is the spec for the ``paste.server_runner`` + protocol/entry_point. + """ + + def __call__(wsgi_app, global_conf, **local_conf): + """ + Serves the given WSGI application. May serve once, many + times, forever; nothing about how the server works is + specified here. + + Note that paste.deploy creates a wrapper for these + objects that implement the IServer interface. + """ + +class ILoader(object): + + """ + This is an object passed into ``IPasteCompositFactory``. It is + currently implemented in ``paste.deploy.loadwsgi`` by + ``ConfigLoader`` and ``EggLoader``. + """ + + def get_app(name_or_uri, global_conf=None): + """ + Return an IWSGIApp object. If the loader supports named + applications, then you can use a simple name; otherwise + you must use a full URI. + + Any global configuration you pass in will be added; you should + generally pass through the global configuration you received. + """ + + def get_filter(name_or_uri, global_conf=None): + """ + Return an IFilter object, like ``get_app``. + """ + + def get_server(name_or_uri, global_conf=None): + """ + Return an IServer object, like ``get_app``. + """ + +############################################################ +## Objects +############################################################ + +class IWSGIApp(object): + + """ + This is an application that conforms to `PEP 333 + `_: Python Web Server + Gateway Interface v1.0 + """ + + def __call__(environ, start_response): + """ + Calls ``start_response(status_code, header_list)`` and returns + an iterator for the body of the response. + """ + +class IFilter(object): + + """ + A filter is a simple case of middleware, where an object + wraps a single WSGI application (IWSGIApp). + """ + + def __call__(wsgi_app): + """ + Returns an IWSGIApp object, typically one that wraps the + ``wsgi_app`` passed in. + """ + +class IServer(object): + + """ + A simple server interface. + """ + + def __call__(wsgi_app): + """ + Serves the given WSGI application. May serve once, many + times, forever; nothing about how the server works is + specified here. + """ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py new file mode 100644 index 0000000..bae77ae --- /dev/null +++ b/paste/deploy/loadwsgi.py @@ -0,0 +1,631 @@ +# (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 +import re +import urllib +from ConfigParser import ConfigParser +import pkg_resources +from paste.deploy.util.fixtypeerror import fix_call + +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + +############################################################ +## Utility functions +############################################################ + +def import_string(s): + return pkg_resources.EntryPoint.parse("x="+s).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 + + 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.iteritems(): + defaults[key] = self._interpolate('DEFAULT', key, val, defaults) + return defaults + + def _interpolate(self, section, option, rawval, vars): + try: + return ConfigParser._interpolate( + self, section, option, rawval, vars) + except Exception, e: + args = list(e.args) + args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( + self.filename, section, option, rawval, e) + e.args = tuple(args) + raise + +############################################################ +## Object types +############################################################ + +class _ObjectType(object): + + name = None + egg_protocols = None + config_prefixes = None + + def __init__(self): + # Normalize these variables: + self.egg_protocols = map(_aslist, _aslist(self.egg_protocols)) + self.config_prefixes = map(_aslist, _aslist(self.config_prefixes)) + + def __repr__(self): + return '<%s protocols=%r prefixes=%r>' % ( + 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 = urllib.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 + +############################################################ +## Loaders +############################################################ + +class _Loader(object): + + 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() + self.parser = NicerConfigParser(self.filename) + # Don't lower-case keys: + self.parser.optionxform = str + # Stupid ConfigParser ignores files that aren't found, so + # we have to add an extra check: + if not os.path.exists(filename): + if filename.strip() != filename: + raise IOError( + "File %r not found; trailing whitespace: " + "did you try to use a # on the same line as a filename? " + "(comments must be on their own line)" % filename) + raise IOError( + "File %r not found" % filename) + self.parser.read(filename) + self.parser._defaults.setdefault( + 'here', os.path.dirname(os.path.abspath(filename))) + self.parser._defaults.setdefault( + '__file__', os.path.abspath(filename)) + + 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) + if global_conf is None: + global_conf = {} + else: + global_conf = global_conf.copy() + defaults = self.parser.defaults() + global_conf.update(defaults) + 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 + 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" + % (', '.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([ + (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 LoaderContext(object): + + 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 new file mode 100644 index 0000000..4434898 --- /dev/null +++ b/paste/deploy/paster_templates.py @@ -0,0 +1,33 @@ +# (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 new file mode 100644 index 0000000..ae55f97 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -0,0 +1,23 @@ +import cgi +from paste.deploy 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 = [ + '%s\n' % greeting, + '

%s!

\n' % greeting, + '\n', + ] + items = environ.items() + items.sort() + for key, value in items: + content.append('\n' + % (key, cgi.escape(repr(value)))) + content.append('
%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 new file mode 100644 index 0000000..57155fc --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,23 @@ +import sampleapp +from paste.deploy.config import ConfigMiddleware + +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 new file mode 100644 index 0000000..0c0ae35 --- /dev/null +++ b/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/paste/deploy/util.py b/paste/deploy/util.py new file mode 100644 index 0000000..86c6972 --- /dev/null +++ b/paste/deploy/util.py @@ -0,0 +1,58 @@ +# (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 = kwargs.items() + kwargs.sort() + args += ', '.join(['%s=...' % n for n, v in kwargs]) + gotspec = '(%s)' % args + msg = '%s; got %s, wanted %s' % (exc_info[1], gotspec, 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[0], exc_info[1], exc_info[2] + return val diff --git a/regen-docs b/regen-docs new file mode 100755 index 0000000..f8dad75 --- /dev/null +++ b/regen-docs @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + scp -r _build/* ianb@webwareforpython.org:/home/paste/htdocs/deploy/ +fi diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1959175 --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +from setuptools import setup, find_packages + +version = '1.3.4' + +import os + +here = os.path.dirname(os.path.abspath(__file__)) +finddata_py = os.path.join(here, 'tests', 'finddata.py') +execfile(finddata_py) + +setup( + name="PasteDeploy", + version=version, + description="Load, configure, and compose WSGI applications and servers", + long_description="""\ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). + +For the latest changes see the `news file +`_. +""", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", + "Framework :: Paste", + ], + keywords='web wsgi application server', + author="Ian Bicking", + author_email="ianb@colorstudy.com", + url="http://pythonpaste.org/deploy/", + license='MIT', + namespace_packages=['paste'], + packages=find_packages(exclude='tests'), + package_data=find_package_data( + exclude_directories=standard_exclude_directories + ('tests',)), + zip_safe=False, + test_suite='nose.collector', + tests_require=['nose>=0.11'], + extras_require={ + 'Config': [], + 'Paste': ['Paste'], + }, + entry_points=""" + [paste.filter_app_factory] + config = paste.deploy.config:make_config_filter [Config] + prefix = paste.deploy.config:make_prefix_middleware + + [paste.paster_create_template] + paste_deploy=paste.deploy.paster_templates:PasteDeploy + + [paste.entry_point_description] + paste.app_factory = paste.deploy.epdesc:AppFactoryDescription + paste.composit_factory = paste.deploy.epdesc:CompositeFactoryDescription + paste.composite_factory = paste.deploy.epdesc:CompositeFactoryDescription + paste.filter_app_factory = paste.deploy.epdesc:FilterAppFactoryDescription + paste.filter_factory = paste.deploy.epdesc:FilterFactoryDescription + paste.server_factory = paste.deploy.epdesc:ServerFactoryDescription + paste.server_runner = paste.deploy.epdesc:ServerRunnerDescription + """, + ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0ba8f8a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,14 @@ +import os +import sys + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +sys.path.insert(0, base) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +pkg_resources.require('PasteDeploy') + diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt new file mode 100644 index 0000000..9bfc986 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -0,0 +1,22 @@ +[paste.app_factory] + + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + + +[paste.composit_factory] + + remote_addr=fakeapp.apps:make_remote_addr + + +[paste.filter_app_factory] + + caps2=fakeapp.apps:CapFilter + + +[paste.filter_factory] + + caps=fakeapp.apps:make_cap_filter + + diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py new file mode 100644 index 0000000..cae7eba --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -0,0 +1,69 @@ +############################################################ +## Apps +############################################################ + +def simple_app(response, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['This is ', response] + +def basic_app(environ, start_response): + return simple_app('basic app', environ, start_response) + +def make_basic_app(global_conf, **conf): + return basic_app + +def basic_app2(environ, start_response): + return simple_app('basic app2', environ, start_response) + +def make_basic_app2(global_conf, **conf): + return basic_app2 + +############################################################ +## Composits +############################################################ + +def make_remote_addr(loader, global_conf, **conf): + apps = {} + addrs = {} + for name, value in conf.items(): + if name.startswith('app.'): + apps[name[4:]] = loader.get_app(value, global_conf) + elif name.startswith('addr.'): + addrs[name[5:]] = value + dispatcher = RemoteAddrDispatch() + for name in apps: + dispatcher.map[addrs[name]] = apps[name] + return dispatcher + +class RemoteAddrDispatch(object): + def __init__(self, map=None): + self.map = map or {} + + def __call__(self, environ, start_response): + addr = environ['REMOTE_ADDR'] + app = self.map.get(addr) or self.map['0.0.0.0'] + return app(environ, start_response) + +############################################################ +## Filters +############################################################ + +def make_cap_filter(global_conf, method_to_call='upper'): + def cap_filter(app): + return CapFilter(app, global_conf, method_to_call) + return cap_filter + +class CapFilter(object): + + def __init__(self, app, global_conf, method_to_call='upper'): + self.app = app + self.method_to_call = method_to_call + self.global_conf = global_conf + + def __call__(self, environ, start_response): + app_iter = self.app(environ, start_response) + for item in app_iter: + yield getattr(item, self.method_to_call)() + if hasattr(app_iter, 'close'): + app_iter.close() + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py new file mode 100644 index 0000000..ef13182 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py @@ -0,0 +1,14 @@ +class SimpleApp(object): + def __init__(self, global_conf, local_conf, name): + self.global_conf = global_conf + self.local_conf = local_conf + self.name = name + + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['I am: ', name] + + def make_app(cls, global_conf, **conf): + return cls(global_conf, conf, 'basic') + make_app = classmethod(make_app) + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py new file mode 100644 index 0000000..854483e --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + entry_points={ + 'paste.app_factory': """ + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + """, + 'paste.composit_factory': """ + remote_addr=fakeapp.apps:make_remote_addr + """, + 'paste.filter_factory': """ + caps=fakeapp.apps:make_cap_filter + """, + 'paste.filter_app_factory': """ + caps2=fakeapp.apps:CapFilter + """, + }, + ) diff --git a/tests/finddata.py b/tests/finddata.py new file mode 100644 index 0000000..16057dc --- /dev/null +++ b/tests/finddata.py @@ -0,0 +1,95 @@ +# (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 +# Note: you may want to copy this into your setup.py file verbatim, as +# you can't import this from another package, when you don't know if +# that package is installed yet. + +import os +import sys +from fnmatch import fnmatchcase +from distutils.util import convert_path + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') +standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', + './dist', 'EGG-INFO', '*.egg-info') + +def find_package_data( + where='.', package='', + exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, + show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + + The dictionary looks like:: + + {'package': [files]} + + Where ``files`` is a list of all the files in that package that + don't match anything in ``exclude``. + + If ``only_in_packages`` is true, then top-level directories that + are not packages won't be included (but directories under packages + will). + + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + + If ``show_ignored`` is true, then all the files that aren't + included in package data are shown on stderr (for debugging + purposes). + + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + + out = {} + stack = [(convert_path(where), '', package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "Directory %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + if (os.path.isfile(os.path.join(fn, '__init__.py')) + and not prefix): + if not package: + new_package = name + else: + new_package = package + '.' + name + stack.append((fn, '', new_package, False)) + else: + stack.append((fn, prefix + name + '/', package, only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print >> sys.stderr, ( + "File %s ignored by pattern %s" + % (fn, pattern)) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix+name) + return out + diff --git a/tests/fixture.py b/tests/fixture.py new file mode 100644 index 0000000..6c3e99f --- /dev/null +++ b/tests/fixture.py @@ -0,0 +1,21 @@ +import os +import sys +import shutil + +test_dir = os.path.dirname(__file__) +egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'EGG-INFO') +info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'FakeApp.egg-info') +if not os.path.exists(egg_info_dir): + try: + os.symlink(info_dir, egg_info_dir) + except: + shutil.copytree(info_dir, egg_info_dir) + +sys.path.append(os.path.dirname(egg_info_dir)) + +from pkg_resources import * +working_set.add_entry(os.path.dirname(egg_info_dir)) +require('FakeApp') + diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini new file mode 100644 index 0000000..f1d931c --- /dev/null +++ b/tests/sample_configs/basic_app.ini @@ -0,0 +1,14 @@ +[application:main] +use = egg:FakeApp#basic_app + +[application:other] +use = egg:FakeApp#other + +[composit:remote_addr] +use = egg:FakeApp#remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 + diff --git a/tests/sample_configs/executable.ini b/tests/sample_configs/executable.ini new file mode 100755 index 0000000..3b75fe9 --- /dev/null +++ b/tests/sample_configs/executable.ini @@ -0,0 +1,10 @@ +#!/usr/bin/env paster +[exe] +sys.path = /fake/path/ + /another/fake/path ../fake_packages/ + +[server] +use = egg:PasteScript#cgi + +[app] +use = egg:FakeApp#basic_app diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini new file mode 100644 index 0000000..69bae5a --- /dev/null +++ b/tests/sample_configs/test_config.ini @@ -0,0 +1,36 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:test1] +use = egg:FakeApp#configed +setting1 = foo +setting2 = bar + +[app:test2] +use = egg:FakeApp#configed +set def1 = test2 +set another = TEST +local conf = something + +[app:test3] +use = test2 +set def1 = test3 +another = something more + across several + lines + +[app:test_foreign_config] +use = config:test_config_included.ini +set glob = override +another = FOO + +[app:test_get] +use = egg:FakeApp#configed +set def2 = TEST +get def1 = def1 +get foo = def2 + +[app:test_global_conf] +use = egg:FakeApp#configed +test_interp = this:%(inherit)s diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini new file mode 100644 index 0000000..cc0da7a --- /dev/null +++ b/tests/sample_configs/test_config_included.ini @@ -0,0 +1,10 @@ +[DEFAULT] +def2 = from include +def3 = c + +[app:main] +# Equivalent to the egg reference, but just for kicks... +paste.app_factory = fakeapp.configapps:SimpleApp.make_app +set glob = orig +bob = your uncle +another = BAR diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini new file mode 100644 index 0000000..bfad8dc --- /dev/null +++ b/tests/sample_configs/test_filter.ini @@ -0,0 +1,22 @@ +[app:normal] +use = egg:FakeApp#basic_app + +[pipeline:piped] +pipeline = egg:FakeApp#caps normal + +[filter-app:filt] +use = egg:FakeApp#caps +method_to_call = lower +next = normal + +[pipeline:piped2] +pipeline = egg:FakeApp#caps2 normal + +[filter-app:filt2] +use = egg:FakeApp#caps2 +method_to_call = lower +next = normal + +[app:inv] +use = egg:FakeApp#basic_app +filter-with = egg:FakeApp#caps diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini new file mode 100644 index 0000000..118804f --- /dev/null +++ b/tests/sample_configs/test_filter_with.ini @@ -0,0 +1,12 @@ +[app:main] +use = egg:FakeApp#basic_app +example = test +filter-with = filter1 + +[filter:filter1] +use = egg:FakeApp#caps +filter-with = filter2 + +[filter:filter2] +use = egg:FakeApp#caps + diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py new file mode 100644 index 0000000..11d1f40 --- /dev/null +++ b/tests/test_basic_app.py @@ -0,0 +1,34 @@ +from paste.deploy import loadapp, loadfilter, appconfig +from fixture import * +import fakeapp.apps + +here = os.path.dirname(__file__) + +def test_main(): + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#ignored', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + +def test_other(): + app = loadapp('config:sample_configs/basic_app.ini#other', + relative_to=here) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:sample_configs/basic_app.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 + + diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..26ba482 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,116 @@ +import os +from paste.deploy import loadapp, loadfilter, appconfig +from fixture import * +import fakeapp.configapps as fc +from pprint import pprint + +ini_file = 'config:sample_configs/test_config.ini' +here = os.path.dirname(__file__) +config_path = os.path.join(here, 'sample_configs') +config_filename = os.path.join(config_path, 'test_config.ini') + +def test_config_egg(): + app = loadapp('egg:FakeApp#configed') + assert isinstance(app, fc.SimpleApp) + +def test_config1(): + app = loadapp(ini_file, relative_to=here, name='test1') + assert app.local_conf == { + 'setting1': 'foo', 'setting2': 'bar'} + assert app.global_conf == { + 'def1': 'a', 'def2': 'b', + 'here': config_path, + '__file__': config_filename} + +def test_config2(): + app = loadapp(ini_file, relative_to=here, name='test2') + assert app.local_conf == { + 'local conf': 'something'} + assert app.global_conf == { + 'def1': 'test2', + 'def2': 'b', + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename} + # Run this to make sure the global-conf-modified test2 + # didn't mess up the general global conf + test_config1() + +def test_config3(): + app = loadapp(ini_file, relative_to=here, name='test3') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'local conf': 'something', + 'another': 'something more\nacross several\nlines'} + assert app.global_conf == { + 'def1': 'test3', + 'def2': 'b', + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename} + test_config2() + +def test_foreign_config(): + app = loadapp(ini_file, relative_to=here, name='test_foreign_config') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'another': 'FOO', + 'bob': 'your uncle'} + pprint(app.global_conf) + assert app.global_conf == { + 'def1': 'a', + 'def2': 'from include', + 'def3': 'c', + 'glob': 'override', + 'here': config_path, + '__file__': os.path.join(config_path, 'test_config.ini')} + +def test_config_get(): + app = loadapp(ini_file, relative_to=here, name='test_get') + assert isinstance(app, fc.SimpleApp) + assert app.local_conf == { + 'def1': 'a', + 'foo': 'TEST'} + assert app.global_conf == { + 'def1': 'a', + 'def2': 'TEST', + 'here': config_path, + '__file__': config_filename} + +def test_appconfig(): + conf = appconfig(ini_file, relative_to=here, name='test_get') + assert conf == { + 'def1': 'a', + 'def2': 'TEST', + 'here': config_path, + '__file__': config_filename, + 'foo': 'TEST'} + assert conf.local_conf == { + 'def1': 'a', + 'foo': 'TEST'} + assert conf.global_conf == { + 'def1': 'a', + 'def2': 'TEST', + 'here': config_path, + '__file__': config_filename,} + +def test_appconfig_filter_with(): + conf = appconfig('config:test_filter_with.ini', relative_to=config_path) + assert conf['example'] == 'test' + +def test_global_conf(): + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + pprint(conf) + assert conf == { + 'def1': 'a', + # Note that this gets overwritten: + 'def2': 'b', + 'here': config_path, + 'inherit': 'bazbar', + '__file__': config_filename, + 'test_interp': 'this:bazbar', + } + assert conf.local_conf == { + 'test_interp': 'this:bazbar', + } + diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py new file mode 100644 index 0000000..868e75f --- /dev/null +++ b/tests/test_config_middleware.py @@ -0,0 +1,18 @@ +from nose.tools import assert_raises +from paste.deploy.config import ConfigMiddleware +from paste.fixture import TestApp + +class Bug(Exception): pass + +def app_with_exception(environ, start_response): + def cont(): + yield "something" + raise Bug + start_response('200 OK', [('Content-type', 'text/html')]) + return cont() + +def test_error(): + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) + test_app = TestApp(wrapped) + assert_raises(Bug, test_app.get, '/') + diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..77ee2ee --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,46 @@ +from paste.deploy import loadapp, loadfilter +from fixture import * +import fakeapp.apps + +here = os.path.dirname(__file__) + +def test_filter_app(): + app = loadapp('config:sample_configs/test_filter.ini#filt', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + +def test_pipeline(): + app = loadapp('config:sample_configs/test_filter.ini#piped', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + +def test_filter_app2(): + app = loadapp('config:sample_configs/test_filter.ini#filt2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + +def test_pipeline2(): + app = loadapp('config:sample_configs/test_filter.ini#piped2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + +def test_filter_app_inverted(): + app = loadapp('config:sample_configs/test_filter.ini#inv', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + +def test_filter_with_filter_with(): + app = loadapp('config:sample_configs/test_filter_with.ini', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert isinstance(app.app, fakeapp.apps.CapFilter) + assert app.app.app is fakeapp.apps.basic_app diff --git a/tests/test_load_package.py b/tests/test_load_package.py new file mode 100644 index 0000000..a66b7d0 --- /dev/null +++ b/tests/test_load_package.py @@ -0,0 +1,10 @@ +import sys, os +import pkg_resources +import site +from pprint import pprint + +def test_load_package(): + print 'Path:' + pprint(sys.path) + print pkg_resources.require('FakeApp') + -- cgit v1.2.1 From c7105c2aa558bc4ea967a15b9f472775ecbe6cd5 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 02:27:02 +0300 Subject: Removed an unnecessary (and dangerous?) import --- paste/deploy/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/paste/deploy/__init__.py b/paste/deploy/__init__.py index f89218f..94c63a8 100644 --- a/paste/deploy/__init__.py +++ b/paste/deploy/__init__.py @@ -1,10 +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 loadwsgi import * -try: - from config import CONFIG -except ImportError: - # @@: Or should we require Paste? Or should we put threadlocal - # into this package too? - pass - +from paste.deploy.loadwsgi import * -- cgit v1.2.1 From 421a56659a81ef62ef90ef961089a4f442f7af77 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 02:27:14 +0300 Subject: Added tox configuration --- tox.ini | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3edf152 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py25,py26,py27,py31,py32,jython,pypy + +[testenv] +deps=nose + Paste +commands=python setup.py test -- cgit v1.2.1 From 2e56b360419dbe1bd5a95e6272738b46071ee2a7 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 02:57:21 +0300 Subject: Added myself as maintainer, cleaned up setup.py and excluded the HTML documentation from the sdist and included template files missing from the previous release(s) --- MANIFEST.in | 3 +-- docs/news.txt | 1 + setup.py | 18 ++++++------------ 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index e5deda2..7a2ffb9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ -include docs/*.html include docs/*.txt -exclude docs/rebuild +recursive-include paste/deploy/paster_templates * diff --git a/docs/news.txt b/docs/news.txt index a48214a..2655b32 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -5,6 +5,7 @@ hg tip ------ * Was printing extraneous data when calling setup.py +* Fixed missing paster template files (fixes "paster create -t paste.deploy") 1.3.4 ----- diff --git a/setup.py b/setup.py index 1959175..583ec97 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,9 @@ from setuptools import setup, find_packages -version = '1.3.4' - -import os - -here = os.path.dirname(os.path.abspath(__file__)) -finddata_py = os.path.join(here, 'tests', 'finddata.py') -execfile(finddata_py) setup( name="PasteDeploy", - version=version, + version='1.5.0dev', description="Load, configure, and compose WSGI applications and servers", long_description="""\ This tool provides code to load WSGI applications and servers from @@ -40,12 +33,13 @@ For the latest changes see the `news file keywords='web wsgi application server', author="Ian Bicking", author_email="ianb@colorstudy.com", + maintainer="Alex Gronholm", + maintainer_email="alex.gronholm@nextday.fi", url="http://pythonpaste.org/deploy/", license='MIT', namespace_packages=['paste'], - packages=find_packages(exclude='tests'), - package_data=find_package_data( - exclude_directories=standard_exclude_directories + ('tests',)), + packages=find_packages(exclude=['tests']), + include_package_data=True, zip_safe=False, test_suite='nose.collector', tests_require=['nose>=0.11'], @@ -70,4 +64,4 @@ For the latest changes see the `news file paste.server_factory = paste.deploy.epdesc:ServerFactoryDescription paste.server_runner = paste.deploy.epdesc:ServerRunnerDescription """, - ) +) -- cgit v1.2.1 From 236d4cea8eedc0071d8688ffe1d57e440d89f1f8 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 03:01:32 +0300 Subject: Fixed fix_call import --- paste/deploy/loadwsgi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index bae77ae..306a8ef 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,11 +1,13 @@ # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +from ConfigParser import ConfigParser import os import re import urllib -from ConfigParser import ConfigParser + import pkg_resources -from paste.deploy.util.fixtypeerror import fix_call + +from paste.deploy.util import fix_call __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] -- cgit v1.2.1 From 6e0724c952f8eb498fcd704acd6831f0bc28a826 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 03:14:25 +0300 Subject: Ignored .tox, .pydevproject and .project (the last two from Pydev/Eclipse) --- .hgignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.hgignore b/.hgignore index 5345d54..c2c2cc0 100644 --- a/.hgignore +++ b/.hgignore @@ -3,3 +3,10 @@ syntax: glob dist/ build/ docs/_build/ + +syntax: glob +.tox +syntax: glob +.project +syntax: glob +.pydevproject \ No newline at end of file -- cgit v1.2.1 From 05fab19596acdaac905b3c22b86a058c751a2e4f Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 03:14:49 +0300 Subject: Removed finddata.py since it's apparently no longer used anywhere --- tests/finddata.py | 95 ------------------------------------------------------- 1 file changed, 95 deletions(-) delete mode 100644 tests/finddata.py diff --git a/tests/finddata.py b/tests/finddata.py deleted file mode 100644 index 16057dc..0000000 --- a/tests/finddata.py +++ /dev/null @@ -1,95 +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 -# Note: you may want to copy this into your setup.py file verbatim, as -# you can't import this from another package, when you don't know if -# that package is installed yet. - -import os -import sys -from fnmatch import fnmatchcase -from distutils.util import convert_path - -# Provided as an attribute, so you can append to these instead -# of replicating them: -standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') -standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', - './dist', 'EGG-INFO', '*.egg-info') - -def find_package_data( - where='.', package='', - exclude=standard_exclude, - exclude_directories=standard_exclude_directories, - only_in_packages=True, - show_ignored=False): - """ - Return a dictionary suitable for use in ``package_data`` - in a distutils ``setup.py`` file. - - The dictionary looks like:: - - {'package': [files]} - - Where ``files`` is a list of all the files in that package that - don't match anything in ``exclude``. - - If ``only_in_packages`` is true, then top-level directories that - are not packages won't be included (but directories under packages - will). - - Directories matching any pattern in ``exclude_directories`` will - be ignored; by default directories with leading ``.``, ``CVS``, - and ``_darcs`` will be ignored. - - If ``show_ignored`` is true, then all the files that aren't - included in package data are shown on stderr (for debugging - purposes). - - Note patterns use wildcards, or can be exact paths (including - leading ``./``), and all searching is case-insensitive. - """ - - out = {} - stack = [(convert_path(where), '', package, only_in_packages)] - while stack: - where, prefix, package, only_in_packages = stack.pop(0) - for name in os.listdir(where): - fn = os.path.join(where, name) - if os.path.isdir(fn): - bad_name = False - for pattern in exclude_directories: - if (fnmatchcase(name, pattern) - or fn.lower() == pattern.lower()): - bad_name = True - if show_ignored: - print >> sys.stderr, ( - "Directory %s ignored by pattern %s" - % (fn, pattern)) - break - if bad_name: - continue - if (os.path.isfile(os.path.join(fn, '__init__.py')) - and not prefix): - if not package: - new_package = name - else: - new_package = package + '.' + name - stack.append((fn, '', new_package, False)) - else: - stack.append((fn, prefix + name + '/', package, only_in_packages)) - elif package or not only_in_packages: - # is a file - bad_name = False - for pattern in exclude: - if (fnmatchcase(name, pattern) - or fn.lower() == pattern.lower()): - bad_name = True - if show_ignored: - print >> sys.stderr, ( - "File %s ignored by pattern %s" - % (fn, pattern)) - break - if bad_name: - continue - out.setdefault(package, []).append(prefix+name) - return out - -- cgit v1.2.1 From 113f5613942fbfd201c53781a2f6eb7a028d5c97 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 03:17:44 +0300 Subject: PEP 8 cleanups --- paste/deploy/config.py | 34 +++++++++++++++++++++------------- paste/deploy/converters.py | 6 +++++- paste/deploy/epdesc.py | 5 +++++ paste/deploy/interfaces.py | 22 +++++++++++++++++++--- paste/deploy/loadwsgi.py | 34 +++++++++++++++++++++++++++++++--- paste/deploy/paster_templates.py | 11 ++++++----- paste/deploy/util.py | 4 ++-- 7 files changed, 89 insertions(+), 27 deletions(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 562a61f..1db5680 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -3,12 +3,14 @@ """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: @@ -21,6 +23,7 @@ def local_dict(): config_local.wsgi_dict = result = {} return result + class DispatchingConfig(object): """ @@ -44,7 +47,7 @@ class DispatchingConfig(object): self.dispatching_id = 0 while 1: self._local_key = 'paste.processconfig_%i' % self.dispatching_id - if not local_dict().has_key(self._local_key): + if not self._local_key in local_dict(): break self.dispatching_id += 1 finally: @@ -93,7 +96,7 @@ class DispatchingConfig(object): 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: @@ -122,7 +125,7 @@ class DispatchingConfig(object): def __contains__(self, key): # I thought __getattr__ would catch this, but apparently not - return self.has_key(key) + return key in self def __setitem__(self, key, value): # I thought __getattr__ would catch this, but apparently not @@ -131,6 +134,7 @@ class DispatchingConfig(object): CONFIG = DispatchingConfig() + class ConfigMiddleware(object): """ @@ -155,7 +159,7 @@ class ConfigMiddleware(object): from paste import wsgilib popped_config = None if 'paste.config' in environ: - popped_config = environ['paste.config'] + popped_config = environ['paste.config'] conf = environ['paste.config'] = self.config.copy() app_iter = None CONFIG.push_thread_config(conf) @@ -181,6 +185,7 @@ class ConfigMiddleware(object): 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) @@ -188,16 +193,17 @@ def make_config_filter(app, global_conf, **local_conf): make_config_middleware = ConfigMiddleware.__doc__ + class PrefixMiddleware(object): """Translate a given prefix into a SCRIPT_NAME for the filtered application. - - PrefixMiddleware provides a way to manually override the root prefix + + 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 + 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 + 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 @@ -213,9 +219,9 @@ class PrefixMiddleware(object): 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] @@ -227,7 +233,7 @@ class PrefixMiddleware(object): 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``, + 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 @@ -249,11 +255,12 @@ class PrefixMiddleware(object): 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 = '/' + if not url: + url = '/' environ['PATH_INFO'] = url environ['SCRIPT_NAME'] = self.prefix if self.translate_forwarded_server: @@ -283,6 +290,7 @@ class PrefixMiddleware(object): environ['wsgi.url_scheme'] = self.scheme return self.app(environ, start_response) + def make_prefix_middleware( app, global_conf, prefix='/', translate_forwarded_server=True, diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py index 98edd8f..f852e6c 100644 --- a/paste/deploy/converters.py +++ b/paste/deploy/converters.py @@ -1,5 +1,7 @@ # (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 + + def asbool(obj): if isinstance(obj, (str, unicode)): obj = obj.strip().lower() @@ -12,13 +14,15 @@ def asbool(obj): "String is not true/false: %r" % obj) return bool(obj) + def asint(obj): try: return int(obj) - except (TypeError, ValueError), e: + except (TypeError, ValueError): raise ValueError( "Bad integer value: %r" % obj) + def aslist(obj, sep=None, strip=True): if isinstance(obj, (str, unicode)): lst = obj.split(sep) diff --git a/paste/deploy/epdesc.py b/paste/deploy/epdesc.py index 7c7deae..5f05175 100644 --- a/paste/deploy/epdesc.py +++ b/paste/deploy/epdesc.py @@ -3,6 +3,7 @@ class AppFactoryDescription(object): This gives a factory/function that can create WSGI apps """ + class CompositeFactoryDescription(object): description = """ This gives a factory/function that can create WSGI apps, and has @@ -10,12 +11,14 @@ class CompositeFactoryDescription(object): apps based on name. """ + class FilterAppFactoryDescription(object): description = """ This gives a factory/function that wraps a WSGI application to create another WSGI application (typically applying middleware) """ + class FilterFactoryDescription(object): description = """ This gives a factory/function that return a function that can wrap @@ -23,6 +26,7 @@ class FilterFactoryDescription(object): paste.filter_app_factory is the same thing with less layers. """ + class ServerFactoryDescription(object): description = """ This gives a factory/function that creates a server, that can be @@ -30,6 +34,7 @@ class ServerFactoryDescription(object): paste.server_runner is the same thing with less layers. """ + class ServerRunnerDescription(object): description = """ This gives a factory/function that, given a WSGI application and diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py index 29163a5..3dbc44e 100644 --- a/paste/deploy/interfaces.py +++ b/paste/deploy/interfaces.py @@ -1,13 +1,15 @@ # (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 + ############################################################ ## Functions ############################################################ + def loadapp(uri, name=None, relative_to=None, global_conf=None): """ Provided by ``paste.deploy.loadapp``. - + Load the specified URI as a WSGI application (returning IWSGIApp). The ``name`` can be in the URI (typically as ``#name``). If it is and ``name`` is given, the keyword argument overrides the URI. @@ -19,6 +21,7 @@ def loadapp(uri, name=None, relative_to=None, global_conf=None): override the values). ``global_conf`` is copied before modifying. """ + def loadfilter(uri, name=None, relative_to=None, global_conf=None): """ Provided by ``paste.deploy.loadfilter``. @@ -26,6 +29,7 @@ def loadfilter(uri, name=None, relative_to=None, global_conf=None): Like ``loadapp()``, except returns in IFilter object. """ + def loadserver(uri, name=None, relative_to=None, global_conf=None): """ Provided by ``paste.deploy.loadserver``. @@ -33,10 +37,12 @@ def loadserver(uri, name=None, relative_to=None, global_conf=None): Like ``loadapp()``, except returns in IServer object. """ + ############################################################ ## Factories ############################################################ + class IPasteAppFactory(object): """ @@ -55,6 +61,7 @@ class IPasteAppFactory(object): capture these values). """ + class IPasteCompositFactory(object): """ @@ -73,6 +80,7 @@ class IPasteCompositFactory(object): applications. """ + class IPasteFilterFactory(object): """ @@ -85,13 +93,14 @@ class IPasteFilterFactory(object): Returns a IFilter object. """ + class IPasteFilterAppFactory(object): """ This is the spec for the ``paste.filter_app_factory`` protocol/entry_point. """ - + def __call__(wsgi_app, global_conf, **local_conf): """ Returns a WSGI application that wraps ``wsgi_app``. @@ -100,6 +109,7 @@ class IPasteFilterAppFactory(object): objects that implement the IFilter interface. """ + class IPasteServerFactory(object): """ @@ -112,6 +122,7 @@ class IPasteServerFactory(object): Returns a IServer object. """ + class IPasteServerRunner(object): """ @@ -129,6 +140,7 @@ class IPasteServerRunner(object): objects that implement the IServer interface. """ + class ILoader(object): """ @@ -151,16 +163,18 @@ class ILoader(object): """ Return an IFilter object, like ``get_app``. """ - + def get_server(name_or_uri, global_conf=None): """ Return an IServer object, like ``get_app``. """ + ############################################################ ## Objects ############################################################ + class IWSGIApp(object): """ @@ -175,6 +189,7 @@ class IWSGIApp(object): an iterator for the body of the response. """ + class IFilter(object): """ @@ -188,6 +203,7 @@ class IFilter(object): ``wsgi_app`` passed in. """ + class IServer(object): """ diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 306a8ef..a4a8b4b 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -11,12 +11,15 @@ from paste.deploy.util import fix_call __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + ############################################################ ## Utility functions ############################################################ + def import_string(s): - return pkg_resources.EntryPoint.parse("x="+s).load(False) + return pkg_resources.EntryPoint.parse("x=" + s).load(False) + def _aslist(obj): """ @@ -30,6 +33,7 @@ def _aslist(obj): else: return [obj] + def _flatten(lst): """ Flatten a nested list. @@ -41,6 +45,7 @@ def _flatten(lst): result.extend(_flatten(item)) return result + class NicerConfigParser(ConfigParser): def __init__(self, filename, *args, **kw): @@ -69,10 +74,12 @@ class NicerConfigParser(ConfigParser): e.args = tuple(args) raise + ############################################################ ## Object types ############################################################ + class _ObjectType(object): name = None @@ -93,6 +100,7 @@ class _ObjectType(object): return fix_call(context.object, context.global_conf, **context.local_conf) + class _App(_ObjectType): name = 'application' @@ -114,6 +122,7 @@ class _App(_ObjectType): APP = _App() + class _Filter(_ObjectType): name = 'filter' egg_protocols = [['paste.filter_factory', 'paste.filter_app_factory']] @@ -135,6 +144,7 @@ class _Filter(_ObjectType): FILTER = _Filter() + class _Server(_ObjectType): name = 'server' egg_protocols = [['paste.server_factory', 'paste.server_runner']] @@ -156,6 +166,7 @@ class _Server(_ObjectType): SERVER = _Server() + # Virtual type: (@@: There's clearly something crufty here; # this probably could be more elegant) class _PipeLine(_ObjectType): @@ -171,6 +182,7 @@ class _PipeLine(_ObjectType): PIPELINE = _PipeLine() + class _FilterApp(_ObjectType): name = 'filter_app' @@ -181,6 +193,7 @@ class _FilterApp(_ObjectType): FILTER_APP = _FilterApp() + class _FilterWith(_App): name = 'filtered_with' @@ -197,19 +210,24 @@ class _FilterWith(_App): 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, @@ -218,6 +236,7 @@ def appconfig(uri, name=None, relative_to=None, global_conf=None): _loaders = {} + def loadobj(object_type, uri, name=None, relative_to=None, global_conf=None): context = loadcontext( @@ -225,6 +244,7 @@ def loadobj(object_type, uri, name=None, relative_to=None, global_conf=global_conf) return context.create() + def loadcontext(object_type, uri, name=None, relative_to=None, global_conf=None): if '#' in uri: @@ -248,6 +268,7 @@ def loadcontext(object_type, uri, name=None, relative_to=None, 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) @@ -273,6 +294,7 @@ def _loadconfig(object_type, uri, path, name, relative_to, _loaders['config'] = _loadconfig + def _loadegg(object_type, uri, spec, name, relative_to, global_conf): loader = EggLoader(spec) @@ -280,10 +302,12 @@ def _loadegg(object_type, uri, spec, name, relative_to, _loaders['egg'] = _loadegg + ############################################################ ## Loaders ############################################################ + class _Loader(object): def get_app(self, name=None, global_conf=None): @@ -311,6 +335,7 @@ class _Loader(object): 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 @@ -319,6 +344,7 @@ class _Loader(object): return False return self._absolute_re.search(name) + class ConfigLoader(_Loader): def __init__(self, filename): @@ -536,8 +562,8 @@ class ConfigLoader(_Loader): found.append(name_prefix) name = 'main' for section in sections: - if section.startswith(name_prefix+':'): - if section[len(name_prefix)+1:].strip() == name: + if section.startswith(name_prefix + ':'): + if section[len(name_prefix) + 1:].strip() == name: found.append(section) return found @@ -598,6 +624,7 @@ class EggLoader(_Loader): % (name, self.spec, ', '.join(_flatten(protocol_options)))) return possible[0] + class LoaderContext(object): def __init__(self, obj, object_type, protocol, @@ -626,6 +653,7 @@ class LoaderContext(object): conf.context = self return conf + class AttrDict(dict): """ A dictionary that can be assigned to. diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py index 4434898..25be21b 100644 --- a/paste/deploy/paster_templates.py +++ b/paste/deploy/paster_templates.py @@ -1,15 +1,17 @@ # (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): @@ -26,8 +28,7 @@ class PasteDeploy(Template): ' main = %(package)s.wsgiapp:make_app\n') % vars, indent=False) if command.verbose: - print '*'*72 + print '*' * 72 print '* Run "paster serve docs/devel_config.ini" to run the sample application' print '* on http://localhost:8080' - print '*'*72 - + print '*' * 72 diff --git a/paste/deploy/util.py b/paste/deploy/util.py index 86c6972..41f8dbd 100644 --- a/paste/deploy/util.py +++ b/paste/deploy/util.py @@ -9,7 +9,7 @@ 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: @@ -42,7 +42,7 @@ def fix_type_error(exc_info, callable, varargs, kwargs): def _short_repr(v): v = repr(v) if len(v) > 12: - v = v[:8]+'...'+v[-4:] + v = v[:8] + '...' + v[-4:] return v -- cgit v1.2.1 From 71acd963cb8da08b19b443da52f8b791996034c1 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 17 May 2011 18:06:47 +0300 Subject: Use a substitution variable for the interpreter name -- fixes tests on Jython --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3edf152..af8e567 100644 --- a/tox.ini +++ b/tox.ini @@ -4,4 +4,4 @@ envlist = py25,py26,py27,py31,py32,jython,pypy [testenv] deps=nose Paste -commands=python setup.py test +commands={envpython} setup.py test -- cgit v1.2.1 From 6e6a0649c43485b1e07487d95d0acfe656163321 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 04:06:49 +0300 Subject: Ignore the FakeApp's egg directory that is generated with the test suite --- .hgignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.hgignore b/.hgignore index c2c2cc0..ff958ef 100644 --- a/.hgignore +++ b/.hgignore @@ -9,4 +9,6 @@ syntax: glob syntax: glob .project syntax: glob -.pydevproject \ No newline at end of file +.pydevproject +syntax: regexp +^tests/fake_packages/FakeApp\.egg$ \ No newline at end of file -- cgit v1.2.1 From 0cd3d719a4c3c14adb945ff46d6c1c5741eaa8f3 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 04:07:48 +0300 Subject: Don't install Paste for py3.x since it's not yet compatible --- tox.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tox.ini b/tox.ini index af8e567..c1bf53e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,3 +5,10 @@ envlist = py25,py26,py27,py31,py32,jython,pypy deps=nose Paste commands={envpython} setup.py test + +# Keep it this way until Paste has been ported to py3k +[testenv:py31] +deps=nose + +[testenv:py32] +deps=nose -- cgit v1.2.1 From ff53b492d7dfa16dd35715c1b5462f0f5363b395 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 04:34:15 +0300 Subject: Added PyPy 1.4 and 1.5 separately to the environment list --- tox.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c1bf53e..07853f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py31,py32,jython,pypy +envlist = py25,py26,py27,py31,py32,jython,pypy1.4,pypy1.5 [testenv] deps=nose @@ -12,3 +12,9 @@ deps=nose [testenv:py32] deps=nose + +[testenv:pypy1.4] +basepython=pypy1.4 + +[testenv:pypy1.5] +basepython=pypy1.5 -- cgit v1.2.1 From 412d4280572e804f36a29a219c330ffc98d3d460 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 05:03:21 +0300 Subject: Refactored the code to be compatible with Python 3.1 and above --- paste/deploy/compat.py | 30 +++++++++ paste/deploy/converters.py | 11 ++-- paste/deploy/loadwsgi.py | 73 ++++++++++++++-------- paste/deploy/paster_templates.py | 10 +-- paste/deploy/util.py | 4 +- tests/__init__.py | 1 - tests/fixture.py | 3 +- tests/sample_configs/test_config.ini | 2 + tests/sample_configs/test_error.ini | 8 +++ tests/test_basic_app.py | 12 ++-- tests/test_config.py | 117 ++++++++++++++++++++++------------- tests/test_config_middleware.py | 16 ++++- tests/test_filter.py | 11 +++- tests/test_load_package.py | 14 +++-- 14 files changed, 211 insertions(+), 101 deletions(-) create mode 100644 paste/deploy/compat.py create mode 100644 tests/sample_configs/test_error.ini diff --git a/paste/deploy/compat.py b/paste/deploy/compat.py new file mode 100644 index 0000000..f7b93f5 --- /dev/null +++ b/paste/deploy/compat.py @@ -0,0 +1,30 @@ +# (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 +"""Python 2<->3 compatibility module""" +import sys + + +def print_(template, *args, **kwargs): + template = str(template) + if args: + template = template % args + elif kwargs: + template = template % kwargs + sys.stdout.writelines(template) + +if sys.version_info < (3, 0): + basestring = basestring + from ConfigParser import ConfigParser + from urllib import unquote + iteritems = lambda d: d.iteritems() + + def reraise(t, e, tb): + exec('raise t, e, tb', dict(t=t, e=e, tb=tb)) +else: + basestring = str + from configparser import ConfigParser + from urllib.parse import unquote + iteritems = lambda d: d.items() + + def reraise(t, e, tb): + exec('raise e from tb', dict(e=e, tb=tb)) diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py index f852e6c..f37d267 100644 --- a/paste/deploy/converters.py +++ b/paste/deploy/converters.py @@ -1,17 +1,17 @@ # (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.compat import basestring def asbool(obj): - if isinstance(obj, (str, unicode)): + if isinstance(obj, basestring): obj = obj.strip().lower() if obj in ['true', 'yes', 'on', 'y', 't', '1']: return True elif obj in ['false', 'no', 'off', 'n', 'f', '0']: return False else: - raise ValueError( - "String is not true/false: %r" % obj) + raise ValueError("String is not true/false: %r" % obj) return bool(obj) @@ -19,12 +19,11 @@ def asint(obj): try: return int(obj) except (TypeError, ValueError): - raise ValueError( - "Bad integer value: %r" % obj) + raise ValueError("Bad integer value: %r" % obj) def aslist(obj, sep=None, strip=True): - if isinstance(obj, (str, unicode)): + if isinstance(obj, basestring): lst = obj.split(sep) if strip: lst = [v.strip() for v in lst] diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index a4a8b4b..2da8213 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -1,12 +1,13 @@ # (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 +from __future__ import with_statement import os +import sys import re -import urllib import pkg_resources +from paste.deploy.compat import ConfigParser, unquote, iteritems from paste.deploy.util import fix_call __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] @@ -51,6 +52,10 @@ class NicerConfigParser(ConfigParser): def __init__(self, filename, *args, **kw): ConfigParser.__init__(self, *args, **kw) self.filename = filename + if hasattr(self, '_interpolation'): + self._interpolation = self.InterpolateWrapper(self._interpolation) + + read_file = getattr(ConfigParser, 'read_file', ConfigParser.readfp) def defaults(self): """Return the defaults, with their values interpolated (with the @@ -59,21 +64,45 @@ class NicerConfigParser(ConfigParser): Mainly to support defaults using values such as %(here)s """ defaults = ConfigParser.defaults(self).copy() - for key, val in defaults.iteritems(): - defaults[key] = self._interpolate('DEFAULT', key, val, defaults) + for key, val in iteritems(defaults): + defaults[key] = self.get('DEFAULT', key) or val return defaults def _interpolate(self, section, option, rawval, vars): + # Python < 3.2 try: return ConfigParser._interpolate( self, section, option, rawval, vars) - except Exception, e: + except Exception: + e = sys.exc_info()[1] args = list(e.args) args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( self.filename, section, option, rawval, e) e.args = tuple(args) + e.message = args[0] raise + class InterpolateWrapper(object): + # Python >= 3.2 + 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] = 'Error in file %s, [%s] %s=%r: %s' % ( + parser.filename, section, option, value, e) + e.args = tuple(args) + e.message = args[0] + raise + ############################################################ ## Object types @@ -88,8 +117,8 @@ class _ObjectType(object): def __init__(self): # Normalize these variables: - self.egg_protocols = map(_aslist, _aslist(self.egg_protocols)) - self.config_prefixes = map(_aslist, _aslist(self.config_prefixes)) + 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 '<%s protocols=%r prefixes=%r>' % ( @@ -286,7 +315,7 @@ def _loadconfig(object_type, uri, path, name, relative_to, path = relative_to + '/' + path if path.startswith('///'): path = path[2:] - path = urllib.unquote(path) + path = unquote(path) loader = ConfigLoader(path) if global_conf: loader.update_defaults(global_conf, overwrite=False) @@ -349,27 +378,17 @@ class ConfigLoader(_Loader): def __init__(self, filename): self.filename = filename = filename.strip() - self.parser = NicerConfigParser(self.filename) - # Don't lower-case keys: - self.parser.optionxform = str - # Stupid ConfigParser ignores files that aren't found, so - # we have to add an extra check: - if not os.path.exists(filename): - if filename.strip() != filename: - raise IOError( - "File %r not found; trailing whitespace: " - "did you try to use a # on the same line as a filename? " - "(comments must be on their own line)" % filename) - raise IOError( - "File %r not found" % filename) - self.parser.read(filename) - self.parser._defaults.setdefault( - 'here', os.path.dirname(os.path.abspath(filename))) - self.parser._defaults.setdefault( - '__file__', os.path.abspath(filename)) + 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(): + for key, value in iteritems(new_defaults): if not overwrite and key in self.parser._defaults: continue self.parser._defaults[key] = value diff --git a/paste/deploy/paster_templates.py b/paste/deploy/paster_templates.py index 25be21b..9c5f942 100644 --- a/paste/deploy/paster_templates.py +++ b/paste/deploy/paster_templates.py @@ -4,6 +4,8 @@ import os from paste.script.templates import Template +from paste.deploy.compat import print_ + class PasteDeploy(Template): @@ -28,7 +30,7 @@ class PasteDeploy(Template): ' 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 + 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/util.py b/paste/deploy/util.py index 41f8dbd..c5056b2 100644 --- a/paste/deploy/util.py +++ b/paste/deploy/util.py @@ -3,6 +3,8 @@ import inspect import sys +from paste.deploy.compat import reraise + def fix_type_error(exc_info, callable, varargs, kwargs): """ @@ -54,5 +56,5 @@ def fix_call(callable, *args, **kw): 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] + reraise(*exc_info) return val diff --git a/tests/__init__.py b/tests/__init__.py index 0ba8f8a..cffe526 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,4 +11,3 @@ import pkg_resources # Make absolutely sure we're testing *this* package, not # some other installed package pkg_resources.require('PasteDeploy') - diff --git a/tests/fixture.py b/tests/fixture.py index 6c3e99f..751659d 100644 --- a/tests/fixture.py +++ b/tests/fixture.py @@ -12,10 +12,9 @@ if not os.path.exists(egg_info_dir): os.symlink(info_dir, egg_info_dir) except: shutil.copytree(info_dir, egg_info_dir) - + sys.path.append(os.path.dirname(egg_info_dir)) from pkg_resources import * working_set.add_entry(os.path.dirname(egg_info_dir)) require('FakeApp') - diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini index 69bae5a..d614829 100644 --- a/tests/sample_configs/test_config.ini +++ b/tests/sample_configs/test_config.ini @@ -1,11 +1,13 @@ [DEFAULT] def1 = a def2 = b +basepath = %(here)s [app:test1] use = egg:FakeApp#configed setting1 = foo setting2 = bar +apppath = %(basepath)s/app [app:test2] use = egg:FakeApp#configed diff --git a/tests/sample_configs/test_error.ini b/tests/sample_configs/test_error.ini new file mode 100644 index 0000000..b6ad5b2 --- /dev/null +++ b/tests/sample_configs/test_error.ini @@ -0,0 +1,8 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:main] +use = egg:FakeApp#configed +setting1 = foo +setting2 = %(does_not_exist)s/bar diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py index 11d1f40..1ddb52b 100644 --- a/tests/test_basic_app.py +++ b/tests/test_basic_app.py @@ -1,9 +1,12 @@ -from paste.deploy import loadapp, loadfilter, appconfig -from fixture import * +from paste.deploy import loadapp + +from tests.fixture import * import fakeapp.apps + here = os.path.dirname(__file__) + def test_main(): app = loadapp('config:sample_configs/basic_app.ini', relative_to=here) @@ -18,11 +21,12 @@ def test_main(): relative_to=here, name='main') assert app is fakeapp.apps.basic_app + def test_other(): app = loadapp('config:sample_configs/basic_app.ini#other', relative_to=here) assert app is fakeapp.apps.basic_app2 - + def test_composit(): app = loadapp('config:sample_configs/basic_app.ini#remote_addr', @@ -30,5 +34,3 @@ def test_composit(): assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) assert app.map['127.0.0.1'] is fakeapp.apps.basic_app assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 - - diff --git a/tests/test_config.py b/tests/test_config.py index 26ba482..b6cd158 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,116 +1,145 @@ -import os -from paste.deploy import loadapp, loadfilter, appconfig -from fixture import * +from nose.tools import eq_ + +from paste.deploy import loadapp, appconfig +from tests.fixture import * import fakeapp.configapps as fc -from pprint import pprint + ini_file = 'config:sample_configs/test_config.ini' here = os.path.dirname(__file__) config_path = os.path.join(here, 'sample_configs') config_filename = os.path.join(config_path, 'test_config.ini') + def test_config_egg(): app = loadapp('egg:FakeApp#configed') assert isinstance(app, fc.SimpleApp) - + + def test_config1(): app = loadapp(ini_file, relative_to=here, name='test1') - assert app.local_conf == { - 'setting1': 'foo', 'setting2': 'bar'} - assert app.global_conf == { - 'def1': 'a', 'def2': 'b', + eq_(app.local_conf, { + 'setting1': 'foo', + 'setting2': 'bar', + 'apppath': os.path.join(config_path, 'app')}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'b', + 'basepath': config_path, 'here': config_path, - '__file__': config_filename} + '__file__': config_filename}) + def test_config2(): app = loadapp(ini_file, relative_to=here, name='test2') - assert app.local_conf == { - 'local conf': 'something'} - assert app.global_conf == { + eq_(app.local_conf, { + 'local conf': 'something'}) + eq_(app.global_conf, { 'def1': 'test2', 'def2': 'b', + 'basepath': config_path, 'another': 'TEST', 'here': config_path, - '__file__': config_filename} + '__file__': config_filename}) # Run this to make sure the global-conf-modified test2 # didn't mess up the general global conf test_config1() + def test_config3(): app = loadapp(ini_file, relative_to=here, name='test3') assert isinstance(app, fc.SimpleApp) - assert app.local_conf == { + eq_(app.local_conf, { 'local conf': 'something', - 'another': 'something more\nacross several\nlines'} - assert app.global_conf == { + 'another': 'something more\nacross several\nlines'}) + eq_(app.global_conf, { 'def1': 'test3', 'def2': 'b', + 'basepath': config_path, 'another': 'TEST', 'here': config_path, - '__file__': config_filename} + '__file__': config_filename}) test_config2() - + + def test_foreign_config(): app = loadapp(ini_file, relative_to=here, name='test_foreign_config') assert isinstance(app, fc.SimpleApp) - assert app.local_conf == { + eq_(app.local_conf, { 'another': 'FOO', - 'bob': 'your uncle'} - pprint(app.global_conf) - assert app.global_conf == { + 'bob': 'your uncle'}) + eq_(app.global_conf, { 'def1': 'a', 'def2': 'from include', 'def3': 'c', + 'basepath': config_path, 'glob': 'override', 'here': config_path, - '__file__': os.path.join(config_path, 'test_config.ini')} - + '__file__': os.path.join(config_path, 'test_config.ini')}) + + def test_config_get(): app = loadapp(ini_file, relative_to=here, name='test_get') assert isinstance(app, fc.SimpleApp) - assert app.local_conf == { + eq_(app.local_conf, { 'def1': 'a', - 'foo': 'TEST'} - assert app.global_conf == { + 'foo': 'TEST'}) + eq_(app.global_conf, { 'def1': 'a', 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), 'here': config_path, - '__file__': config_filename} + '__file__': config_filename}) + def test_appconfig(): conf = appconfig(ini_file, relative_to=here, name='test_get') - assert conf == { + eq_(conf, { 'def1': 'a', 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), 'here': config_path, '__file__': config_filename, - 'foo': 'TEST'} - assert conf.local_conf == { + 'foo': 'TEST'}) + eq_(conf.local_conf, { 'def1': 'a', - 'foo': 'TEST'} - assert conf.global_conf == { + 'foo': 'TEST'}) + eq_(conf.global_conf, { 'def1': 'a', 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), 'here': config_path, - '__file__': config_filename,} + '__file__': config_filename}) + def test_appconfig_filter_with(): conf = appconfig('config:test_filter_with.ini', relative_to=config_path) - assert conf['example'] == 'test' + eq_(conf['example'], 'test') + def test_global_conf(): - conf = appconfig(ini_file, relative_to=here, name='test_global_conf', global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) - pprint(conf) - assert conf == { + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', + global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + eq_(conf, { 'def1': 'a', # Note that this gets overwritten: 'def2': 'b', + 'basepath': os.path.join(here, 'sample_configs'), 'here': config_path, 'inherit': 'bazbar', '__file__': config_filename, 'test_interp': 'this:bazbar', - } - assert conf.local_conf == { - 'test_interp': 'this:bazbar', - } - + }) + eq_(conf.local_conf, { + 'test_interp': 'this:bazbar'}) + + +def test_interpolate_exception(): + try: + appconfig('config:test_error.ini', relative_to=config_path) + except Exception: + e = sys.exc_info()[1] + expected = "Error in file %s" % os.path.join(config_path, 'test_error.ini') + eq_(str(e).split(',')[0], expected) + else: + assert False, 'Should have raised an exception' diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py index 868e75f..cc315e3 100644 --- a/tests/test_config_middleware.py +++ b/tests/test_config_middleware.py @@ -1,8 +1,12 @@ from nose.tools import assert_raises +from nose.plugins.skip import SkipTest + from paste.deploy.config import ConfigMiddleware -from paste.fixture import TestApp -class Bug(Exception): pass + +class Bug(Exception): + pass + def app_with_exception(environ, start_response): def cont(): @@ -11,8 +15,14 @@ def app_with_exception(environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) return cont() + def test_error(): + # This import is conditional due to Paste not yet working on py3k + try: + from paste.fixture import TestApp + except ImportError: + raise SkipTest + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) test_app = TestApp(wrapped) assert_raises(Bug, test_app.get, '/') - diff --git a/tests/test_filter.py b/tests/test_filter.py index 77ee2ee..a76af7c 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,9 +1,11 @@ -from paste.deploy import loadapp, loadfilter -from fixture import * +from paste.deploy import loadapp +from tests.fixture import * import fakeapp.apps + here = os.path.dirname(__file__) + def test_filter_app(): app = loadapp('config:sample_configs/test_filter.ini#filt', relative_to=here) @@ -11,6 +13,7 @@ def test_filter_app(): assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'lower' + def test_pipeline(): app = loadapp('config:sample_configs/test_filter.ini#piped', relative_to=here) @@ -18,6 +21,7 @@ def test_pipeline(): assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'upper' + def test_filter_app2(): app = loadapp('config:sample_configs/test_filter.ini#filt2', relative_to=here) @@ -25,6 +29,7 @@ def test_filter_app2(): assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'lower' + def test_pipeline2(): app = loadapp('config:sample_configs/test_filter.ini#piped2', relative_to=here) @@ -32,12 +37,14 @@ def test_pipeline2(): assert app.app is fakeapp.apps.basic_app assert app.method_to_call == 'upper' + def test_filter_app_inverted(): app = loadapp('config:sample_configs/test_filter.ini#inv', relative_to=here) assert isinstance(app, fakeapp.apps.CapFilter) assert app.app is fakeapp.apps.basic_app + def test_filter_with_filter_with(): app = loadapp('config:sample_configs/test_filter_with.ini', relative_to=here) diff --git a/tests/test_load_package.py b/tests/test_load_package.py index a66b7d0..b3fea55 100644 --- a/tests/test_load_package.py +++ b/tests/test_load_package.py @@ -1,10 +1,12 @@ -import sys, os -import pkg_resources -import site from pprint import pprint +import sys + +import pkg_resources + +from paste.deploy.compat import print_ + def test_load_package(): - print 'Path:' + print_('Path:') pprint(sys.path) - print pkg_resources.require('FakeApp') - + print_(pkg_resources.require('FakeApp')) -- cgit v1.2.1 From 35779a476ce2fd61647daf0889f828d4b497f3bb Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 05:39:51 +0300 Subject: Added the proper classifiers for supported Python versions --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 583ec97..d3127a7 100644 --- a/setup.py +++ b/setup.py @@ -23,11 +23,17 @@ For the latest changes see the `news file "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", + "Programming Language :: Python :: 2.5", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", + "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Paste", ], keywords='web wsgi application server', -- cgit v1.2.1 From f972b92de5ceab675c1c82b49c0c51ec02bba431 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 22 May 2011 06:05:11 +0300 Subject: Updated news --- docs/news.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/news.txt b/docs/news.txt index 2655b32..63a52d2 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -4,9 +4,18 @@ Paste Deployment News hg tip ------ +* Project is now maintained by Alex Grönholm + * Was printing extraneous data when calling setup.py + * Fixed missing paster template files (fixes "paster create -t paste.deploy") +* Excluded tests from release distributions + +* Dropped Python 2.4 support + +* Added Python 3.x support + 1.3.4 ----- -- cgit v1.2.1 From 46650ff51babab37ed5fac66c32a3619320343ae Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 02:13:25 +0300 Subject: Updated WSGI reference to PEP 3333 --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 3150e59..cb10c66 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -219,7 +219,7 @@ Basic Usage ----------- The basic way you'll use Paste Deployment is to load `WSGI -`_ applications. Many +`_ applications. Many Python frameworks now support WSGI, so applications written for these frameworks should be usable. -- cgit v1.2.1 From fcac2b183d491801439dec0036fc0db7d7bfc9a7 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 04:02:35 +0300 Subject: Recommended pip/distribute over setuptools/easy_install --- docs/index.txt | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index cb10c66..a1678fa 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -64,13 +64,16 @@ new entry points will be defined. Installation ------------ -First install `setuptools -`_. +First make sure you have either +`setuptools `_ or its +modern replacement +`distribute `_ installed. +For Python 3.x you need distribute as setuptools does not work on it. -You can install Paste Deployment by installing `easy_install -`_ and running:: +Then you can install Paste Deployment using `pip +`_ by running:: - $ sudo easy_install.py PasteDeploy + $ sudo pip install PasteDeploy If you want to track development, do:: @@ -85,7 +88,7 @@ For downloads and other information see the `Cheese Shop PasteDeploy page `_. A complimentary package is `Paste Script `_. To install -that use ``easy_install PasteScript`` (or ``easy_install +that, use ``pip install PasteScript`` (or ``pip install PasteScript==dev``). From the User Perspective @@ -197,7 +200,7 @@ with ``next`` (which refers to the next section). The could imagine it logs people in and checks permissions. That last section is just a reference to an application that you -probably installed with ``easy_install BlogApp``, and one bit of +probably installed with ``pip install BlogApp``, and one bit of configuration you passed to it (``database``). Lastly:: @@ -447,8 +450,9 @@ other with the attributes ``.local_conf`` and ``.global_conf``. `Python Eggs `_ are a distribution and installation format produced by `setuptools -`_ that adds -metadata to a normal Python package (among other things). +`_ and +`distribute `_ that adds metadata to a +normal Python package (among other things). You don't need to understand a whole lot about Eggs to use them. If you have a `distutils -- cgit v1.2.1 From 66f879dffb0432f6e297c9bd0457f7ea7ff12b12 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 04:11:40 +0300 Subject: Removed interfaces and entry point descriptions as useless (entry points are documented in the manual anyway) --- docs/news.txt | 3 + paste/deploy/epdesc.py | 42 --------- paste/deploy/interfaces.py | 218 --------------------------------------------- setup.py | 9 -- 4 files changed, 3 insertions(+), 269 deletions(-) delete mode 100644 paste/deploy/epdesc.py delete mode 100644 paste/deploy/interfaces.py diff --git a/docs/news.txt b/docs/news.txt index 337347c..d89cc76 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -19,6 +19,9 @@ hg tip * Dropped Python 2.4 support +* Removed the ``paste.deploy.epdesc`` and ``paste.deploy.interfaces`` modules + -- contact the maintainer if you actually needed them + 1.3.4 ----- diff --git a/paste/deploy/epdesc.py b/paste/deploy/epdesc.py deleted file mode 100644 index 5f05175..0000000 --- a/paste/deploy/epdesc.py +++ /dev/null @@ -1,42 +0,0 @@ -class AppFactoryDescription(object): - description = """ - This gives a factory/function that can create WSGI apps - """ - - -class CompositeFactoryDescription(object): - description = """ - This gives a factory/function that can create WSGI apps, and has - access to the application creator so that it can in turn fetch - apps based on name. - """ - - -class FilterAppFactoryDescription(object): - description = """ - This gives a factory/function that wraps a WSGI application to - create another WSGI application (typically applying middleware) - """ - - -class FilterFactoryDescription(object): - description = """ - This gives a factory/function that return a function that can wrap - a WSGI application and returns another WSGI application. - paste.filter_app_factory is the same thing with less layers. - """ - - -class ServerFactoryDescription(object): - description = """ - This gives a factory/function that creates a server, that can be - called with a WSGI application to run indefinitely. - paste.server_runner is the same thing with less layers. - """ - - -class ServerRunnerDescription(object): - description = """ - This gives a factory/function that, given a WSGI application and - configuration, will serve the application indefinitely. - """ diff --git a/paste/deploy/interfaces.py b/paste/deploy/interfaces.py deleted file mode 100644 index 3dbc44e..0000000 --- a/paste/deploy/interfaces.py +++ /dev/null @@ -1,218 +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 - -############################################################ -## Functions -############################################################ - - -def loadapp(uri, name=None, relative_to=None, global_conf=None): - """ - Provided by ``paste.deploy.loadapp``. - - Load the specified URI as a WSGI application (returning IWSGIApp). - The ``name`` can be in the URI (typically as ``#name``). If it is - and ``name`` is given, the keyword argument overrides the URI. - - If the URI contains a relative filename, then ``relative_to`` is - used (if ``relative_to`` is not provided, then it is an error). - - ``global_conf`` is used to load the configuration (additions - override the values). ``global_conf`` is copied before modifying. - """ - - -def loadfilter(uri, name=None, relative_to=None, global_conf=None): - """ - Provided by ``paste.deploy.loadfilter``. - - Like ``loadapp()``, except returns in IFilter object. - """ - - -def loadserver(uri, name=None, relative_to=None, global_conf=None): - """ - Provided by ``paste.deploy.loadserver``. - - Like ``loadapp()``, except returns in IServer object. - """ - - -############################################################ -## Factories -############################################################ - - -class IPasteAppFactory(object): - - """ - This is the spec for the ``paste.app_factory`` - protocol/entry_point. - """ - - def __call__(global_conf, **local_conf): - """ - Returns a WSGI application (IWSGIAPP) given the global - configuration and the local configuration passed in as keyword - arguments. - - All keys are strings, but values in local_conf may not be - valid Python identifiers (if you use ``**kw`` you can still - capture these values). - """ - - -class IPasteCompositFactory(object): - - """ - This is the spec for the ``paste.composit_factory`` - protocol/entry_point. - - This also produces WSGI applications, like ``paste.app_factory``, - but is given more access to the context in which it is loaded. - """ - - def __call__(loader, global_conf, **local_conf): - """ - Like IPasteAppFactory this returns a WSGI application - (IWSGIApp). The ``loader`` value conforms to the ``ILoader`` - interface, and can be used to load (contextually) more - applications. - """ - - -class IPasteFilterFactory(object): - - """ - This is the spec for the ``paste.filter_factory`` - protocol/entry_point. - """ - - def __call__(global_conf, **local_conf): - """ - Returns a IFilter object. - """ - - -class IPasteFilterAppFactory(object): - - """ - This is the spec for the ``paste.filter_app_factory`` - protocol/entry_point. - """ - - def __call__(wsgi_app, global_conf, **local_conf): - """ - Returns a WSGI application that wraps ``wsgi_app``. - - Note that paste.deploy creates a wrapper for these - objects that implement the IFilter interface. - """ - - -class IPasteServerFactory(object): - - """ - This is the spec for the ``paste.server_factory`` - protocol/entry_point. - """ - - def __call__(global_conf, **local_conf): - """ - Returns a IServer object. - """ - - -class IPasteServerRunner(object): - - """ - This is the spec for the ``paste.server_runner`` - protocol/entry_point. - """ - - def __call__(wsgi_app, global_conf, **local_conf): - """ - Serves the given WSGI application. May serve once, many - times, forever; nothing about how the server works is - specified here. - - Note that paste.deploy creates a wrapper for these - objects that implement the IServer interface. - """ - - -class ILoader(object): - - """ - This is an object passed into ``IPasteCompositFactory``. It is - currently implemented in ``paste.deploy.loadwsgi`` by - ``ConfigLoader`` and ``EggLoader``. - """ - - def get_app(name_or_uri, global_conf=None): - """ - Return an IWSGIApp object. If the loader supports named - applications, then you can use a simple name; otherwise - you must use a full URI. - - Any global configuration you pass in will be added; you should - generally pass through the global configuration you received. - """ - - def get_filter(name_or_uri, global_conf=None): - """ - Return an IFilter object, like ``get_app``. - """ - - def get_server(name_or_uri, global_conf=None): - """ - Return an IServer object, like ``get_app``. - """ - - -############################################################ -## Objects -############################################################ - - -class IWSGIApp(object): - - """ - This is an application that conforms to `PEP 333 - `_: Python Web Server - Gateway Interface v1.0 - """ - - def __call__(environ, start_response): - """ - Calls ``start_response(status_code, header_list)`` and returns - an iterator for the body of the response. - """ - - -class IFilter(object): - - """ - A filter is a simple case of middleware, where an object - wraps a single WSGI application (IWSGIApp). - """ - - def __call__(wsgi_app): - """ - Returns an IWSGIApp object, typically one that wraps the - ``wsgi_app`` passed in. - """ - - -class IServer(object): - - """ - A simple server interface. - """ - - def __call__(wsgi_app): - """ - Serves the given WSGI application. May serve once, many - times, forever; nothing about how the server works is - specified here. - """ diff --git a/setup.py b/setup.py index d3127a7..f03bc7a 100644 --- a/setup.py +++ b/setup.py @@ -60,14 +60,5 @@ For the latest changes see the `news file [paste.paster_create_template] paste_deploy=paste.deploy.paster_templates:PasteDeploy - - [paste.entry_point_description] - paste.app_factory = paste.deploy.epdesc:AppFactoryDescription - paste.composit_factory = paste.deploy.epdesc:CompositeFactoryDescription - paste.composite_factory = paste.deploy.epdesc:CompositeFactoryDescription - paste.filter_app_factory = paste.deploy.epdesc:FilterAppFactoryDescription - paste.filter_factory = paste.deploy.epdesc:FilterFactoryDescription - paste.server_factory = paste.deploy.epdesc:ServerFactoryDescription - paste.server_runner = paste.deploy.epdesc:ServerRunnerDescription """, ) -- cgit v1.2.1 From 421047cfbc6e0db5e0aae42199c18e45e0ca6678 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 04:16:29 +0300 Subject: PEP 8 cleanups in templates --- .../deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl | 5 +++-- paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl index ae55f97..6df5a6e 100644 --- a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -1,6 +1,8 @@ import cgi + from paste.deploy 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 @@ -19,5 +21,4 @@ def application(environ, start_response): % (key, cgi.escape(repr(value)))) content.append('') return content - - + \ No newline at end of file diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl index 57155fc..97decb8 100644 --- a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -1,6 +1,8 @@ -import sampleapp from paste.deploy.config import ConfigMiddleware +import sampleapp + + def make_app( global_conf, # Optional and required configuration parameters @@ -20,4 +22,3 @@ def make_app( # configuration dictionary we set up: app = ConfigMiddleware(app, conf) return app - -- cgit v1.2.1 From 0e743d9c6f686aa6d65b231c2dcd918129b79e13 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 04:21:19 +0300 Subject: Updated hg ignores --- .hgignore | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.hgignore b/.hgignore index ff958ef..44abbb2 100644 --- a/.hgignore +++ b/.hgignore @@ -1,14 +1,13 @@ syntax: glob *.egg-info/ +*.egg/ +*.pyc +*.class dist/ build/ docs/_build/ - -syntax: glob .tox -syntax: glob .project -syntax: glob .pydevproject -syntax: regexp -^tests/fake_packages/FakeApp\.egg$ \ No newline at end of file +.settings +__pycache__ -- cgit v1.2.1 From 8862d5ea336451ada00a3e5573f77d53a3d01d47 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 04:53:44 +0300 Subject: Shortened the enhanced error message from _interpolate to only include the file name as all other information is already there --- paste/deploy/loadwsgi.py | 6 ++---- tests/test_config.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index fe24add..42aa9b3 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -76,8 +76,7 @@ class NicerConfigParser(ConfigParser): except Exception: e = sys.exc_info()[1] args = list(e.args) - args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( - self.filename, section, option, rawval, e) + args[0] = 'Error in file %s: %s' % (self.filename, e) e.args = tuple(args) e.message = args[0] raise @@ -97,8 +96,7 @@ class NicerConfigParser(ConfigParser): except Exception: e = sys.exc_info()[1] args = list(e.args) - args[0] = 'Error in file %s, [%s] %s=%r: %s' % ( - parser.filename, section, option, value, e) + args[0] = 'Error in file %s: %s' % (parser.filename, e) e.args = tuple(args) e.message = args[0] raise diff --git a/tests/test_config.py b/tests/test_config.py index 6dee066..de40a2a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -168,6 +168,6 @@ def test_interpolate_exception(): except Exception: e = sys.exc_info()[1] expected = "Error in file %s" % os.path.join(config_path, 'test_error.ini') - eq_(str(e).split(',')[0], expected) + eq_(str(e).split(':')[0], expected) else: assert False, 'Should have raised an exception' -- cgit v1.2.1 From 1f966054308e0c1f4fa4070f07a1a97595bba656 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Tue, 24 May 2011 23:41:05 +0300 Subject: Bumped up version to 1.5.0 proper --- docs/conf.py | 2 +- docs/news.txt | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 76beb2f..bd009e9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ copyright = '2011, Ian Bicking and contributors' # The short X.Y version. version = '1.5' # The full version, including alpha/beta/rc tags. -release = '1.5.0dev' +release = '1.5.0' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/news.txt b/docs/news.txt index d89cc76..c534868 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,8 +1,8 @@ Paste Deployment News ===================== -hg tip ------- +1.5.0 +----- * Project is now maintained by Alex Grönholm diff --git a/setup.py b/setup.py index f03bc7a..63e4600 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup( name="PasteDeploy", - version='1.5.0dev', + version='1.5.0', description="Load, configure, and compose WSGI applications and servers", long_description="""\ This tool provides code to load WSGI applications and servers from -- cgit v1.2.1 -- cgit v1.2.1 -- cgit v1.2.1 From 9779d80019829072589da60094a21e26e3a114e6 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Wed, 25 May 2011 00:34:47 +0300 Subject: Fixed typo that prevented the appconfig function from being included in the docs --- docs/modules/loadwsgi.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt index fc3dc4c..feebf28 100644 --- a/docs/modules/loadwsgi.txt +++ b/docs/modules/loadwsgi.txt @@ -9,5 +9,5 @@ Module Contents .. autofunction:: loadapp .. autofunction:: loadserver .. autofunction:: loadfilter -.. autofunction;: appconfig +.. autofunction:: appconfig -- cgit v1.2.1 From ab48ce42feb3cc9fd4cf20eebdd1c5278b75bd19 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Mon, 15 Aug 2011 02:08:54 +0300 Subject: Fixed use of the wrong variable when determining the context protocol --- paste/deploy/loadwsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 42aa9b3..66046c7 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -493,7 +493,7 @@ class ConfigLoader(_Loader): # 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' % context_protocol + context.protocol = 'paste.%s_factory' % section_protocol return context -- cgit v1.2.1 From 9e8069125c369594040b55f6152fb4fab23b9c9b Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Mon, 15 Aug 2011 15:06:50 +0300 Subject: Moved the description to README, switched version to development --- README | 11 +++++++++++ docs/conf.py | 2 +- setup.py | 62 ++++++++++++++++++++++++------------------------------------ 3 files changed, 37 insertions(+), 38 deletions(-) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..291a026 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). + +For the latest changes see the `news file +`_. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index bd009e9..49aed08 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ copyright = '2011, Ian Bicking and contributors' # The short X.Y version. version = '1.5' # The full version, including alpha/beta/rc tags. -release = '1.5.0' +release = '1.5.1.dev1' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/setup.py b/setup.py index 63e4600..f53a561 100644 --- a/setup.py +++ b/setup.py @@ -2,46 +2,34 @@ from setuptools import setup, find_packages setup( - name="PasteDeploy", - version='1.5.0', - description="Load, configure, and compose WSGI applications and servers", - long_description="""\ -This tool provides code to load WSGI applications and servers from -URIs; these URIs can refer to Python Eggs for INI-style configuration -files. `Paste Script `_ provides -commands to serve applications based on this configuration file. - -The latest version is available in a `Mercurial repository -`_ (or a `tarball -`_). - -For the latest changes see the `news file -`_. -""", + name='PasteDeploy', + version='1.5.1.dev1', + description='Load, configure, and compose WSGI applications and servers', + long_description=open('README').read(), classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 2.5", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.1", - "Programming Language :: Python :: 3.2", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - "Topic :: Software Development :: Libraries :: Python Modules", - "Framework :: Paste", + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Paste', ], keywords='web wsgi application server', - author="Ian Bicking", - author_email="ianb@colorstudy.com", - maintainer="Alex Gronholm", - maintainer_email="alex.gronholm@nextday.fi", - url="http://pythonpaste.org/deploy/", + author='Ian Bicking', + author_email='ianb@colorstudy.com', + maintainer='Alex Gronholm', + maintainer_email='alex.gronholm@nextday.fi', + url='http://pythonpaste.org/deploy/', license='MIT', namespace_packages=['paste'], packages=find_packages(exclude=['tests']), -- cgit v1.2.1 From fb6b42484b11b5c978af912d014b9306664f4b3f Mon Sep 17 00:00:00 2001 From: Alexandre Conrad Date: Sat, 3 Dec 2011 18:40:08 -0800 Subject: use frozenset for asbool() lookup --- paste/deploy/converters.py | 8 ++++++-- tests/test_converters.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/test_converters.py diff --git a/paste/deploy/converters.py b/paste/deploy/converters.py index f37d267..c9d87de 100644 --- a/paste/deploy/converters.py +++ b/paste/deploy/converters.py @@ -3,12 +3,16 @@ from paste.deploy.compat import basestring +truthy = frozenset(['true', 'yes', 'on', 'y', 't', '1']) +falsy = frozenset(['false', 'no', 'off', 'n', 'f', '0']) + + def asbool(obj): if isinstance(obj, basestring): obj = obj.strip().lower() - if obj in ['true', 'yes', 'on', 'y', 't', '1']: + if obj in truthy: return True - elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + elif obj in falsy: return False else: raise ValueError("String is not true/false: %r" % obj) diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 0000000..5361310 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,17 @@ +def test_asbool_truthy(): + from paste.deploy.converters import asbool + assert asbool('true') + assert asbool('yes') + assert asbool('on') + assert asbool('y') + assert asbool('t') + assert asbool('1') + +def test_asbool_falsy(): + from paste.deploy.converters import asbool + assert not asbool('false') + assert not asbool('no') + assert not asbool('off') + assert not asbool('n') + assert not asbool('f') + assert not asbool('0') -- cgit v1.2.1 From dfcac9a752289ed00567f71c92b060376a4eb809 Mon Sep 17 00:00:00 2001 From: Atsushi Odagiri Date: Tue, 7 Feb 2012 06:46:00 -0800 Subject: fix template bug --- .hgignore | 13 + MANIFEST.in | 2 + README | 11 + docs/_static/paste.css | 15 + docs/_templates/layout.html | 20 + docs/conf.py | 132 ++++ docs/index.txt | 672 +++++++++++++++++++ docs/license.txt | 20 + docs/modules/config.txt | 13 + docs/modules/converters.txt | 11 + docs/modules/loadwsgi.txt | 13 + docs/news.txt | 159 +++++ paste/__init__.py | 18 + paste/deploy/__init__.py | 3 + paste/deploy/compat.py | 30 + paste/deploy/config.py | 305 +++++++++ paste/deploy/converters.py | 36 + paste/deploy/loadwsgi.py | 725 +++++++++++++++++++++ paste/deploy/paster_templates.py | 36 + .../paste_deploy/+package+/sampleapp.py_tmpl | 24 + .../paste_deploy/+package+/wsgiapp.py_tmpl | 24 + .../paste_deploy/docs/devel_config.ini_tmpl | 22 + paste/deploy/util.py | 74 +++ regen-docs | 9 + setup.py | 52 ++ tests/__init__.py | 13 + .../FakeApp.egg/FakeApp.egg-info/PKG-INFO | 10 + .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 22 + .../FakeApp.egg/FakeApp.egg-info/top_level.txt | 1 + .../fake_packages/FakeApp.egg/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 69 ++ .../FakeApp.egg/fakeapp/configapps.py | 14 + tests/fake_packages/FakeApp.egg/setup.py | 23 + tests/fixture.py | 20 + tests/sample_configs/basic_app.ini | 14 + tests/sample_configs/executable.ini | 10 + tests/sample_configs/test_config.ini | 38 ++ tests/sample_configs/test_config_included.ini | 10 + tests/sample_configs/test_error.ini | 8 + tests/sample_configs/test_filter.ini | 22 + tests/sample_configs/test_filter_with.ini | 12 + tests/sample_configs/test_func.ini | 13 + tests/test_basic_app.py | 36 + tests/test_config.py | 173 +++++ tests/test_config_middleware.py | 28 + tests/test_filter.py | 53 ++ tests/test_load_package.py | 12 + tox.ini | 20 + 48 files changed, 3061 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 docs/_static/paste.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/conf.py create mode 100644 docs/index.txt create mode 100644 docs/license.txt create mode 100644 docs/modules/config.txt create mode 100644 docs/modules/converters.txt create mode 100644 docs/modules/loadwsgi.txt create mode 100644 docs/news.txt create mode 100644 paste/__init__.py create mode 100644 paste/deploy/__init__.py create mode 100644 paste/deploy/compat.py create mode 100644 paste/deploy/config.py create mode 100644 paste/deploy/converters.py create mode 100644 paste/deploy/loadwsgi.py create mode 100644 paste/deploy/paster_templates.py create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl create mode 100644 paste/deploy/util.py create mode 100755 regen-docs create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/apps.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/configapps.py create mode 100644 tests/fake_packages/FakeApp.egg/setup.py create mode 100644 tests/fixture.py create mode 100644 tests/sample_configs/basic_app.ini create mode 100755 tests/sample_configs/executable.ini create mode 100644 tests/sample_configs/test_config.ini create mode 100644 tests/sample_configs/test_config_included.ini create mode 100644 tests/sample_configs/test_error.ini create mode 100644 tests/sample_configs/test_filter.ini create mode 100644 tests/sample_configs/test_filter_with.ini create mode 100644 tests/sample_configs/test_func.ini create mode 100644 tests/test_basic_app.py create mode 100644 tests/test_config.py create mode 100644 tests/test_config_middleware.py create mode 100644 tests/test_filter.py create mode 100644 tests/test_load_package.py create mode 100644 tox.ini diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..44abbb2 --- /dev/null +++ b/.hgignore @@ -0,0 +1,13 @@ +syntax: glob +*.egg-info/ +*.egg/ +*.pyc +*.class +dist/ +build/ +docs/_build/ +.tox +.project +.pydevproject +.settings +__pycache__ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7a2ffb9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include docs/*.txt +recursive-include paste/deploy/paster_templates * diff --git a/README b/README new file mode 100644 index 0000000..291a026 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). + +For the latest changes see the `news file +`_. \ No newline at end of file diff --git a/docs/_static/paste.css b/docs/_static/paste.css new file mode 100644 index 0000000..6705e5d --- /dev/null +++ b/docs/_static/paste.css @@ -0,0 +1,15 @@ +a.invisible-link { + color: #fff; + text-decoration: none; +} + +a.invisible-link:visited { + color: #fff; + text-decoration: none; +} + +a.invisible:link { + color: #fff; + text-decoration: none; +} + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..a6d2a97 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,20 @@ +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block sidebartoc %} +

Python Paste

+ + + +{{ super() }} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..49aed08 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Paste Deploy' +copyright = '2011, Ian Bicking and contributors' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '1.5' +# The full version, including alpha/beta/rc tags. +release = '1.5.1.dev1' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = ['include/contact.txt', 'include/reference_header.txt'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PasteDeploydoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..a1678fa --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,672 @@ +Paste Deployment +================ + +:author: Ian Bicking + +.. contents:: + +Documents: + +.. toctree:: + :maxdepth: 1 + + news + modules/loadwsgi + modules/config + modules/converters + license + +.. comment: + The names used in sections should be more concrete, and it should + be clearer that they are just arbitrary names. + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +To see updates that have been made to Paste Deploy see the `news file +`_. + +Paste Deploy is released under the `MIT license +`_. + +Status +------ + +Paste Deploy has passed version 1.0. Paste Script is an actively +maintained project. As of 1.0, we'll make a strong effort to maintain +backward compatibility (this actually started happening long before +1.0, but now it is explicit). This will include deprecation warnings +when necessary. Major changes will take place under new functions or +with new entry points. + +Note that the most key aspect of Paste Deploy is the entry points it +defines (such as ``paste.app_factory``). Paste Deploy is not the only +consumer of these entry points, and many extensions can best take +place by utilizing the entry points instead of using Paste Deploy +directly. The entry points will not change; if changes are necessary, +new entry points will be defined. + +Installation +------------ + +First make sure you have either +`setuptools `_ or its +modern replacement +`distribute `_ installed. +For Python 3.x you need distribute as setuptools does not work on it. + +Then you can install Paste Deployment using `pip +`_ by running:: + + $ sudo pip install PasteDeploy + +If you want to track development, do:: + + $ hg clone http://bitbucket.org/ianb/pastedeploy + $ cd pastedeploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. You can also simply install ``PasteDeploy==dev``. + +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + +A complimentary package is `Paste Script `_. To install +that, use ``pip install PasteScript`` (or ``pip install +PasteScript==dev``). + +From the User Perspective +------------------------- + +In the following sections, the Python API for using Paste Deploy is +given. This isn't what users will be using (but it is useful for +Python developers and useful for setting up tests fixtures). + +The primary interaction with Paste Deploy is through its configuration +files. The primary thing you want to do with a configuration file is +serve it. To learn about serving configuration files, see `the +``paster serve`` command +`_. + +The Config File +~~~~~~~~~~~~~~~ + +A config file has different sections. The only sections Paste Deploy +cares about have prefixes, like ``app:main`` or ``filter:errors`` -- +the part after the ``:`` is the "name" of the section, and the part +before gives the "type". Other sections are ignored. + +The format is a simple `INI format +`_: ``name = value``. You can +extend the value by indenting subsequent lines. ``#`` is a comment. + +Typically you have one or two sections, named "main": an application +section (``[app:main]``) and a server section (``[server:main]``). +``[composite:...]`` signifies something that dispatches to multiple +applications (example below). + +Here's a typical configuration file that also shows off mounting +multiple applications using `paste.urlmap +`_:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /wiki = wiki + /cms = config:cms.ini + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +I'll explain each section in detail now:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + +That this is a ``composite`` section means it dispatches the request +to other applications. ``use = egg:Paste#urlmap`` means to use the +composite application named ``urlmap`` from the ``Paste`` package. +``urlmap`` is a particularly common composite application -- it uses a +path prefix to map your request to another application. These are +the applications like "home", "blog", "wiki" and "config:cms.ini". The last +one just refers to another file ``cms.ini`` in the same directory. + +Next up:: + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + +``egg:Paste#static`` is another simple application, in this case it +just serves up non-dynamic files. It takes one bit of configuration: +``document_root``. You can use variable substitution, which will pull +variables from the section ``[DEFAULT]`` (case sensitive!) with +markers like ``%(var_name)s``. The special variable ``%(here)s`` is +the directory containing the configuration file; you should use that +in lieu of relative filenames (which depend on the current directory, +which can change depending how the server is run). + +Then:: + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +The ``[filter-app:blog]`` section means that you want an application +with a filter applied. The application being filtered is indicated +with ``next`` (which refers to the next section). The +``egg:Authentication#auth`` filter doesn't actually exist, but one +could imagine it logs people in and checks permissions. + +That last section is just a reference to an application that you +probably installed with ``pip install BlogApp``, and one bit of +configuration you passed to it (``database``). + +Lastly:: + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +This section is similar to the previous one, with one important difference. +Instead of an entry point in an egg, it refers directly to the ``application`` +variable in the ``mywiki.main`` module. The reference consist of two parts, +separated by a colon. The left part is the full name of the module and the +right part is the path to the variable, as a Python expression relative to the +containing module. + +So, that's most of the features you'll use. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or a callable from a module: + [app:mythirdapp] + use = call:my.project:myapplication + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composite Applications +~~~~~~~~~~~~~~~~~~~~~~ + +"Composite" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composite:main] + use = egg:Paste#urlmap + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composite application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +Filter Composition +~~~~~~~~~~~~~~~~~~ + +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + # and you could have another filter-with here, and so on... + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + + [filter:filter1] + ... + +Getting Configuration +~~~~~~~~~~~~~~~~~~~~~ + +If you want to get the configuration without creating the application, +you can use the ``appconfig(uri)`` function, which is just like the +``loadapp()`` function except it returns the configuration that would +be used, as a dictionary. Both global and local configuration is +combined into a single dictionary, but you can look at just one or the +other with the attributes ``.local_conf`` and ``.global_conf``. + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ and +`distribute `_ that adds metadata to a +normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory``, +``paste.composite_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composite_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composites are just slightly more complex:: + + def composite_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composite factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composite:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.filter_app_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + +``paste.server_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +The implementation of ``Server`` is left to the user. + +``paste.server_runner`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + +Outstanding Issues +------------------ + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory`` and ``paste.composite_factory`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + diff --git a/docs/license.txt b/docs/license.txt new file mode 100644 index 0000000..c810dec --- /dev/null +++ b/docs/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2006-2007 Ian Bicking and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/modules/config.txt b/docs/modules/config.txt new file mode 100644 index 0000000..9d8f894 --- /dev/null +++ b/docs/modules/config.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.config` -- Configuration and Environment middleware +====================================================================== + +.. automodule:: paste.deploy.config + +Module Contents +--------------- + +.. autoclass:: DispatchingConfig +.. autoclass:: ConfigMiddleware +.. autoclass:: PrefixMiddleware + +.. comment: FIXME: do something about CONFIG (manual docs?) diff --git a/docs/modules/converters.txt b/docs/modules/converters.txt new file mode 100644 index 0000000..4948104 --- /dev/null +++ b/docs/modules/converters.txt @@ -0,0 +1,11 @@ +:mod:`paste.deploy.converters` -- Conversion helpers for String Configuration +============================================================================= + +.. automodule:: paste.deploy.converters + +Module Contents +--------------- + +.. autofunction:: asbool +.. autofunction:: asint +.. autofunction:: aslist diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt new file mode 100644 index 0000000..feebf28 --- /dev/null +++ b/docs/modules/loadwsgi.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.loadwsgi` -- Load WSGI applications from config files +======================================================================== + +.. automodule:: paste.deploy.loadwsgi + +Module Contents +--------------- + +.. autofunction:: loadapp +.. autofunction:: loadserver +.. autofunction:: loadfilter +.. autofunction:: appconfig + diff --git a/docs/news.txt b/docs/news.txt new file mode 100644 index 0000000..c534868 --- /dev/null +++ b/docs/news.txt @@ -0,0 +1,159 @@ +Paste Deployment News +===================== + +1.5.0 +----- + +* Project is now maintained by Alex Grönholm + +* Was printing extraneous data when calling setup.py + +* Fixed missing paster template files (fixes "paster create -t paste.deploy") + +* Excluded tests from release distributions + +* Added support for the "call:" protocol for loading apps directly as + functions (contributed by Jason Stitt) + +* Added Python 3.x support + +* Dropped Python 2.4 support + +* Removed the ``paste.deploy.epdesc`` and ``paste.deploy.interfaces`` modules + -- contact the maintainer if you actually needed them + +1.3.4 +----- + +* Fix loadconfig path handling on Jython on Windows. + +1.3.3 +----- + +* In :class:`paste.deploy.config.PrefixMiddleware` the headers + ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated + to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` + is translated to ``environ['REMOTE_ADDR']`` + +* Also in PrefixMiddleware, if X-Forwarded-Host has multiple + (comma-separated) values, use only the first value. + +1.3.2 +----- + +* Added ``paste.deploy.converters.asint()``. +* fixed use sections overwriting the config's __file__ value with the + use'd filename. +* ``paste.deploy.loadwsgi`` now supports variable expansion in the + DEFAULT section of config files (unlike plain ConfigParser). + +1.3.1 +----- + +* Fix ``appconfig`` config loading when using a config file with + ``filter-with`` in it (previously you'd get TypeError: iteration + over non-sequence) + +1.3 +--- + +* Added ``scheme`` option to ``PrefixMiddleware``, so you can force a + scheme (E.g., when proxying an HTTPS connection over HTTP). + +* Pop proper values into ``environ['paste.config']`` in + ``ConfigMiddleware``. + +1.1 +--- + +* Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other + loaders) will show up as though they were in ``[DEFAULT]``, so they + can be used in variable interpolation. Note: this won't overwrite + any existing values in ``[DEFAULT]``. + +* Added ``force_port`` option to + ``paste.deploy.config.PrefixMiddleware``. Also the ``prefix`` + argument is stripped of any trailing ``/``, which can't be valid in + that position. + +1.0 +--- + +* Added some documentation for the different kinds of entry points + Paste Deploy uses. + +* Added a feature to ``PrefixMiddleware`` that translates the + ``X-Forwarded-Server`` header to ``Host``. + +0.9.6 +----- + +* Added ``PrefixMiddleware`` which compensates for cases where the + wsgi app is behind a proxy of some sort that isn't moving the prefix + into the SCRIPT_NAME in advance. + +* Changed _loadconfig() so that it works with Windows absolute paths. + +* Make the error messages prettier when you call a function and fail + to give an argument, like a required function argument. + +0.5 +--- + +* Made the ``paste_deploy`` template (used with ``paster create + --template=paste_deploy``) more useful, with an example application + and entry point. + +0.4 +--- + +* Allow filters to have ``filter-with`` values, just like + applications. + +* Renamed ``composit`` to ``composite`` (old names still work, but + aren't documented). + +* Added ``appconfig()`` to load along with ``loadapp()``, but return + the configuration without invoking the application. + +0.3 +--- + +* Allow variable setting like:: + + get local_var = global_var_name + + To bring in global variables to the local scope. + +* Allow interpolation in files, like ``%(here)s``. Anything in the + ``[DEFAULTS]`` section will be available to substitute into a value, + as will variables in the same section. Also, the special value + ``here`` will be the directory the configuration file is located in. + +0.2 +--- + +Released 26 August 2004 + +* Added a ``filter-with`` setting to applications. + +* Removed the ``1`` from all the protocol names (e.g., + ``paste.app_factory1`` is not ``paste.app_factory``). + +* Added ``filter-app:`` and ``pipeline:`` sections. `Docs + `__. + +* Added ``paste.filter_app_factory1`` (`doc + `__) and + ``paste.server_runner1`` (`doc + `__) protocols. + +* Added ``paste.deploy.converters`` module for handling the + string values that are common with this system. + +0.1 +--- + +Released 22 August 2004 + +Initial version released. It's all new. diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..cdb6121 --- /dev/null +++ b/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/paste/deploy/__init__.py b/paste/deploy/__init__.py new file mode 100644 index 0000000..94c63a8 --- /dev/null +++ b/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/paste/deploy/compat.py b/paste/deploy/compat.py new file mode 100644 index 0000000..f7b93f5 --- /dev/null +++ b/paste/deploy/compat.py @@ -0,0 +1,30 @@ +# (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 +"""Python 2<->3 compatibility module""" +import sys + + +def print_(template, *args, **kwargs): + template = str(template) + if args: + template = template % args + elif kwargs: + template = template % kwargs + sys.stdout.writelines(template) + +if sys.version_info < (3, 0): + basestring = basestring + from ConfigParser import ConfigParser + from urllib import unquote + iteritems = lambda d: d.iteritems() + + def reraise(t, e, tb): + exec('raise t, e, tb', dict(t=t, e=e, tb=tb)) +else: + basestring = str + from configparser import ConfigParser + from urllib.parse import unquote + iteritems = lambda d: d.items() + + def reraise(t, e, tb): + exec('raise e from tb', dict(e=e, tb=tb)) diff --git a/paste/deploy/config.py b/paste/deploy/config.py new file mode 100644 index 0000000..1db5680 --- /dev/null +++ b/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(object): + + """ + 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(object): + + """ + 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(object): + """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') + 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 = '%s:%s' % (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 new file mode 100644 index 0000000..f37d267 --- /dev/null +++ b/paste/deploy/converters.py @@ -0,0 +1,36 @@ +# (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.compat import basestring + + +def asbool(obj): + if isinstance(obj, basestring): + obj = obj.strip().lower() + if obj in ['true', 'yes', 'on', 'y', 't', '1']: + return True + elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + 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, basestring): + 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 new file mode 100644 index 0000000..66046c7 --- /dev/null +++ b/paste/deploy/loadwsgi.py @@ -0,0 +1,725 @@ +# (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 __future__ import with_statement +import os +import sys +import re + +import pkg_resources + +from paste.deploy.compat import ConfigParser, unquote, iteritems +from paste.deploy.util import fix_call, lookup_object + +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + + +############################################################ +## Utility functions +############################################################ + + +def import_string(s): + return pkg_resources.EntryPoint.parse("x=" + s).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 + if hasattr(self, '_interpolation'): + self._interpolation = self.InterpolateWrapper(self._interpolation) + + read_file = getattr(ConfigParser, 'read_file', ConfigParser.readfp) + + 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 iteritems(defaults): + defaults[key] = self.get('DEFAULT', key) or val + return defaults + + def _interpolate(self, section, option, rawval, vars): + # Python < 3.2 + try: + return ConfigParser._interpolate( + self, section, option, rawval, vars) + except Exception: + e = sys.exc_info()[1] + args = list(e.args) + args[0] = 'Error in file %s: %s' % (self.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + class InterpolateWrapper(object): + # Python >= 3.2 + 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] = 'Error in file %s: %s' % (parser.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + +############################################################ +## Object types +############################################################ + + +class _ObjectType(object): + + 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 '<%s protocols=%r prefixes=%r>' % ( + 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(object): + + 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 iteritems(new_defaults): + 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) + if global_conf is None: + global_conf = {} + else: + global_conf = global_conf.copy() + defaults = self.parser.defaults() + global_conf.update(defaults) + 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" + % (', '.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([ + (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(object): + + 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 new file mode 100644 index 0000000..9c5f942 --- /dev/null +++ b/paste/deploy/paster_templates.py @@ -0,0 +1,36 @@ +# (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 + +from paste.deploy.compat import print_ + + +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 new file mode 100644 index 0000000..6df5a6e --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -0,0 +1,24 @@ +import cgi + +from paste.deploy 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 = [ + '%s\n' % greeting, + '

%s!

\n' % greeting, + '\n', + ] + items = environ.items() + items.sort() + for key, value in items: + content.append('\n' + % (key, cgi.escape(repr(value)))) + content.append('
%s%s
') + return content + \ No newline at end of file diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl new file mode 100644 index 0000000..97decb8 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,24 @@ +from paste.deploy.config import ConfigMiddleware + +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 new file mode 100644 index 0000000..0c0ae35 --- /dev/null +++ b/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/paste/deploy/util.py b/paste/deploy/util.py new file mode 100644 index 0000000..02d3fa2 --- /dev/null +++ b/paste/deploy/util.py @@ -0,0 +1,74 @@ +# (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 + +from paste.deploy.compat import reraise + + +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 = kwargs.items() + kwargs.sort() + args += ', '.join(['%s=...' % n for n, v in kwargs]) + gotspec = '(%s)' % args + msg = '%s; got %s, wanted %s' % (exc_info[1], gotspec, 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) + reraise(*exc_info) + 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/regen-docs b/regen-docs new file mode 100755 index 0000000..f8dad75 --- /dev/null +++ b/regen-docs @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + scp -r _build/* ianb@webwareforpython.org:/home/paste/htdocs/deploy/ +fi diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f53a561 --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +from setuptools import setup, find_packages + + +setup( + name='PasteDeploy', + version='1.5.1.dev1', + description='Load, configure, and compose WSGI applications and servers', + long_description=open('README').read(), + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Paste', + ], + keywords='web wsgi application server', + author='Ian Bicking', + author_email='ianb@colorstudy.com', + maintainer='Alex Gronholm', + maintainer_email='alex.gronholm@nextday.fi', + url='http://pythonpaste.org/deploy/', + license='MIT', + namespace_packages=['paste'], + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + test_suite='nose.collector', + tests_require=['nose>=0.11'], + extras_require={ + 'Config': [], + 'Paste': ['Paste'], + }, + entry_points=""" + [paste.filter_app_factory] + config = paste.deploy.config:make_config_filter [Config] + prefix = paste.deploy.config:make_prefix_middleware + + [paste.paster_create_template] + paste_deploy=paste.deploy.paster_templates:PasteDeploy + """, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..cffe526 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +import os +import sys + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +sys.path.insert(0, base) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +pkg_resources.require('PasteDeploy') diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt new file mode 100644 index 0000000..9bfc986 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -0,0 +1,22 @@ +[paste.app_factory] + + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + + +[paste.composit_factory] + + remote_addr=fakeapp.apps:make_remote_addr + + +[paste.filter_app_factory] + + caps2=fakeapp.apps:CapFilter + + +[paste.filter_factory] + + caps=fakeapp.apps:make_cap_filter + + diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py new file mode 100644 index 0000000..cae7eba --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -0,0 +1,69 @@ +############################################################ +## Apps +############################################################ + +def simple_app(response, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['This is ', response] + +def basic_app(environ, start_response): + return simple_app('basic app', environ, start_response) + +def make_basic_app(global_conf, **conf): + return basic_app + +def basic_app2(environ, start_response): + return simple_app('basic app2', environ, start_response) + +def make_basic_app2(global_conf, **conf): + return basic_app2 + +############################################################ +## Composits +############################################################ + +def make_remote_addr(loader, global_conf, **conf): + apps = {} + addrs = {} + for name, value in conf.items(): + if name.startswith('app.'): + apps[name[4:]] = loader.get_app(value, global_conf) + elif name.startswith('addr.'): + addrs[name[5:]] = value + dispatcher = RemoteAddrDispatch() + for name in apps: + dispatcher.map[addrs[name]] = apps[name] + return dispatcher + +class RemoteAddrDispatch(object): + def __init__(self, map=None): + self.map = map or {} + + def __call__(self, environ, start_response): + addr = environ['REMOTE_ADDR'] + app = self.map.get(addr) or self.map['0.0.0.0'] + return app(environ, start_response) + +############################################################ +## Filters +############################################################ + +def make_cap_filter(global_conf, method_to_call='upper'): + def cap_filter(app): + return CapFilter(app, global_conf, method_to_call) + return cap_filter + +class CapFilter(object): + + def __init__(self, app, global_conf, method_to_call='upper'): + self.app = app + self.method_to_call = method_to_call + self.global_conf = global_conf + + def __call__(self, environ, start_response): + app_iter = self.app(environ, start_response) + for item in app_iter: + yield getattr(item, self.method_to_call)() + if hasattr(app_iter, 'close'): + app_iter.close() + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py new file mode 100644 index 0000000..ef13182 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py @@ -0,0 +1,14 @@ +class SimpleApp(object): + def __init__(self, global_conf, local_conf, name): + self.global_conf = global_conf + self.local_conf = local_conf + self.name = name + + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['I am: ', name] + + def make_app(cls, global_conf, **conf): + return cls(global_conf, conf, 'basic') + make_app = classmethod(make_app) + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py new file mode 100644 index 0000000..854483e --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + entry_points={ + 'paste.app_factory': """ + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + """, + 'paste.composit_factory': """ + remote_addr=fakeapp.apps:make_remote_addr + """, + 'paste.filter_factory': """ + caps=fakeapp.apps:make_cap_filter + """, + 'paste.filter_app_factory': """ + caps2=fakeapp.apps:CapFilter + """, + }, + ) diff --git a/tests/fixture.py b/tests/fixture.py new file mode 100644 index 0000000..751659d --- /dev/null +++ b/tests/fixture.py @@ -0,0 +1,20 @@ +import os +import sys +import shutil + +test_dir = os.path.dirname(__file__) +egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'EGG-INFO') +info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'FakeApp.egg-info') +if not os.path.exists(egg_info_dir): + try: + os.symlink(info_dir, egg_info_dir) + except: + shutil.copytree(info_dir, egg_info_dir) + +sys.path.append(os.path.dirname(egg_info_dir)) + +from pkg_resources import * +working_set.add_entry(os.path.dirname(egg_info_dir)) +require('FakeApp') diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini new file mode 100644 index 0000000..f1d931c --- /dev/null +++ b/tests/sample_configs/basic_app.ini @@ -0,0 +1,14 @@ +[application:main] +use = egg:FakeApp#basic_app + +[application:other] +use = egg:FakeApp#other + +[composit:remote_addr] +use = egg:FakeApp#remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 + diff --git a/tests/sample_configs/executable.ini b/tests/sample_configs/executable.ini new file mode 100755 index 0000000..3b75fe9 --- /dev/null +++ b/tests/sample_configs/executable.ini @@ -0,0 +1,10 @@ +#!/usr/bin/env paster +[exe] +sys.path = /fake/path/ + /another/fake/path ../fake_packages/ + +[server] +use = egg:PasteScript#cgi + +[app] +use = egg:FakeApp#basic_app diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini new file mode 100644 index 0000000..d614829 --- /dev/null +++ b/tests/sample_configs/test_config.ini @@ -0,0 +1,38 @@ +[DEFAULT] +def1 = a +def2 = b +basepath = %(here)s + +[app:test1] +use = egg:FakeApp#configed +setting1 = foo +setting2 = bar +apppath = %(basepath)s/app + +[app:test2] +use = egg:FakeApp#configed +set def1 = test2 +set another = TEST +local conf = something + +[app:test3] +use = test2 +set def1 = test3 +another = something more + across several + lines + +[app:test_foreign_config] +use = config:test_config_included.ini +set glob = override +another = FOO + +[app:test_get] +use = egg:FakeApp#configed +set def2 = TEST +get def1 = def1 +get foo = def2 + +[app:test_global_conf] +use = egg:FakeApp#configed +test_interp = this:%(inherit)s diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini new file mode 100644 index 0000000..cc0da7a --- /dev/null +++ b/tests/sample_configs/test_config_included.ini @@ -0,0 +1,10 @@ +[DEFAULT] +def2 = from include +def3 = c + +[app:main] +# Equivalent to the egg reference, but just for kicks... +paste.app_factory = fakeapp.configapps:SimpleApp.make_app +set glob = orig +bob = your uncle +another = BAR diff --git a/tests/sample_configs/test_error.ini b/tests/sample_configs/test_error.ini new file mode 100644 index 0000000..b6ad5b2 --- /dev/null +++ b/tests/sample_configs/test_error.ini @@ -0,0 +1,8 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:main] +use = egg:FakeApp#configed +setting1 = foo +setting2 = %(does_not_exist)s/bar diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini new file mode 100644 index 0000000..bfad8dc --- /dev/null +++ b/tests/sample_configs/test_filter.ini @@ -0,0 +1,22 @@ +[app:normal] +use = egg:FakeApp#basic_app + +[pipeline:piped] +pipeline = egg:FakeApp#caps normal + +[filter-app:filt] +use = egg:FakeApp#caps +method_to_call = lower +next = normal + +[pipeline:piped2] +pipeline = egg:FakeApp#caps2 normal + +[filter-app:filt2] +use = egg:FakeApp#caps2 +method_to_call = lower +next = normal + +[app:inv] +use = egg:FakeApp#basic_app +filter-with = egg:FakeApp#caps diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini new file mode 100644 index 0000000..118804f --- /dev/null +++ b/tests/sample_configs/test_filter_with.ini @@ -0,0 +1,12 @@ +[app:main] +use = egg:FakeApp#basic_app +example = test +filter-with = filter1 + +[filter:filter1] +use = egg:FakeApp#caps +filter-with = filter2 + +[filter:filter2] +use = egg:FakeApp#caps + diff --git a/tests/sample_configs/test_func.ini b/tests/sample_configs/test_func.ini new file mode 100644 index 0000000..a0d28c4 --- /dev/null +++ b/tests/sample_configs/test_func.ini @@ -0,0 +1,13 @@ +[application:main] +use = call:fakeapp.apps:make_basic_app + +[application:other] +use = call:fakeapp.apps:make_basic_app2 + +[composit:remote_addr] +use = call:fakeapp.apps:make_remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 \ No newline at end of file diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py new file mode 100644 index 0000000..1ddb52b --- /dev/null +++ b/tests/test_basic_app.py @@ -0,0 +1,36 @@ +from paste.deploy import loadapp + +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_main(): + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#ignored', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:sample_configs/basic_app.ini#other', + relative_to=here) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:sample_configs/basic_app.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..de40a2a --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,173 @@ +from nose.tools import eq_ + +from paste.deploy import loadapp, appconfig +from tests.fixture import * +import fakeapp.configapps as fc +import fakeapp.apps + + +ini_file = 'config:sample_configs/test_config.ini' +here = os.path.dirname(__file__) +config_path = os.path.join(here, 'sample_configs') +config_filename = os.path.join(config_path, 'test_config.ini') + + +def test_config_egg(): + app = loadapp('egg:FakeApp#configed') + assert isinstance(app, fc.SimpleApp) + + +def test_config1(): + app = loadapp(ini_file, relative_to=here, name='test1') + eq_(app.local_conf, { + 'setting1': 'foo', + 'setting2': 'bar', + 'apppath': os.path.join(config_path, 'app')}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'b', + 'basepath': config_path, + 'here': config_path, + '__file__': config_filename}) + + +def test_config2(): + app = loadapp(ini_file, relative_to=here, name='test2') + eq_(app.local_conf, { + 'local conf': 'something'}) + eq_(app.global_conf, { + 'def1': 'test2', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + # Run this to make sure the global-conf-modified test2 + # didn't mess up the general global conf + test_config1() + + +def test_config3(): + app = loadapp(ini_file, relative_to=here, name='test3') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'local conf': 'something', + 'another': 'something more\nacross several\nlines'}) + eq_(app.global_conf, { + 'def1': 'test3', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + test_config2() + + +def test_main(): + app = loadapp('config:test_func.ini', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#main', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#ignored', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:test_func.ini#other', relative_to=config_path) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:test_func.ini#remote_addr', relative_to=config_path) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 + + +def test_foreign_config(): + app = loadapp(ini_file, relative_to=here, name='test_foreign_config') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'another': 'FOO', + 'bob': 'your uncle'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'from include', + 'def3': 'c', + 'basepath': config_path, + 'glob': 'override', + 'here': config_path, + '__file__': os.path.join(config_path, 'test_config.ini')}) + + +def test_config_get(): + app = loadapp(ini_file, relative_to=here, name='test_get') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig(): + conf = appconfig(ini_file, relative_to=here, name='test_get') + eq_(conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename, + 'foo': 'TEST'}) + eq_(conf.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(conf.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig_filter_with(): + conf = appconfig('config:test_filter_with.ini', relative_to=config_path) + eq_(conf['example'], 'test') + + +def test_global_conf(): + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', + global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + eq_(conf, { + 'def1': 'a', + # Note that this gets overwritten: + 'def2': 'b', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + 'inherit': 'bazbar', + '__file__': config_filename, + 'test_interp': 'this:bazbar', + }) + eq_(conf.local_conf, { + 'test_interp': 'this:bazbar'}) + + +def test_interpolate_exception(): + try: + appconfig('config:test_error.ini', relative_to=config_path) + except Exception: + e = sys.exc_info()[1] + expected = "Error in file %s" % os.path.join(config_path, 'test_error.ini') + eq_(str(e).split(':')[0], expected) + else: + assert False, 'Should have raised an exception' diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py new file mode 100644 index 0000000..cc315e3 --- /dev/null +++ b/tests/test_config_middleware.py @@ -0,0 +1,28 @@ +from nose.tools import assert_raises +from nose.plugins.skip import SkipTest + +from paste.deploy.config import ConfigMiddleware + + +class Bug(Exception): + pass + + +def app_with_exception(environ, start_response): + def cont(): + yield "something" + raise Bug + start_response('200 OK', [('Content-type', 'text/html')]) + return cont() + + +def test_error(): + # This import is conditional due to Paste not yet working on py3k + try: + from paste.fixture import TestApp + except ImportError: + raise SkipTest + + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) + test_app = TestApp(wrapped) + assert_raises(Bug, test_app.get, '/') diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..a76af7c --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,53 @@ +from paste.deploy import loadapp +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_filter_app(): + app = loadapp('config:sample_configs/test_filter.ini#filt', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline(): + app = loadapp('config:sample_configs/test_filter.ini#piped', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app2(): + app = loadapp('config:sample_configs/test_filter.ini#filt2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline2(): + app = loadapp('config:sample_configs/test_filter.ini#piped2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app_inverted(): + app = loadapp('config:sample_configs/test_filter.ini#inv', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + + +def test_filter_with_filter_with(): + app = loadapp('config:sample_configs/test_filter_with.ini', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert isinstance(app.app, fakeapp.apps.CapFilter) + assert app.app.app is fakeapp.apps.basic_app diff --git a/tests/test_load_package.py b/tests/test_load_package.py new file mode 100644 index 0000000..b3fea55 --- /dev/null +++ b/tests/test_load_package.py @@ -0,0 +1,12 @@ +from pprint import pprint +import sys + +import pkg_resources + +from paste.deploy.compat import print_ + + +def test_load_package(): + print_('Path:') + pprint(sys.path) + print_(pkg_resources.require('FakeApp')) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..07853f5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = py25,py26,py27,py31,py32,jython,pypy1.4,pypy1.5 + +[testenv] +deps=nose + Paste +commands={envpython} setup.py test + +# Keep it this way until Paste has been ported to py3k +[testenv:py31] +deps=nose + +[testenv:py32] +deps=nose + +[testenv:pypy1.4] +basepython=pypy1.4 + +[testenv:pypy1.5] +basepython=pypy1.5 -- cgit v1.2.1 From d4ac1f226fcfe58fef22f73ef8460eceea9dd175 Mon Sep 17 00:00:00 2001 From: Atsushi Odagiri Date: Tue, 7 Feb 2012 06:50:02 -0800 Subject: fix invalid import of paste.deploy.Config to paste.deploy.config.Config --- paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl index 6df5a6e..cb49352 100644 --- a/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl +++ b/paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl @@ -1,6 +1,6 @@ import cgi -from paste.deploy import CONFIG +from paste.deploy.config import CONFIG def application(environ, start_response): @@ -21,4 +21,3 @@ def application(environ, start_response): % (key, cgi.escape(repr(value)))) content.append('') return content - \ No newline at end of file -- cgit v1.2.1 -- cgit v1.2.1 From 05e148fa8fc4b19464a5b296e7fab9c88123aed3 Mon Sep 17 00:00:00 2001 From: Pior Bastida Date: Tue, 31 Jul 2012 16:55:01 -0400 Subject: Fix multi proxy IPs bug in X-Forwarded-For header in PrefixMiddleware --- paste/deploy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/config.py b/paste/deploy/config.py index 1db5680..a503007 100644 --- a/paste/deploy/config.py +++ b/paste/deploy/config.py @@ -269,7 +269,7 @@ class PrefixMiddleware(object): 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') + 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: -- cgit v1.2.1 From 228632fe6abd81d81ec6dc62a24e78fb8f514965 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Sun, 16 Sep 2012 00:39:01 +0300 Subject: Fixed TypeError when trying to raise LookupError on Python 3 (fixes #10) --- paste/deploy/compat.py | 2 ++ paste/deploy/loadwsgi.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/paste/deploy/compat.py b/paste/deploy/compat.py index f7b93f5..bd2063f 100644 --- a/paste/deploy/compat.py +++ b/paste/deploy/compat.py @@ -17,6 +17,7 @@ if sys.version_info < (3, 0): from ConfigParser import ConfigParser from urllib import unquote iteritems = lambda d: d.iteritems() + dictkeys = lambda d: d.keys() def reraise(t, e, tb): exec('raise t, e, tb', dict(t=t, e=e, tb=tb)) @@ -25,6 +26,7 @@ else: from configparser import ConfigParser from urllib.parse import unquote iteritems = lambda d: d.items() + dictkeys = lambda d: list(d.keys()) def reraise(t, e, tb): exec('raise e from tb', dict(e=e, tb=tb)) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 66046c7..8b2849d 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -7,7 +7,7 @@ import re import pkg_resources -from paste.deploy.compat import ConfigParser, unquote, iteritems +from paste.deploy.compat import ConfigParser, unquote, iteritems, dictkeys from paste.deploy.util import fix_call, lookup_object __all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] @@ -655,7 +655,7 @@ class EggLoader(_Loader): dist.location, ', '.join(_flatten(object_type.egg_protocols)), ', '.join(_flatten([ - (pkg_resources.get_entry_info(self.spec, prot, name) or {}).keys() + dictkeys(pkg_resources.get_entry_info(self.spec, prot, name) or {}) for prot in protocol_options] or '(no entry points)')))) if len(possible) > 1: raise LookupError( -- cgit v1.2.1 From 09f33672969a118c8faedb51f905e3291c9aab40 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Wed, 8 May 2013 17:06:41 -0400 Subject: Use reraise syntax that actually works in Py3k. --- paste/deploy/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paste/deploy/compat.py b/paste/deploy/compat.py index bd2063f..05047db 100644 --- a/paste/deploy/compat.py +++ b/paste/deploy/compat.py @@ -29,4 +29,4 @@ else: dictkeys = lambda d: list(d.keys()) def reraise(t, e, tb): - exec('raise e from tb', dict(e=e, tb=tb)) + raise e.with_traceback(tb) -- cgit v1.2.1 From bf663458722b48b73178356bd7e28dfc0456c75f Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Thu, 16 May 2013 13:35:18 -0400 Subject: Py3k-compatible sorting of 'kwargs.items'. --- .hgignore | 13 + MANIFEST.in | 2 + README | 11 + docs/_static/paste.css | 15 + docs/_templates/layout.html | 20 + docs/conf.py | 132 ++++ docs/index.txt | 672 +++++++++++++++++++ docs/license.txt | 20 + docs/modules/config.txt | 13 + docs/modules/converters.txt | 11 + docs/modules/loadwsgi.txt | 13 + docs/news.txt | 159 +++++ paste/__init__.py | 18 + paste/deploy/__init__.py | 3 + paste/deploy/compat.py | 32 + paste/deploy/config.py | 305 +++++++++ paste/deploy/converters.py | 40 ++ paste/deploy/loadwsgi.py | 725 +++++++++++++++++++++ paste/deploy/paster_templates.py | 36 + .../paste_deploy/+package+/sampleapp.py_tmpl | 23 + .../paste_deploy/+package+/wsgiapp.py_tmpl | 24 + .../paste_deploy/docs/devel_config.ini_tmpl | 22 + paste/deploy/util.py | 73 +++ regen-docs | 9 + setup.py | 52 ++ tests/__init__.py | 13 + .../FakeApp.egg/FakeApp.egg-info/PKG-INFO | 10 + .../FakeApp.egg/FakeApp.egg-info/entry_points.txt | 22 + .../FakeApp.egg/FakeApp.egg-info/top_level.txt | 1 + .../fake_packages/FakeApp.egg/fakeapp/__init__.py | 1 + tests/fake_packages/FakeApp.egg/fakeapp/apps.py | 69 ++ .../FakeApp.egg/fakeapp/configapps.py | 14 + tests/fake_packages/FakeApp.egg/setup.py | 23 + tests/fixture.py | 20 + tests/sample_configs/basic_app.ini | 14 + tests/sample_configs/executable.ini | 10 + tests/sample_configs/test_config.ini | 38 ++ tests/sample_configs/test_config_included.ini | 10 + tests/sample_configs/test_error.ini | 8 + tests/sample_configs/test_filter.ini | 22 + tests/sample_configs/test_filter_with.ini | 12 + tests/sample_configs/test_func.ini | 13 + tests/test_basic_app.py | 36 + tests/test_config.py | 173 +++++ tests/test_config_middleware.py | 28 + tests/test_converters.py | 17 + tests/test_filter.py | 53 ++ tests/test_load_package.py | 12 + tox.ini | 20 + 49 files changed, 3082 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 docs/_static/paste.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/conf.py create mode 100644 docs/index.txt create mode 100644 docs/license.txt create mode 100644 docs/modules/config.txt create mode 100644 docs/modules/converters.txt create mode 100644 docs/modules/loadwsgi.txt create mode 100644 docs/news.txt create mode 100644 paste/__init__.py create mode 100644 paste/deploy/__init__.py create mode 100644 paste/deploy/compat.py create mode 100644 paste/deploy/config.py create mode 100644 paste/deploy/converters.py create mode 100644 paste/deploy/loadwsgi.py create mode 100644 paste/deploy/paster_templates.py create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/sampleapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl create mode 100644 paste/deploy/paster_templates/paste_deploy/docs/devel_config.ini_tmpl create mode 100644 paste/deploy/util.py create mode 100755 regen-docs create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt create mode 100644 tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/__init__.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/apps.py create mode 100644 tests/fake_packages/FakeApp.egg/fakeapp/configapps.py create mode 100644 tests/fake_packages/FakeApp.egg/setup.py create mode 100644 tests/fixture.py create mode 100644 tests/sample_configs/basic_app.ini create mode 100755 tests/sample_configs/executable.ini create mode 100644 tests/sample_configs/test_config.ini create mode 100644 tests/sample_configs/test_config_included.ini create mode 100644 tests/sample_configs/test_error.ini create mode 100644 tests/sample_configs/test_filter.ini create mode 100644 tests/sample_configs/test_filter_with.ini create mode 100644 tests/sample_configs/test_func.ini create mode 100644 tests/test_basic_app.py create mode 100644 tests/test_config.py create mode 100644 tests/test_config_middleware.py create mode 100644 tests/test_converters.py create mode 100644 tests/test_filter.py create mode 100644 tests/test_load_package.py create mode 100644 tox.ini diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..44abbb2 --- /dev/null +++ b/.hgignore @@ -0,0 +1,13 @@ +syntax: glob +*.egg-info/ +*.egg/ +*.pyc +*.class +dist/ +build/ +docs/_build/ +.tox +.project +.pydevproject +.settings +__pycache__ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7a2ffb9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include docs/*.txt +recursive-include paste/deploy/paster_templates * diff --git a/README b/README new file mode 100644 index 0000000..291a026 --- /dev/null +++ b/README @@ -0,0 +1,11 @@ +This tool provides code to load WSGI applications and servers from +URIs; these URIs can refer to Python Eggs for INI-style configuration +files. `Paste Script `_ provides +commands to serve applications based on this configuration file. + +The latest version is available in a `Mercurial repository +`_ (or a `tarball +`_). + +For the latest changes see the `news file +`_. \ No newline at end of file diff --git a/docs/_static/paste.css b/docs/_static/paste.css new file mode 100644 index 0000000..6705e5d --- /dev/null +++ b/docs/_static/paste.css @@ -0,0 +1,15 @@ +a.invisible-link { + color: #fff; + text-decoration: none; +} + +a.invisible-link:visited { + color: #fff; + text-decoration: none; +} + +a.invisible:link { + color: #fff; + text-decoration: none; +} + diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 0000000..a6d2a97 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,20 @@ +{% extends "!layout.html" %} + +{% block extrahead %} +{{ super() }} + +{% endblock %} + +{% block sidebartoc %} +

Python Paste

+ + + +{{ super() }} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..49aed08 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# +# Paste documentation build configuration file, created by +# sphinx-quickstart on Tue Apr 22 22:08:49 2008. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# All configuration values have a default value; values that are commented out +# serve to show the default value. + +import sys + +# If your extensions are in another directory, add it here. +#sys.path.append('some/directory') + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Paste Deploy' +copyright = '2011, Ian Bicking and contributors' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '1.5' +# The full version, including alpha/beta/rc tags. +release = '1.5.1.dev1' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = ['include/contact.txt', 'include/reference_header.txt'] + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +html_style = 'default.css' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Content template for the index page. +#html_index = '' + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PasteDeploydoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +#latex_documents = [] + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.txt b/docs/index.txt new file mode 100644 index 0000000..a1678fa --- /dev/null +++ b/docs/index.txt @@ -0,0 +1,672 @@ +Paste Deployment +================ + +:author: Ian Bicking + +.. contents:: + +Documents: + +.. toctree:: + :maxdepth: 1 + + news + modules/loadwsgi + modules/config + modules/converters + license + +.. comment: + The names used in sections should be more concrete, and it should + be clearer that they are just arbitrary names. + +Introduction +------------ + +Paste Deployment is a system for finding and configuring WSGI +applications and servers. For WSGI application consumers it provides +a single, simple function (``loadapp``) for loading a WSGI application +from a configuration file or a Python Egg. For WSGI application +providers it only asks for a single, simple entry point to your +application, so that application users don't need to be exposed to the +implementation details of your application. + +The result is something a system administrator can install and manage +without knowing any Python, or the details of the WSGI application or +its container. + +Paste Deployment currently does not require other parts of `Paste +`_, and is distributed as a separate package. + +To see updates that have been made to Paste Deploy see the `news file +`_. + +Paste Deploy is released under the `MIT license +`_. + +Status +------ + +Paste Deploy has passed version 1.0. Paste Script is an actively +maintained project. As of 1.0, we'll make a strong effort to maintain +backward compatibility (this actually started happening long before +1.0, but now it is explicit). This will include deprecation warnings +when necessary. Major changes will take place under new functions or +with new entry points. + +Note that the most key aspect of Paste Deploy is the entry points it +defines (such as ``paste.app_factory``). Paste Deploy is not the only +consumer of these entry points, and many extensions can best take +place by utilizing the entry points instead of using Paste Deploy +directly. The entry points will not change; if changes are necessary, +new entry points will be defined. + +Installation +------------ + +First make sure you have either +`setuptools `_ or its +modern replacement +`distribute `_ installed. +For Python 3.x you need distribute as setuptools does not work on it. + +Then you can install Paste Deployment using `pip +`_ by running:: + + $ sudo pip install PasteDeploy + +If you want to track development, do:: + + $ hg clone http://bitbucket.org/ianb/pastedeploy + $ cd pastedeploy + $ sudo python setup.py develop + +This will install the package globally, but will load the files in the +checkout. You can also simply install ``PasteDeploy==dev``. + +For downloads and other information see the `Cheese Shop PasteDeploy +page `_. + +A complimentary package is `Paste Script `_. To install +that, use ``pip install PasteScript`` (or ``pip install +PasteScript==dev``). + +From the User Perspective +------------------------- + +In the following sections, the Python API for using Paste Deploy is +given. This isn't what users will be using (but it is useful for +Python developers and useful for setting up tests fixtures). + +The primary interaction with Paste Deploy is through its configuration +files. The primary thing you want to do with a configuration file is +serve it. To learn about serving configuration files, see `the +``paster serve`` command +`_. + +The Config File +~~~~~~~~~~~~~~~ + +A config file has different sections. The only sections Paste Deploy +cares about have prefixes, like ``app:main`` or ``filter:errors`` -- +the part after the ``:`` is the "name" of the section, and the part +before gives the "type". Other sections are ignored. + +The format is a simple `INI format +`_: ``name = value``. You can +extend the value by indenting subsequent lines. ``#`` is a comment. + +Typically you have one or two sections, named "main": an application +section (``[app:main]``) and a server section (``[server:main]``). +``[composite:...]`` signifies something that dispatches to multiple +applications (example below). + +Here's a typical configuration file that also shows off mounting +multiple applications using `paste.urlmap +`_:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /wiki = wiki + /cms = config:cms.ini + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +I'll explain each section in detail now:: + + [composite:main] + use = egg:Paste#urlmap + / = home + /blog = blog + /cms = config:cms.ini + +That this is a ``composite`` section means it dispatches the request +to other applications. ``use = egg:Paste#urlmap`` means to use the +composite application named ``urlmap`` from the ``Paste`` package. +``urlmap`` is a particularly common composite application -- it uses a +path prefix to map your request to another application. These are +the applications like "home", "blog", "wiki" and "config:cms.ini". The last +one just refers to another file ``cms.ini`` in the same directory. + +Next up:: + + [app:home] + use = egg:Paste#static + document_root = %(here)s/htdocs + +``egg:Paste#static`` is another simple application, in this case it +just serves up non-dynamic files. It takes one bit of configuration: +``document_root``. You can use variable substitution, which will pull +variables from the section ``[DEFAULT]`` (case sensitive!) with +markers like ``%(var_name)s``. The special variable ``%(here)s`` is +the directory containing the configuration file; you should use that +in lieu of relative filenames (which depend on the current directory, +which can change depending how the server is run). + +Then:: + + [filter-app:blog] + use = egg:Authentication#auth + next = blogapp + roles = admin + htpasswd = /home/me/users.htpasswd + + [app:blogapp] + use = egg:BlogApp + database = sqlite:/home/me/blog.db + +The ``[filter-app:blog]`` section means that you want an application +with a filter applied. The application being filtered is indicated +with ``next`` (which refers to the next section). The +``egg:Authentication#auth`` filter doesn't actually exist, but one +could imagine it logs people in and checks permissions. + +That last section is just a reference to an application that you +probably installed with ``pip install BlogApp``, and one bit of +configuration you passed to it (``database``). + +Lastly:: + + [app:wiki] + use = call:mywiki.main:application + database = sqlite:/home/me/wiki.db + +This section is similar to the previous one, with one important difference. +Instead of an entry point in an egg, it refers directly to the ``application`` +variable in the ``mywiki.main`` module. The reference consist of two parts, +separated by a colon. The left part is the full name of the module and the +right part is the path to the variable, as a Python expression relative to the +containing module. + +So, that's most of the features you'll use. + +Basic Usage +----------- + +The basic way you'll use Paste Deployment is to load `WSGI +`_ applications. Many +Python frameworks now support WSGI, so applications written for these +frameworks should be usable. + +The primary function is ``paste.deploy.loadapp``. This loads an +application given a URI. You can use it like:: + + from paste.deploy import loadapp + wsgi_app = loadapp('config:/path/to/config.ini') + +There's two URI formats currently supported: ``config:`` and ``egg:``. + +``config:`` URIs +---------------- + +URIs that being with ``config:`` refer to configuration files. These +filenames can be relative if you pass the ``relative_to`` keyword +argument to ``loadapp()``. + +.. note:: + + Filenames are never considered relative to the current working + directory, as that is a unpredictable location. Generally when + a URI has a context it will be seen as relative to that context; + for example, if you have a ``config:`` URI inside another + configuration file, the path is considered relative to the + directory that contains that configuration file. + +Config Format +~~~~~~~~~~~~~ + +Configuration files are in the INI format. This is a simple format +that looks like:: + + [section_name] + key = value + another key = a long value + that extends over multiple lines + +All values are strings (no quoting is necessary). The keys and +section names are case-sensitive, and may contain punctuation and +spaces (though both keys and values are stripped of leading and +trailing whitespace). Lines can be continued with leading whitespace. + +Lines beginning with ``#`` (preferred) or ``;`` are considered +comments. + +Applications +~~~~~~~~~~~~ + +You can define multiple applications in a single file; each +application goes in its own section. Even if you have just one +application, you must put it in a section. + +Each section name defining an application should be prefixed with +``app:``. The "main" section (when just defining one application) +would go in ``[app:main]`` or just ``[app]``. + +There's two ways to indicate the Python code for the application. The +first is to refer to another URI or name:: + + [app:myapp] + use = config:another_config_file.ini#app_name + + # or any URI: + [app:myotherapp] + use = egg:MyApp + + # or a callable from a module: + [app:mythirdapp] + use = call:my.project:myapplication + + # or even another section: + [app:mylastapp] + use = myotherapp + +It would seem at first that this was pointless; just a way to point to +another location. However, in addition to loading the application +from that location, you can also add or change the configuration. + +The other way to define an application is to point exactly to some +Python code:: + + [app:myapp] + paste.app_factory = myapp.modulename:app_factory + +You must give an explicit *protocol* (in this case +``paste.app_factory``), and the value is something to import. In +this case the module ``myapp.modulename`` is loaded, and the +``app_factory`` object retrieved from it. + +See `Defining Factories`_ for more about the protocols. + +Configuration +~~~~~~~~~~~~~ + +Configuration is done through keys besides ``use`` (or the protocol +names). Any other keys found in the section will be passed as keyword +arguments to the factory. This might look like:: + + [app:blog] + use = egg:MyBlog + database = mysql://localhost/blogdb + blogname = This Is My Blog! + +You can override these in other sections, like:: + + [app:otherblog] + use = blog + blogname = The other face of my blog + +This way some settings could be defined in a generic configuration +file (if you have ``use = config:other_config_file``) or you can +publish multiple (more specialized) applications just by adding a +section. + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +Often many applications share the same configuration. While you can +do that a bit by using other config sections and overriding values, +often you want that done for a bunch of disparate configuration +values. And typically applications can't take "extra" configuration +parameters; with global configuration you do something equivalent to +"if this application wants to know the admin email, this is it". + +Applications are passed the global configuration separately, so they +must specifically pull values out of it; typically the global +configuration serves as the basis for defaults when no local +configuration is passed in. + +Global configuration to apply to every application defined in a file +should go in a special section named ``[DEFAULT]``. You can override +global configuration locally like:: + + [DEFAULT] + admin_email = webmaster@example.com + + [app:main] + use = ... + set admin_email = bob@example.com + +That is, by using ``set`` in front of the key. + +Composite Applications +~~~~~~~~~~~~~~~~~~~~~~ + +"Composite" applications are things that act like applications, but +are made up of other applications. One example would be a URL mapper, +where you mount applications at different URL paths. This might look +like:: + + [composite:main] + use = egg:Paste#urlmap + / = mainapp + /files = staticapp + + [app:mainapp] + use = egg:MyApp + + [app:staticapp] + use = egg:Paste#static + document_root = /path/to/docroot + +The composite application "main" is just like any other application +from the outside (you load it with ``loadapp`` for instance), but it +has access to other applications defined in the configuration file. + +Other Objects +~~~~~~~~~~~~~ + +In addition to sections with ``app:``, you can define filters and +servers in a configuration file, with ``server:`` and ``filter:`` +prefixes. You load these with ``loadserver`` and ``loadfilter``. The +configuration works just the same; you just get back different kinds +of objects. + +Filter Composition +~~~~~~~~~~~~~~~~~~ + +There are several ways to apply filters to applications. It mostly +depends on how many filters, and in what order you want to apply them. + +The first way is to use the ``filter-with`` setting, like:: + + [app:main] + use = egg:MyEgg + filter-with = printdebug + + [filter:printdebug] + use = egg:Paste#printdebug + # and you could have another filter-with here, and so on... + +Also, two special section types exist to apply filters to your +applications: ``[filter-app:...]`` and ``[pipeline:...]``. Both of +these sections define applications, and so can be used wherever an +application is needed. + +``filter-app`` defines a filter (just like you would in a +``[filter:...]`` section), and then a special key ``next`` which +points to the application to apply the filter to. + +``pipeline:`` is used when you need apply a number of filters. It +takes *one* configuration key ``pipeline`` (plus any global +configuration overrides you want). ``pipeline`` is a list of filters +ended by an application, like:: + + [pipeline:main] + pipeline = filter1 egg:FilterEgg#filter2 filter3 app + + [filter:filter1] + ... + +Getting Configuration +~~~~~~~~~~~~~~~~~~~~~ + +If you want to get the configuration without creating the application, +you can use the ``appconfig(uri)`` function, which is just like the +``loadapp()`` function except it returns the configuration that would +be used, as a dictionary. Both global and local configuration is +combined into a single dictionary, but you can look at just one or the +other with the attributes ``.local_conf`` and ``.global_conf``. + +``egg:`` URIs +------------- + +`Python Eggs `_ +are a distribution and installation format produced by `setuptools +`_ and +`distribute `_ that adds metadata to a +normal Python package (among other things). + +You don't need to understand a whole lot about Eggs to use them. If +you have a `distutils +`_ +``setup.py`` script, just change:: + + from distutils.core import setup + +to:: + + from setuptools import setup + +Now when you install the package it will be installed as an egg. + +The first important part about an Egg is that it has a +*specification*. This is formed from the name of your distribution +(the ``name`` keyword argument to ``setup()``), and you can specify a +specific version. So you can have an egg named ``MyApp``, or +``MyApp==0.1`` to specify a specific version. + +The second is *entry points*. These are references to Python objects +in your packages that are named and have a specific protocol. +"Protocol" here is just a way of saying that we will call them with +certain arguments, and expect a specific return value. We'll talk +more about the protocols later_. + +.. _later: `Defining Factories`_ + +The important part here is how we define entry points. You'll add an +argument to ``setup()`` like:: + + setup( + name='MyApp', + ... + entry_points={ + 'paste.app_factory': [ + 'main=myapp.mymodule:app_factory', + 'ob2=myapp.mymodule:ob_factory'], + }, + ) + +This defines two applications named ``main`` and ``ob2``. You can +then refer to these by ``egg:MyApp#main`` (or just ``egg:MyApp``, +since ``main`` is the default) and ``egg:MyApp#ob2``. + +The values are instructions for importing the objects. ``main`` is +located in the ``myapp.mymodule`` module, in an object named +``app_factory``. + +There's no way to add configuration to objects imported as Eggs. + +Defining Factories +------------------ + +This lets you point to factories (that obey the specific protocols we +mentioned). But that's not much use unless you can create factories +for your applications. + +There's a few protocols: ``paste.app_factory``, +``paste.composite_factory``, ``paste.filter_factory``, and lastly +``paste.server_factory``. Each of these expects a callable (like a +function, method, or class). + +``paste.app_factory`` +~~~~~~~~~~~~~~~~~~~~~~ + +The application is the most common. You define one like:: + + def app_factory(global_config, **local_conf): + return wsgi_app + +The ``global_config`` is a dictionary, and local configuration is +passed as keyword arguments. The function returns a WSGI application. + +``paste.composite_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Composites are just slightly more complex:: + + def composite_factory(loader, global_config, **local_conf): + return wsgi_app + +The ``loader`` argument is an object that has a couple interesting +methods. ``get_app(name_or_uri, global_conf=None)`` return a WSGI +application with the given name. ``get_filter`` and ``get_server`` +work the same way. + +A more interesting example might be a composite factory that does +something. For instance, consider a "pipeline" application:: + + def pipeline_factory(loader, global_config, pipeline): + # space-separated list of filter and app names: + pipeline = pipeline.split() + filters = [loader.get_filter(n) for n in pipeline[:-1]] + app = loader.get_app(pipeline[-1]) + filters.reverse() # apply in reverse order! + for filter in filters: + app = filter(app) + return app + +Then we use it like:: + + [composite:main] + use = + pipeline = egg:Paste#printdebug session myapp + + [filter:session] + use = egg:Paste#session + store = memory + + [app:myapp] + use = egg:MyApp + +``paste.filter_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Filter factories are just like app factories (same signature), except +they return filters. Filters are callables that take a WSGI +application as the only argument, and return a "filtered" version of +that application. + +Here's an example of a filter that checks that the ``REMOTE_USER`` CGI +variable is set, creating a really simple authentication filter:: + + def auth_filter_factory(global_conf, req_usernames): + # space-separated list of usernames: + req_usernames = req_usernames.split() + def filter(app): + return AuthFilter(app, req_usernames) + return filter + + class AuthFilter(object): + def __init__(self, app, req_usernames): + self.app = app + self.req_usernames = req_usernames + + def __call__(self, environ, start_response): + if environ.get('REMOTE_USER') in self.req_usernames: + return self.app(environ, start_response) + start_response( + '403 Forbidden', [('Content-type', 'text/html')]) + return ['You are forbidden to view this resource'] + +``paste.filter_app_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is very similar to ``paste.filter_factory``, except that it also +takes a ``wsgi_app`` argument, and returns a WSGI application. So if +you changed the above example to:: + + class AuthFilter(object): + def __init__(self, app, global_conf, req_usernames): + .... + +Then ``AuthFilter`` would serve as a filter_app_factory +(``req_usernames`` is a required local configuration key in this +case). + +``paste.server_factory`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This takes the same signature as applications and filters, but returns +a server. + +A server is a callable that takes a single argument, a WSGI +application. It then serves the application. + +An example might look like:: + + def server_factory(global_conf, host, port): + port = int(port) + def serve(app): + s = Server(app, host=host, port=port) + s.serve_forever() + return serve + +The implementation of ``Server`` is left to the user. + +``paste.server_runner`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Like ``paste.server_factory``, except ``wsgi_app`` is passed as the +first argument, and the server should run immediately. + +Outstanding Issues +------------------ + +* Should there be a "default" protocol for each type of object? Since + there's currently only one protocol, it seems like it makes sense + (in the future there could be multiple). Except that + ``paste.app_factory`` and ``paste.composite_factory`` overlap + considerably. + +* ConfigParser's INI parsing is kind of annoying. I'd like it both + more constrained and less constrained. Some parts are sloppy (like + the way it interprets ``[DEFAULT]``). + +* ``config:`` URLs should be potentially relative to other locations, + e.g., ``config:$docroot/...``. Maybe using variables from + ``global_conf``? + +* Should other variables have access to ``global_conf``? + +* Should objects be Python-syntax, instead of always strings? Lots of + code isn't usable with Python strings without a thin wrapper to + translate objects into their proper types. + +* Some short-form for a filter/app, where the filter refers to the + "next app". Maybe like:: + + [app-filter:app_name] + use = egg:... + next = next_app + + [app:next_app] + ... + diff --git a/docs/license.txt b/docs/license.txt new file mode 100644 index 0000000..c810dec --- /dev/null +++ b/docs/license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2006-2007 Ian Bicking and Contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/docs/modules/config.txt b/docs/modules/config.txt new file mode 100644 index 0000000..9d8f894 --- /dev/null +++ b/docs/modules/config.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.config` -- Configuration and Environment middleware +====================================================================== + +.. automodule:: paste.deploy.config + +Module Contents +--------------- + +.. autoclass:: DispatchingConfig +.. autoclass:: ConfigMiddleware +.. autoclass:: PrefixMiddleware + +.. comment: FIXME: do something about CONFIG (manual docs?) diff --git a/docs/modules/converters.txt b/docs/modules/converters.txt new file mode 100644 index 0000000..4948104 --- /dev/null +++ b/docs/modules/converters.txt @@ -0,0 +1,11 @@ +:mod:`paste.deploy.converters` -- Conversion helpers for String Configuration +============================================================================= + +.. automodule:: paste.deploy.converters + +Module Contents +--------------- + +.. autofunction:: asbool +.. autofunction:: asint +.. autofunction:: aslist diff --git a/docs/modules/loadwsgi.txt b/docs/modules/loadwsgi.txt new file mode 100644 index 0000000..feebf28 --- /dev/null +++ b/docs/modules/loadwsgi.txt @@ -0,0 +1,13 @@ +:mod:`paste.deploy.loadwsgi` -- Load WSGI applications from config files +======================================================================== + +.. automodule:: paste.deploy.loadwsgi + +Module Contents +--------------- + +.. autofunction:: loadapp +.. autofunction:: loadserver +.. autofunction:: loadfilter +.. autofunction:: appconfig + diff --git a/docs/news.txt b/docs/news.txt new file mode 100644 index 0000000..c534868 --- /dev/null +++ b/docs/news.txt @@ -0,0 +1,159 @@ +Paste Deployment News +===================== + +1.5.0 +----- + +* Project is now maintained by Alex Grönholm + +* Was printing extraneous data when calling setup.py + +* Fixed missing paster template files (fixes "paster create -t paste.deploy") + +* Excluded tests from release distributions + +* Added support for the "call:" protocol for loading apps directly as + functions (contributed by Jason Stitt) + +* Added Python 3.x support + +* Dropped Python 2.4 support + +* Removed the ``paste.deploy.epdesc`` and ``paste.deploy.interfaces`` modules + -- contact the maintainer if you actually needed them + +1.3.4 +----- + +* Fix loadconfig path handling on Jython on Windows. + +1.3.3 +----- + +* In :class:`paste.deploy.config.PrefixMiddleware` the headers + ``X-Forwarded-Scheme`` and ``X-Forwarded-Proto`` are now translated + to the key ``environ['wsgi.url_scheme']``. Also ``X-Forwarded-For`` + is translated to ``environ['REMOTE_ADDR']`` + +* Also in PrefixMiddleware, if X-Forwarded-Host has multiple + (comma-separated) values, use only the first value. + +1.3.2 +----- + +* Added ``paste.deploy.converters.asint()``. +* fixed use sections overwriting the config's __file__ value with the + use'd filename. +* ``paste.deploy.loadwsgi`` now supports variable expansion in the + DEFAULT section of config files (unlike plain ConfigParser). + +1.3.1 +----- + +* Fix ``appconfig`` config loading when using a config file with + ``filter-with`` in it (previously you'd get TypeError: iteration + over non-sequence) + +1.3 +--- + +* Added ``scheme`` option to ``PrefixMiddleware``, so you can force a + scheme (E.g., when proxying an HTTPS connection over HTTP). + +* Pop proper values into ``environ['paste.config']`` in + ``ConfigMiddleware``. + +1.1 +--- + +* Any ``global_conf`` extra keys you pass to ``loadapp`` (or the other + loaders) will show up as though they were in ``[DEFAULT]``, so they + can be used in variable interpolation. Note: this won't overwrite + any existing values in ``[DEFAULT]``. + +* Added ``force_port`` option to + ``paste.deploy.config.PrefixMiddleware``. Also the ``prefix`` + argument is stripped of any trailing ``/``, which can't be valid in + that position. + +1.0 +--- + +* Added some documentation for the different kinds of entry points + Paste Deploy uses. + +* Added a feature to ``PrefixMiddleware`` that translates the + ``X-Forwarded-Server`` header to ``Host``. + +0.9.6 +----- + +* Added ``PrefixMiddleware`` which compensates for cases where the + wsgi app is behind a proxy of some sort that isn't moving the prefix + into the SCRIPT_NAME in advance. + +* Changed _loadconfig() so that it works with Windows absolute paths. + +* Make the error messages prettier when you call a function and fail + to give an argument, like a required function argument. + +0.5 +--- + +* Made the ``paste_deploy`` template (used with ``paster create + --template=paste_deploy``) more useful, with an example application + and entry point. + +0.4 +--- + +* Allow filters to have ``filter-with`` values, just like + applications. + +* Renamed ``composit`` to ``composite`` (old names still work, but + aren't documented). + +* Added ``appconfig()`` to load along with ``loadapp()``, but return + the configuration without invoking the application. + +0.3 +--- + +* Allow variable setting like:: + + get local_var = global_var_name + + To bring in global variables to the local scope. + +* Allow interpolation in files, like ``%(here)s``. Anything in the + ``[DEFAULTS]`` section will be available to substitute into a value, + as will variables in the same section. Also, the special value + ``here`` will be the directory the configuration file is located in. + +0.2 +--- + +Released 26 August 2004 + +* Added a ``filter-with`` setting to applications. + +* Removed the ``1`` from all the protocol names (e.g., + ``paste.app_factory1`` is not ``paste.app_factory``). + +* Added ``filter-app:`` and ``pipeline:`` sections. `Docs + `__. + +* Added ``paste.filter_app_factory1`` (`doc + `__) and + ``paste.server_runner1`` (`doc + `__) protocols. + +* Added ``paste.deploy.converters`` module for handling the + string values that are common with this system. + +0.1 +--- + +Released 22 August 2004 + +Initial version released. It's all new. diff --git a/paste/__init__.py b/paste/__init__.py new file mode 100644 index 0000000..cdb6121 --- /dev/null +++ b/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/paste/deploy/__init__.py b/paste/deploy/__init__.py new file mode 100644 index 0000000..94c63a8 --- /dev/null +++ b/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/paste/deploy/compat.py b/paste/deploy/compat.py new file mode 100644 index 0000000..05047db --- /dev/null +++ b/paste/deploy/compat.py @@ -0,0 +1,32 @@ +# (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 +"""Python 2<->3 compatibility module""" +import sys + + +def print_(template, *args, **kwargs): + template = str(template) + if args: + template = template % args + elif kwargs: + template = template % kwargs + sys.stdout.writelines(template) + +if sys.version_info < (3, 0): + basestring = basestring + from ConfigParser import ConfigParser + from urllib import unquote + iteritems = lambda d: d.iteritems() + dictkeys = lambda d: d.keys() + + def reraise(t, e, tb): + exec('raise t, e, tb', dict(t=t, e=e, tb=tb)) +else: + basestring = str + from configparser import ConfigParser + from urllib.parse import unquote + iteritems = lambda d: d.items() + dictkeys = lambda d: list(d.keys()) + + def reraise(t, e, tb): + raise e.with_traceback(tb) diff --git a/paste/deploy/config.py b/paste/deploy/config.py new file mode 100644 index 0000000..a503007 --- /dev/null +++ b/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(object): + + """ + 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(object): + + """ + 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(object): + """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 = '%s:%s' % (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 new file mode 100644 index 0000000..c9d87de --- /dev/null +++ b/paste/deploy/converters.py @@ -0,0 +1,40 @@ +# (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.compat import basestring + + +truthy = frozenset(['true', 'yes', 'on', 'y', 't', '1']) +falsy = frozenset(['false', 'no', 'off', 'n', 'f', '0']) + + +def asbool(obj): + if isinstance(obj, basestring): + 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, basestring): + 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 new file mode 100644 index 0000000..8b2849d --- /dev/null +++ b/paste/deploy/loadwsgi.py @@ -0,0 +1,725 @@ +# (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 __future__ import with_statement +import os +import sys +import re + +import pkg_resources + +from paste.deploy.compat import ConfigParser, unquote, iteritems, dictkeys +from paste.deploy.util import fix_call, lookup_object + +__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig'] + + +############################################################ +## Utility functions +############################################################ + + +def import_string(s): + return pkg_resources.EntryPoint.parse("x=" + s).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 + if hasattr(self, '_interpolation'): + self._interpolation = self.InterpolateWrapper(self._interpolation) + + read_file = getattr(ConfigParser, 'read_file', ConfigParser.readfp) + + 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 iteritems(defaults): + defaults[key] = self.get('DEFAULT', key) or val + return defaults + + def _interpolate(self, section, option, rawval, vars): + # Python < 3.2 + try: + return ConfigParser._interpolate( + self, section, option, rawval, vars) + except Exception: + e = sys.exc_info()[1] + args = list(e.args) + args[0] = 'Error in file %s: %s' % (self.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + class InterpolateWrapper(object): + # Python >= 3.2 + 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] = 'Error in file %s: %s' % (parser.filename, e) + e.args = tuple(args) + e.message = args[0] + raise + + +############################################################ +## Object types +############################################################ + + +class _ObjectType(object): + + 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 '<%s protocols=%r prefixes=%r>' % ( + 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(object): + + 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 iteritems(new_defaults): + 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) + if global_conf is None: + global_conf = {} + else: + global_conf = global_conf.copy() + defaults = self.parser.defaults() + global_conf.update(defaults) + 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" + % (', '.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([ + dictkeys(pkg_resources.get_entry_info(self.spec, prot, name) or {}) + 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(object): + + 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 new file mode 100644 index 0000000..9c5f942 --- /dev/null +++ b/paste/deploy/paster_templates.py @@ -0,0 +1,36 @@ +# (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 + +from paste.deploy.compat import print_ + + +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 new file mode 100644 index 0000000..cb49352 --- /dev/null +++ b/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 = [ + '%s\n' % greeting, + '

%s!

\n' % greeting, + '\n', + ] + items = environ.items() + items.sort() + for key, value in items: + content.append('\n' + % (key, cgi.escape(repr(value)))) + content.append('
%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 new file mode 100644 index 0000000..97decb8 --- /dev/null +++ b/paste/deploy/paster_templates/paste_deploy/+package+/wsgiapp.py_tmpl @@ -0,0 +1,24 @@ +from paste.deploy.config import ConfigMiddleware + +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 new file mode 100644 index 0000000..0c0ae35 --- /dev/null +++ b/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/paste/deploy/util.py b/paste/deploy/util.py new file mode 100644 index 0000000..b6f766a --- /dev/null +++ b/paste/deploy/util.py @@ -0,0 +1,73 @@ +# (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 + +from paste.deploy.compat import reraise + + +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 = '%s; got %s, wanted %s' % (exc_info[1], gotspec, 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) + reraise(*exc_info) + 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/regen-docs b/regen-docs new file mode 100755 index 0000000..f8dad75 --- /dev/null +++ b/regen-docs @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p docs/_static docs/_build +sphinx-build -E -b html docs/ docs/_build || exit 1 +if [ "$1" = "publish" ] ; then + cd docs/ + echo "Uploading files..." + scp -r _build/* ianb@webwareforpython.org:/home/paste/htdocs/deploy/ +fi diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f53a561 --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +from setuptools import setup, find_packages + + +setup( + name='PasteDeploy', + version='1.5.1.dev1', + description='Load, configure, and compose WSGI applications and servers', + long_description=open('README').read(), + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Paste', + ], + keywords='web wsgi application server', + author='Ian Bicking', + author_email='ianb@colorstudy.com', + maintainer='Alex Gronholm', + maintainer_email='alex.gronholm@nextday.fi', + url='http://pythonpaste.org/deploy/', + license='MIT', + namespace_packages=['paste'], + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + test_suite='nose.collector', + tests_require=['nose>=0.11'], + extras_require={ + 'Config': [], + 'Paste': ['Paste'], + }, + entry_points=""" + [paste.filter_app_factory] + config = paste.deploy.config:make_config_filter [Config] + prefix = paste.deploy.config:make_prefix_middleware + + [paste.paster_create_template] + paste_deploy=paste.deploy.paster_templates:PasteDeploy + """, +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..cffe526 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +import os +import sys + +here = os.path.dirname(__file__) +base = os.path.dirname(here) +sys.path.insert(0, base) + +# We can only import this after we adjust the paths +import pkg_resources + +# Make absolutely sure we're testing *this* package, not +# some other installed package +pkg_resources.require('PasteDeploy') diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO new file mode 100644 index 0000000..a2a1137 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: FakeApp +Version: 1.0 +Summary: UNKNOWN +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt new file mode 100644 index 0000000..9bfc986 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt @@ -0,0 +1,22 @@ +[paste.app_factory] + + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + + +[paste.composit_factory] + + remote_addr=fakeapp.apps:make_remote_addr + + +[paste.filter_app_factory] + + caps2=fakeapp.apps:CapFilter + + +[paste.filter_factory] + + caps=fakeapp.apps:make_cap_filter + + diff --git a/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt new file mode 100644 index 0000000..79ed67a --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/FakeApp.egg-info/top_level.txt @@ -0,0 +1 @@ +fakeapp diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/apps.py b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py new file mode 100644 index 0000000..cae7eba --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/apps.py @@ -0,0 +1,69 @@ +############################################################ +## Apps +############################################################ + +def simple_app(response, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['This is ', response] + +def basic_app(environ, start_response): + return simple_app('basic app', environ, start_response) + +def make_basic_app(global_conf, **conf): + return basic_app + +def basic_app2(environ, start_response): + return simple_app('basic app2', environ, start_response) + +def make_basic_app2(global_conf, **conf): + return basic_app2 + +############################################################ +## Composits +############################################################ + +def make_remote_addr(loader, global_conf, **conf): + apps = {} + addrs = {} + for name, value in conf.items(): + if name.startswith('app.'): + apps[name[4:]] = loader.get_app(value, global_conf) + elif name.startswith('addr.'): + addrs[name[5:]] = value + dispatcher = RemoteAddrDispatch() + for name in apps: + dispatcher.map[addrs[name]] = apps[name] + return dispatcher + +class RemoteAddrDispatch(object): + def __init__(self, map=None): + self.map = map or {} + + def __call__(self, environ, start_response): + addr = environ['REMOTE_ADDR'] + app = self.map.get(addr) or self.map['0.0.0.0'] + return app(environ, start_response) + +############################################################ +## Filters +############################################################ + +def make_cap_filter(global_conf, method_to_call='upper'): + def cap_filter(app): + return CapFilter(app, global_conf, method_to_call) + return cap_filter + +class CapFilter(object): + + def __init__(self, app, global_conf, method_to_call='upper'): + self.app = app + self.method_to_call = method_to_call + self.global_conf = global_conf + + def __call__(self, environ, start_response): + app_iter = self.app(environ, start_response) + for item in app_iter: + yield getattr(item, self.method_to_call)() + if hasattr(app_iter, 'close'): + app_iter.close() + diff --git a/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py new file mode 100644 index 0000000..ef13182 --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/fakeapp/configapps.py @@ -0,0 +1,14 @@ +class SimpleApp(object): + def __init__(self, global_conf, local_conf, name): + self.global_conf = global_conf + self.local_conf = local_conf + self.name = name + + def __call__(self, environ, start_response): + start_response('200 OK', [('Content-type', 'text/html')]) + return ['I am: ', name] + + def make_app(cls, global_conf, **conf): + return cls(global_conf, conf, 'basic') + make_app = classmethod(make_app) + diff --git a/tests/fake_packages/FakeApp.egg/setup.py b/tests/fake_packages/FakeApp.egg/setup.py new file mode 100644 index 0000000..854483e --- /dev/null +++ b/tests/fake_packages/FakeApp.egg/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="FakeApp", + version="1.0", + packages=find_packages(), + entry_points={ + 'paste.app_factory': """ + basic_app=fakeapp.apps:make_basic_app + other=fakeapp.apps:make_basic_app2 + configed=fakeapp.configapps:SimpleApp.make_app + """, + 'paste.composit_factory': """ + remote_addr=fakeapp.apps:make_remote_addr + """, + 'paste.filter_factory': """ + caps=fakeapp.apps:make_cap_filter + """, + 'paste.filter_app_factory': """ + caps2=fakeapp.apps:CapFilter + """, + }, + ) diff --git a/tests/fixture.py b/tests/fixture.py new file mode 100644 index 0000000..751659d --- /dev/null +++ b/tests/fixture.py @@ -0,0 +1,20 @@ +import os +import sys +import shutil + +test_dir = os.path.dirname(__file__) +egg_info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'EGG-INFO') +info_dir = os.path.join(test_dir, 'fake_packages', 'FakeApp.egg', + 'FakeApp.egg-info') +if not os.path.exists(egg_info_dir): + try: + os.symlink(info_dir, egg_info_dir) + except: + shutil.copytree(info_dir, egg_info_dir) + +sys.path.append(os.path.dirname(egg_info_dir)) + +from pkg_resources import * +working_set.add_entry(os.path.dirname(egg_info_dir)) +require('FakeApp') diff --git a/tests/sample_configs/basic_app.ini b/tests/sample_configs/basic_app.ini new file mode 100644 index 0000000..f1d931c --- /dev/null +++ b/tests/sample_configs/basic_app.ini @@ -0,0 +1,14 @@ +[application:main] +use = egg:FakeApp#basic_app + +[application:other] +use = egg:FakeApp#other + +[composit:remote_addr] +use = egg:FakeApp#remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 + diff --git a/tests/sample_configs/executable.ini b/tests/sample_configs/executable.ini new file mode 100755 index 0000000..3b75fe9 --- /dev/null +++ b/tests/sample_configs/executable.ini @@ -0,0 +1,10 @@ +#!/usr/bin/env paster +[exe] +sys.path = /fake/path/ + /another/fake/path ../fake_packages/ + +[server] +use = egg:PasteScript#cgi + +[app] +use = egg:FakeApp#basic_app diff --git a/tests/sample_configs/test_config.ini b/tests/sample_configs/test_config.ini new file mode 100644 index 0000000..d614829 --- /dev/null +++ b/tests/sample_configs/test_config.ini @@ -0,0 +1,38 @@ +[DEFAULT] +def1 = a +def2 = b +basepath = %(here)s + +[app:test1] +use = egg:FakeApp#configed +setting1 = foo +setting2 = bar +apppath = %(basepath)s/app + +[app:test2] +use = egg:FakeApp#configed +set def1 = test2 +set another = TEST +local conf = something + +[app:test3] +use = test2 +set def1 = test3 +another = something more + across several + lines + +[app:test_foreign_config] +use = config:test_config_included.ini +set glob = override +another = FOO + +[app:test_get] +use = egg:FakeApp#configed +set def2 = TEST +get def1 = def1 +get foo = def2 + +[app:test_global_conf] +use = egg:FakeApp#configed +test_interp = this:%(inherit)s diff --git a/tests/sample_configs/test_config_included.ini b/tests/sample_configs/test_config_included.ini new file mode 100644 index 0000000..cc0da7a --- /dev/null +++ b/tests/sample_configs/test_config_included.ini @@ -0,0 +1,10 @@ +[DEFAULT] +def2 = from include +def3 = c + +[app:main] +# Equivalent to the egg reference, but just for kicks... +paste.app_factory = fakeapp.configapps:SimpleApp.make_app +set glob = orig +bob = your uncle +another = BAR diff --git a/tests/sample_configs/test_error.ini b/tests/sample_configs/test_error.ini new file mode 100644 index 0000000..b6ad5b2 --- /dev/null +++ b/tests/sample_configs/test_error.ini @@ -0,0 +1,8 @@ +[DEFAULT] +def1 = a +def2 = b + +[app:main] +use = egg:FakeApp#configed +setting1 = foo +setting2 = %(does_not_exist)s/bar diff --git a/tests/sample_configs/test_filter.ini b/tests/sample_configs/test_filter.ini new file mode 100644 index 0000000..bfad8dc --- /dev/null +++ b/tests/sample_configs/test_filter.ini @@ -0,0 +1,22 @@ +[app:normal] +use = egg:FakeApp#basic_app + +[pipeline:piped] +pipeline = egg:FakeApp#caps normal + +[filter-app:filt] +use = egg:FakeApp#caps +method_to_call = lower +next = normal + +[pipeline:piped2] +pipeline = egg:FakeApp#caps2 normal + +[filter-app:filt2] +use = egg:FakeApp#caps2 +method_to_call = lower +next = normal + +[app:inv] +use = egg:FakeApp#basic_app +filter-with = egg:FakeApp#caps diff --git a/tests/sample_configs/test_filter_with.ini b/tests/sample_configs/test_filter_with.ini new file mode 100644 index 0000000..118804f --- /dev/null +++ b/tests/sample_configs/test_filter_with.ini @@ -0,0 +1,12 @@ +[app:main] +use = egg:FakeApp#basic_app +example = test +filter-with = filter1 + +[filter:filter1] +use = egg:FakeApp#caps +filter-with = filter2 + +[filter:filter2] +use = egg:FakeApp#caps + diff --git a/tests/sample_configs/test_func.ini b/tests/sample_configs/test_func.ini new file mode 100644 index 0000000..a0d28c4 --- /dev/null +++ b/tests/sample_configs/test_func.ini @@ -0,0 +1,13 @@ +[application:main] +use = call:fakeapp.apps:make_basic_app + +[application:other] +use = call:fakeapp.apps:make_basic_app2 + +[composit:remote_addr] +use = call:fakeapp.apps:make_remote_addr +app.1 = main +addr.1 = 127.0.0.1 + +app.2 = other +addr.2 = 0.0.0.0 \ No newline at end of file diff --git a/tests/test_basic_app.py b/tests/test_basic_app.py new file mode 100644 index 0000000..1ddb52b --- /dev/null +++ b/tests/test_basic_app.py @@ -0,0 +1,36 @@ +from paste.deploy import loadapp + +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_main(): + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#main', + relative_to=here) + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:sample_configs/basic_app.ini#ignored', + relative_to=here, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:sample_configs/basic_app.ini#other', + relative_to=here) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:sample_configs/basic_app.ini#remote_addr', + relative_to=here) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..de40a2a --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,173 @@ +from nose.tools import eq_ + +from paste.deploy import loadapp, appconfig +from tests.fixture import * +import fakeapp.configapps as fc +import fakeapp.apps + + +ini_file = 'config:sample_configs/test_config.ini' +here = os.path.dirname(__file__) +config_path = os.path.join(here, 'sample_configs') +config_filename = os.path.join(config_path, 'test_config.ini') + + +def test_config_egg(): + app = loadapp('egg:FakeApp#configed') + assert isinstance(app, fc.SimpleApp) + + +def test_config1(): + app = loadapp(ini_file, relative_to=here, name='test1') + eq_(app.local_conf, { + 'setting1': 'foo', + 'setting2': 'bar', + 'apppath': os.path.join(config_path, 'app')}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'b', + 'basepath': config_path, + 'here': config_path, + '__file__': config_filename}) + + +def test_config2(): + app = loadapp(ini_file, relative_to=here, name='test2') + eq_(app.local_conf, { + 'local conf': 'something'}) + eq_(app.global_conf, { + 'def1': 'test2', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + # Run this to make sure the global-conf-modified test2 + # didn't mess up the general global conf + test_config1() + + +def test_config3(): + app = loadapp(ini_file, relative_to=here, name='test3') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'local conf': 'something', + 'another': 'something more\nacross several\nlines'}) + eq_(app.global_conf, { + 'def1': 'test3', + 'def2': 'b', + 'basepath': config_path, + 'another': 'TEST', + 'here': config_path, + '__file__': config_filename}) + test_config2() + + +def test_main(): + app = loadapp('config:test_func.ini', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#main', + relative_to=config_path) + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + app = loadapp('config:test_func.ini#ignored', + relative_to=config_path, name='main') + assert app is fakeapp.apps.basic_app + + +def test_other(): + app = loadapp('config:test_func.ini#other', relative_to=config_path) + assert app is fakeapp.apps.basic_app2 + + +def test_composit(): + app = loadapp('config:test_func.ini#remote_addr', relative_to=config_path) + assert isinstance(app, fakeapp.apps.RemoteAddrDispatch) + assert app.map['127.0.0.1'] is fakeapp.apps.basic_app + assert app.map['0.0.0.0'] is fakeapp.apps.basic_app2 + + +def test_foreign_config(): + app = loadapp(ini_file, relative_to=here, name='test_foreign_config') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'another': 'FOO', + 'bob': 'your uncle'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'from include', + 'def3': 'c', + 'basepath': config_path, + 'glob': 'override', + 'here': config_path, + '__file__': os.path.join(config_path, 'test_config.ini')}) + + +def test_config_get(): + app = loadapp(ini_file, relative_to=here, name='test_get') + assert isinstance(app, fc.SimpleApp) + eq_(app.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(app.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig(): + conf = appconfig(ini_file, relative_to=here, name='test_get') + eq_(conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename, + 'foo': 'TEST'}) + eq_(conf.local_conf, { + 'def1': 'a', + 'foo': 'TEST'}) + eq_(conf.global_conf, { + 'def1': 'a', + 'def2': 'TEST', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + '__file__': config_filename}) + + +def test_appconfig_filter_with(): + conf = appconfig('config:test_filter_with.ini', relative_to=config_path) + eq_(conf['example'], 'test') + + +def test_global_conf(): + conf = appconfig(ini_file, relative_to=here, name='test_global_conf', + global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) + eq_(conf, { + 'def1': 'a', + # Note that this gets overwritten: + 'def2': 'b', + 'basepath': os.path.join(here, 'sample_configs'), + 'here': config_path, + 'inherit': 'bazbar', + '__file__': config_filename, + 'test_interp': 'this:bazbar', + }) + eq_(conf.local_conf, { + 'test_interp': 'this:bazbar'}) + + +def test_interpolate_exception(): + try: + appconfig('config:test_error.ini', relative_to=config_path) + except Exception: + e = sys.exc_info()[1] + expected = "Error in file %s" % os.path.join(config_path, 'test_error.ini') + eq_(str(e).split(':')[0], expected) + else: + assert False, 'Should have raised an exception' diff --git a/tests/test_config_middleware.py b/tests/test_config_middleware.py new file mode 100644 index 0000000..cc315e3 --- /dev/null +++ b/tests/test_config_middleware.py @@ -0,0 +1,28 @@ +from nose.tools import assert_raises +from nose.plugins.skip import SkipTest + +from paste.deploy.config import ConfigMiddleware + + +class Bug(Exception): + pass + + +def app_with_exception(environ, start_response): + def cont(): + yield "something" + raise Bug + start_response('200 OK', [('Content-type', 'text/html')]) + return cont() + + +def test_error(): + # This import is conditional due to Paste not yet working on py3k + try: + from paste.fixture import TestApp + except ImportError: + raise SkipTest + + wrapped = ConfigMiddleware(app_with_exception, {'test': 1}) + test_app = TestApp(wrapped) + assert_raises(Bug, test_app.get, '/') diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 0000000..5361310 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,17 @@ +def test_asbool_truthy(): + from paste.deploy.converters import asbool + assert asbool('true') + assert asbool('yes') + assert asbool('on') + assert asbool('y') + assert asbool('t') + assert asbool('1') + +def test_asbool_falsy(): + from paste.deploy.converters import asbool + assert not asbool('false') + assert not asbool('no') + assert not asbool('off') + assert not asbool('n') + assert not asbool('f') + assert not asbool('0') diff --git a/tests/test_filter.py b/tests/test_filter.py new file mode 100644 index 0000000..a76af7c --- /dev/null +++ b/tests/test_filter.py @@ -0,0 +1,53 @@ +from paste.deploy import loadapp +from tests.fixture import * +import fakeapp.apps + + +here = os.path.dirname(__file__) + + +def test_filter_app(): + app = loadapp('config:sample_configs/test_filter.ini#filt', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline(): + app = loadapp('config:sample_configs/test_filter.ini#piped', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app2(): + app = loadapp('config:sample_configs/test_filter.ini#filt2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'lower' + + +def test_pipeline2(): + app = loadapp('config:sample_configs/test_filter.ini#piped2', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + assert app.method_to_call == 'upper' + + +def test_filter_app_inverted(): + app = loadapp('config:sample_configs/test_filter.ini#inv', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert app.app is fakeapp.apps.basic_app + + +def test_filter_with_filter_with(): + app = loadapp('config:sample_configs/test_filter_with.ini', + relative_to=here) + assert isinstance(app, fakeapp.apps.CapFilter) + assert isinstance(app.app, fakeapp.apps.CapFilter) + assert app.app.app is fakeapp.apps.basic_app diff --git a/tests/test_load_package.py b/tests/test_load_package.py new file mode 100644 index 0000000..b3fea55 --- /dev/null +++ b/tests/test_load_package.py @@ -0,0 +1,12 @@ +from pprint import pprint +import sys + +import pkg_resources + +from paste.deploy.compat import print_ + + +def test_load_package(): + print_('Path:') + pprint(sys.path) + print_(pkg_resources.require('FakeApp')) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..07853f5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,20 @@ +[tox] +envlist = py25,py26,py27,py31,py32,jython,pypy1.4,pypy1.5 + +[testenv] +deps=nose + Paste +commands={envpython} setup.py test + +# Keep it this way until Paste has been ported to py3k +[testenv:py31] +deps=nose + +[testenv:py32] +deps=nose + +[testenv:pypy1.4] +basepython=pypy1.4 + +[testenv:pypy1.5] +basepython=pypy1.5 -- cgit v1.2.1 -- cgit v1.2.1 From 0235f9eec2edb5229a5236594ba1f570440e6492 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Fri, 27 Dec 2013 18:37:31 +0200 Subject: Cleaned up the envlist in tox.ini --- tox.ini | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tox.ini b/tox.ini index 07853f5..4ac34fd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25,py26,py27,py31,py32,jython,pypy1.4,pypy1.5 +envlist = py26,py27,py32,py33,pypy [testenv] deps=nose @@ -7,14 +7,8 @@ deps=nose commands={envpython} setup.py test # Keep it this way until Paste has been ported to py3k -[testenv:py31] -deps=nose - [testenv:py32] deps=nose -[testenv:pypy1.4] -basepython=pypy1.4 - -[testenv:pypy1.5] -basepython=pypy1.5 +[testenv:py33] +deps=nose -- cgit v1.2.1 From 26f66be27155714540495919e43c3bf76dd76676 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Fri, 27 Dec 2013 18:41:33 +0200 Subject: Released v1.5.1 --- docs/conf.py | 2 +- docs/news.txt | 15 +++++++++++++++ setup.cfg | 2 ++ setup.py | 53 ++++++++++++++++++++++++++++++----------------------- 4 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 setup.cfg diff --git a/docs/conf.py b/docs/conf.py index 49aed08..8958fdf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,7 @@ copyright = '2011, Ian Bicking and contributors' # The short X.Y version. version = '1.5' # The full version, including alpha/beta/rc tags. -release = '1.5.1.dev1' +release = '1.5.1' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/news.txt b/docs/news.txt index c534868..854b5ad 100644 --- a/docs/news.txt +++ b/docs/news.txt @@ -1,6 +1,21 @@ Paste Deployment News ===================== +1.5.1 +----- + +* Fixed use of the wrong variable when determining the context protocol + +* Fixed invalid import of paste.deploy.Config to paste.deploy.config.Config + +* Fixed multi proxy IPs bug in X-Forwarded-For header in PrefixMiddleware + +* Fixed TypeError when trying to raise LookupError on Python 3 + +* Fixed exception reraise on Python 3 + +Thanks to Alexandre Conrad, Atsushi Odagiri, Pior Bastida and Tres Seaver for their contributions. + 1.5.0 ----- diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f15c017 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = true diff --git a/setup.py b/setup.py index f53a561..b054254 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,36 @@ +import os + from setuptools import setup, find_packages +here = os.path.dirname(__file__) +readme_path = os.path.join(here, 'README') +readme = open(readme_path).read() + setup( name='PasteDeploy', - version='1.5.1.dev1', + version='1.5.1', description='Load, configure, and compose WSGI applications and servers', - long_description=open('README').read(), + long_description=readme, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Internet :: WWW/HTTP :: WSGI', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Framework :: Paste', - ], + 'Development Status :: 6 - Mature', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Paste', + ], keywords='web wsgi application server', author='Ian Bicking', author_email='ianb@colorstudy.com', @@ -38,9 +45,9 @@ setup( test_suite='nose.collector', tests_require=['nose>=0.11'], extras_require={ - 'Config': [], - 'Paste': ['Paste'], - }, + 'Config': [], + 'Paste': ['Paste'], + }, entry_points=""" [paste.filter_app_factory] config = paste.deploy.config:make_config_filter [Config] @@ -48,5 +55,5 @@ setup( [paste.paster_create_template] paste_deploy=paste.deploy.paster_templates:PasteDeploy - """, + """ ) -- cgit v1.2.1 -- cgit v1.2.1 -- cgit v1.2.1 From 556611a4f7edef0c3363562ec9330915bb470074 Mon Sep 17 00:00:00 2001 From: Laurence Rowe Date: Thu, 4 Dec 2014 02:13:31 +0000 Subject: Allow global_conf to override defaults, see: #7. --- paste/deploy/loadwsgi.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/paste/deploy/loadwsgi.py b/paste/deploy/loadwsgi.py index 8b2849d..fc766b0 100644 --- a/paste/deploy/loadwsgi.py +++ b/paste/deploy/loadwsgi.py @@ -406,12 +406,11 @@ class ConfigLoader(_Loader): global_conf=global_conf) section = self.find_config_section( object_type, name=name) - if global_conf is None: - global_conf = {} - else: - global_conf = global_conf.copy() defaults = self.parser.defaults() - global_conf.update(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 = {} -- cgit v1.2.1 From 71e021d87200e1926f373c0536aa4888c6869f7a Mon Sep 17 00:00:00 2001 From: Laurence Rowe Date: Fri, 5 Dec 2014 19:03:17 -0800 Subject: Update tests to reflect new behaviour of globals and DEFAULT --- tests/test_config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index de40a2a..f64dd21 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -98,7 +98,8 @@ def test_foreign_config(): 'bob': 'your uncle'}) eq_(app.global_conf, { 'def1': 'a', - 'def2': 'from include', + # Note overwrite of DEFAULT value from foreign config + 'def2': 'b', 'def3': 'c', 'basepath': config_path, 'glob': 'override', @@ -150,8 +151,8 @@ def test_global_conf(): global_conf={'def2': 'TEST DEF 2', 'inherit': 'bazbar'}) eq_(conf, { 'def1': 'a', - # Note that this gets overwritten: - 'def2': 'b', + # Note overwrite of DEFAULT value + 'def2': 'TEST DEF 2', 'basepath': os.path.join(here, 'sample_configs'), 'here': config_path, 'inherit': 'bazbar', -- cgit v1.2.1