diff options
Diffstat (limited to 'paste')
| -rw-r--r-- | paste/default_config.conf | 13 | ||||
| -rw-r--r-- | paste/pyconfig.py | 39 | ||||
| -rwxr-xr-x | paste/server.py | 25 | ||||
| -rw-r--r-- | paste/tests/test_urlmap.py | 43 | ||||
| -rw-r--r-- | paste/urlmap.py | 137 |
5 files changed, 254 insertions, 3 deletions
diff --git a/paste/default_config.conf b/paste/default_config.conf index d92dcff..812df11 100644 --- a/paste/default_config.conf +++ b/paste/default_config.conf @@ -11,3 +11,16 @@ host = 'localhost' verbose = False quiet = False reload = False + +import sys +sys_path = sys.path + +package_urls = { + 'wsgiutils': 'http://www.owlfish.com/software/wsgiutils/downloads/WSGI%20Utils-0.5.tar.gz', + 'ZPTKit': 'http://svn.w4py.org/ZPTKit/trunk', + 'ZopePageTemplates': 'http://prdownloads.sourceforge.net/zpt/ZopePageTemplates-1.4.0.tgz?download', + 'scgi': 'http://www.mems-exchange.org/software/files/scgi/scgi-1.2.tar.gz', + 'sqlobject': 'http://svn.colorstudy.com/trunk/SQLObject', + } + +app_packages = 'app-packages' diff --git a/paste/pyconfig.py b/paste/pyconfig.py index 342ece2..d181e3c 100644 --- a/paste/pyconfig.py +++ b/paste/pyconfig.py @@ -51,6 +51,8 @@ class Config(UserDict.DictMixin, object): A dictionary-like object that represents configuration. """ + builtins = {} + def __init__(self, with_default=False): self.namespaces = [] if with_default: @@ -84,7 +86,7 @@ class Config(UserDict.DictMixin, object): def read_file(self, filename, namespace=None, load_self=True): - special_keys = ('__file__', 'load', 'include') + special_keys = ['__file__', 'load', 'include'] + self.builtins.keys() watch_file(filename) f = open(filename, 'rb') content = f.read() @@ -100,7 +102,9 @@ class Config(UserDict.DictMixin, object): orig = namespace.copy() namespace['__file__'] = os.path.abspath(filename) namespace['load'] = self.make_loader(filename, namespace) - namespace['include'] = self.make_includer(filename, namespace)
+ namespace['include'] = self.make_includer(filename, namespace) + for key, builder in self.builtins.items(): + namespace[key] = builder(self, filename, namespace) content = content.replace("\r\n","\n") exec content in namespace if load_self: @@ -420,3 +424,34 @@ def make_list(option): else: return option return [s.strip() for s in option.split(',')] + +def make_use_package(conf, relative_to, namespace): + import easy_install + from pkg_resources import DistributionNotFound, require + def use_package(package_spec, url=None, instdir=None, zip_ok=False, + multi=None, tmpdir=None): + if instdir is None: + instdir = namespace.get('app_packages', 'app-packages') + instdir = os.path.join(os.path.dirname(relative_to), instdir) + if instdir not in sys.path: + sys.path.insert(0, instdir) + if not os.path.exists(instdir): + os.makedirs(instdir) + try: + require(package_spec) + except DistributionNotFound, e: + pass + else: + return + if url is None: + url = namespace['package_urls'][package_spec] + installer = easy_install.Installer(instdir, zip_ok, multi, tmpdir) + try: + downloaded = installer.download(url) + installer.install_eggs(downloaded) + finally: + installer.close() + require(package_spec) + return use_package + +Config.builtins['use_package'] = make_use_package diff --git a/paste/server.py b/paste/server.py index 2db0ed7..980eb35 100755 --- a/paste/server.py +++ b/paste/server.py @@ -53,6 +53,7 @@ from paste import reloader from paste import CONFIG from paste.util import plugin from paste import pyconfig +from paste import urlmap reloader_environ_key = 'WSGI_RELOADER_SHOULD_RUN' @@ -177,6 +178,10 @@ def help(): program = sys.argv[0] return help_message % {'program': program} +def make_app_from_filename(filename): + conf = pyconfig.setup_config(filename) + return make_app(conf) + def make_app(conf): framework_name = conf.get('framework', 'default') framework = plugin.load_plugin_module( @@ -185,7 +190,25 @@ def make_app(conf): framework_name, '_framework') app = framework.build_application(conf) - return app + mapper = conf.get('system_urlmap') + if mapper: + mapper[''] = app + return mapper + else: + return app + +def make_proxy(conf, filename, namespace): + mapper = conf.get('system_urlmap') + if mapper is None: + namespace['system_urlmap'] = mapper = urlmap.URLMap() + current_url = conf.get('base_paste_url') + if current_url is None: + namespace['base_paste_url'] = current_url = '' + return urlmap.PathProxyURLMap( + mapper, current_url, os.path.dirname(filename), + make_app_from_filename) + +pyconfig.Config.builtins['urlmap'] = make_proxy def restart_with_reloader(conf): if conf['verbose']: diff --git a/paste/tests/test_urlmap.py b/paste/tests/test_urlmap.py new file mode 100644 index 0000000..c73b017 --- /dev/null +++ b/paste/tests/test_urlmap.py @@ -0,0 +1,43 @@ +from paste.urlmap import * +from fixture import * +del setup_module + +def make_app(response_text): + def app(environ, start_response): + headers = [('Content-type', 'text/html')] + start_response('200 OK', headers) + return [response_text % environ] + return app + +def test_map(): + mapper = URLMap() + app = TestApp(mapper) + text = '%s script_name="%%(SCRIPT_NAME)s" path_info="%%(PATH_INFO)s"' + mapper[''] = make_app(text % 'root') + mapper['/foo'] = make_app(text % 'foo-only') + mapper['/foo/bar'] = make_app(text % 'foo:bar') + mapper['/f'] = make_app(text % 'f-only') + res = app.get('/') + res.mustcontain('root') + res.mustcontain('script_name=""') + res.mustcontain('path_info="/"') + res = app.get('/blah') + res.mustcontain('root') + res.mustcontain('script_name=""') + res.mustcontain('path_info="/blah"') + res = app.get('/foo/and/more') + res.mustcontain('script_name="/foo"') + res.mustcontain('path_info="/and/more"') + res.mustcontain('foo-only') + res = app.get('/foo/bar/baz') + res.mustcontain('foo:bar') + res.mustcontain('script_name="/foo/bar"') + res.mustcontain('path_info="/baz"') + res = app.get('/fffzzz') + res.mustcontain('root') + res.mustcontain('path_info="/fffzzz"') + res = app.get('/f/z/y') + res.mustcontain('script_name="/f"') + res.mustcontain('path_info="/z/y"') + res.mustcontain('f-only') + diff --git a/paste/urlmap.py b/paste/urlmap.py new file mode 100644 index 0000000..e3bdb23 --- /dev/null +++ b/paste/urlmap.py @@ -0,0 +1,137 @@ +from UserDict import DictMixin +import re +import os +import wsgilib +from docsupport import metadata + +__all__ = ['URLMap', 'PathProxyURLMap'] + +class URLMap(DictMixin): + + """ + URLMap instances are dictionary-like object that dispatch to one + of several applications based on the URL. + + The dictionary keys are URLs to match (like + ``PATH_INFO.startswith(url)``), and the values are applications to + dispatch to. URLs are matched most-specific-first, i.e., longest + URL first. The ``SCRIPT_NAME`` and ``PATH_INFO`` environmental + variables are adjusted to indicate the new context. + """ + + def __init__(self): + self.applications = [] + self.not_found_application = wsgilib.error_response_app( + '404 Not Found', 'The resource was not found') + + attr_not_found_application = metadata.Attribute(""" + An application that is run when no other application matches + the URL. (Note that the URL ``""`` is also a catch-all URL.) + By default this application returns a simple 404 Not Found + result.""") + + norm_url_re = re.compile('//+') + + def normalize_url(self, url, trim=True): + assert not url or url.startswith('/'), ( + "URL fragments must start with / (you gave %r)" % url) + url = self.norm_url_re.sub('/', url) + if trim: + return url.rstrip('/') + else: + return url + + def sort_apps(self): + """ + Make sure applications are sorted with longest URLs first + """ + self.applications.sort( + lambda a, b: cmp(len(b[0]), len(a[0]))) + + def __setitem__(self, url, app): + url = self.normalize_url(url) + if url in self: + del self[url] + self.applications.append((url, app)) + self.sort_apps() + + def __getitem__(self, url): + url = self.normalize_url(url) + for app_url, app in self.applications: + if app_url == url: + return app + raise KeyError( + "No application with the url %r" % url) + + def __delitem__(self, url): + url = self.normalize_url(url) + for app_url, app in self.applications: + if app_url == url: + self.applications.remove((app_url, url)) + break + else: + raise KeyError( + "No application with the url %r" % url) + + def keys(self): + return [app_url for app_url, app in self.applications] + + def __call__(self, environ, start_response): + path_info = environ.get('PATH_INFO') + if not path_info: + # Kind of just a fast-track for this one case. + last_url, last_app = self.applications[-1] + if not last_url: + return last_app(environ, start_response) + else: + return self.not_found_application(environ, start_response) + path_info = self.normalize_url(path_info, False) + for app_url, app in self.applications: + if (path_info == app_url + or path_info.startswith(app_url + '/')): + environ['SCRIPT_NAME'] += app_url + environ['PATH_INFO'] = path_info[len(app_url):] + return app(environ, start_response) + return self.not_found_application(environ, start_response) + + +class PathProxyURLMap(object): + + """ + This is a wrapper for URLMap that catches any strings that + are passed in as applications; these strings are treated as + filenames (relative to `base_path`) and are passed to the + callable `builder`, which will return an application. + + This is intended for cases when configuration files can be + treated as applications. + + `base_paste_url` is the URL under which all applications added through + this wrapper must go. Use ``""`` if you want this to not + change incoming URLs. + """ + + def __init__(self, map, base_paste_url, base_path, builder): + self.map = map + self.base_paste_url = self.map.normalize_url(base_paste_url) + self.base_path = base_path + self.builder = builder + + def __setitem__(self, url, app): + if isinstance(app, (str, unicode)): + app_fn = os.path.join(self.base_path, app) + app = self.builder(app_fn) + url = self.base_paste_url + url + self.map[url] = app + + def __getattr__(self, attr): + return getattr(self.map, attr) + + # This is really the only settable attribute + def not_found_application__get(self): + return self.map.not_found_application + def not_found_application__set(self, value): + self.map.not_found_application = value + not_found_application = property(not_found_application__get, + not_found_application__set) + |
