diff options
author | ianb <devnull@localhost> | 2006-05-25 21:09:07 +0000 |
---|---|---|
committer | ianb <devnull@localhost> | 2006-05-25 21:09:07 +0000 |
commit | c533be78b0abd1dfd4fce41f9da3e21bc327e543 (patch) | |
tree | 7efdd6197cdcc9b8fd05227c9fed7a03bcfa79f2 /paste/urlmap.py | |
parent | 57d1fba0ef624e1a529793bb8a8920d5bca9d228 (diff) | |
download | paste-c533be78b0abd1dfd4fce41f9da3e21bc327e543.tar.gz |
Somehow I 'cleaned' the urlmap module, totally ruining it; must be changes that leaked from elsewhere. Damn.
Diffstat (limited to 'paste/urlmap.py')
-rw-r--r-- | paste/urlmap.py | 281 |
1 files changed, 212 insertions, 69 deletions
diff --git a/paste/urlmap.py b/paste/urlmap.py index a5d2487..80030fa 100644 --- a/paste/urlmap.py +++ b/paste/urlmap.py @@ -1,7 +1,70 @@ +# (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 +""" +Map URL prefixes to WSGI applications. See ``URLMap`` +""" + from UserDict import DictMixin import re +import os +import httpexceptions + +__all__ = ['URLMap', 'PathProxyURLMap'] + +def urlmap_factory(loader, global_conf, **local_conf): + if 'not_found_app' in local_conf: + not_found_app = local_conf.pop('not_found_app') + else: + not_found_app = global_conf.get('not_found_app') + if not_found_app: + not_found_app = loader.get_app(not_found_app, global_conf=global_conf) + urlmap = URLMap(not_found_app=not_found_app) + for path, app_name in local_conf.items(): + path = parse_path_expression(path) + app = loader.get_app(app_name, global_conf=global_conf) + urlmap[path] = app + return urlmap -__all__ = ['URLMap'] +def parse_path_expression(path): + """ + Parses a path expression like 'domain foobar.com port 20 /' or + just '/foobar' for a path alone. Returns as an address that + URLMap likes. + """ + parts = path.split() + domain = port = path = None + while parts: + if parts[0] == 'domain': + parts.pop(0) + if not parts: + raise ValueError("'domain' must be followed with a domain name") + if domain: + raise ValueError("'domain' given twice") + domain = parts.pop(0) + elif parts[0] == 'port': + parts.pop(0) + if not parts: + raise ValueError("'port' must be followed with a port number") + if port: + raise ValueError("'port' given twice") + port = parts.pop(0) + else: + if path: + raise ValueError("more than one path given (have %r, got %r)" + % (path, parts[0])) + path = parts.pop(0) + s = '' + if domain: + s = 'http://%s' % domain + if port: + if not domain: + raise ValueError("If you give a port, you must also give a domain") + s += ':' + port + if path: + if s: + s += '/' + s += path + return s class URLMap(DictMixin): @@ -9,98 +72,178 @@ class URLMap(DictMixin): URLMap instances are dictionary-like object that dispatch to one of several applications based on the URL. - The dictionary keys are paths to match (like - PATH_INFO.startswith(path)), and the values are applications to - dispatch to. PAths are matched most-specific-first, i.e., longest - path first. The SCRIPT_NAME and PATH_INFO environmental variables - are adjusted to indicate the new context. - """ + 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. + + URLs can also include domains, like ``http://blah.com/foo``, or as + tuples ``('blah.com', '/foo')``. This will match domain names; without + the ``http://domain`` or with a domain of ``None`` any domain will be + matched (so long as no other explicit domain matches). """ def __init__(self, not_found_app=None): self.applications = [] - self.not_found_application = not_found_app + self.not_found_application = self.not_found_app - norm_path_re = re.compile('//+') + norm_url_re = re.compile('//+') + domain_url_re = re.compile('^(http|https)://') def not_found_app(self, environ, start_response): - full_path = (environ.get('SCRIPT_NAME', '') - + environ.get('PATH_INFO', '')) - body = ( - "<html><head>\n" - "<title>Not Found</title>\n" - "</head><body>\n" - "<h1>Not Found</h1>\n" - "<p>The page %s was not found. </p>\n" - "<p><small>(URL mapper failed to find %r)</small></p>" - "</body></html>" - % (full_path, environ.get('PATH_INFO'))) - start_response('404 Not Found', - [('Content-type', 'text/html')]) - return [body] - - def normalize_path(self, path, trim=True): - if path.startswith('/'): - raise ValueError( - 'Mapped paths must not start with / (you gave %r)' % path) - path = self.norm_path_re.sub('/', path) + mapper = environ.get('paste.urlmap_object') + if mapper: + matches = [p for p, a in mapper.applications] + extra = 'defined apps: %s' % ( + ',\n '.join(map(repr, matches))) + else: + extra = '' + extra += '\nSCRIPT_NAME: %r' % environ.get('SCRIPT_NAME') + extra += '\nPATH_INFO: %r' % environ.get('PATH_INFO') + extra += '\nHTTP_HOST: %r' % environ.get('HTTP_HOST') + app = httpexceptions.HTTPNotFound( + 'The resource was not found', + comment=extra).wsgi_application + return app(environ, start_response) + + def normalize_url(self, url, trim=True): + if isinstance(url, (list, tuple)): + domain = url[0] + url = self.normalize_url(url[1])[1] + return domain, url + assert (not url or url.startswith('/') + or self.domain_url_re.search(url)), ( + "URL fragments must start with / or http:// (you gave %r)" % url) + match = self.domain_url_re.search(url) + if match: + url = url[match.end():] + if '/' in url: + domain, url = url.split('/', 1) + url = '/' + url + else: + domain, url = url, '' + else: + domain = None + url = self.norm_url_re.sub('/', url) if trim: - path = path.rstrip('/') - return path + url = url.rstrip('/') + return domain, url def sort_apps(self): """ Make sure applications are sorted with longest URLs first """ - self.applications.sort(key=lambda p: -len(p)) + def key(app_desc): + (domain, url), app = app_desc + if not domain: + # Make sure empty domains sort last: + return -len(url), '\xff' + else: + return -len(url), domain + apps = [(key(desc), desc) for desc in self.applications] + apps.sort() + self.applications = [desc for (sortable, desc) in apps] - def __setitem__(self, path, app): - path = self.normalize_path(path) - if path in self: - del self[path] - self.applications.append((path, app)) + def __setitem__(self, url, app): + if app is None: + try: + del self[url] + except KeyError: + pass + return + dom_url = self.normalize_url(url) + if dom_url in self: + del self[dom_url] + self.applications.append((dom_url, app)) self.sort_apps() - def __getitem__(self, path): - path = self.normalize_path(path) - for app_path, app in self.applications: - if app_path == path: + def __getitem__(self, url): + dom_url = self.normalize_url(url) + for app_url, app in self.applications: + if app_url == dom_url: return app raise KeyError( - "No application associated with the path %r" - % path) - - def __delitem__(self, path): - path = self.normalize_path(path) - for app_path, app in self.applications: - if app_path == path: - self.applications.remove((app_path, app)) + "No application with the url %r (domain: %r; existing: %s)" + % (url[1], url[0] or '*', self.applications)) + + 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, app)) break else: raise KeyError( - "No application associated with the path %r" % (path,)) + "No application with the url %r" % (url,)) def keys(self): - return [app_path - for app_path, app in self.applications] + return [app_url for app_url, app in self.applications] def __call__(self, environ, start_response): - path_info = environ.get('PATH_INFO', '') - path_info = self.normalize_path(path_info, False)[1] - for app_path, app in self.applications: - if path_info == app_path: - # We actually have to redirect in this - # case to add a /... - return self.add_slash(environ, start_response) - if path_info.startswith(app_path + '/'): - environ['SCRIPT_NAME'] += app_path - environ['PATH_INFO'] = path_info[len(app_path):] - return app(environ, start_response) - environ['stdlib.urlmap_object'] = self - if self.not_found_application is None: - return self.not_found_app( - environ, start_response) + host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() + if ':' in host: + host, port = host.split(':', 1)[0] else: - return self.not_found_application( - environ, start_response) + if environ['wsgi.url_scheme'] == 'http': + port = '80' + else: + port = '443' + path_info = environ.get('PATH_INFO') + path_info = self.normalize_url(path_info, False)[1] + for (domain, app_url), app in self.applications: + if domain and domain != host and domain != host+':'+port: + continue + 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) + environ['paste.urlmap_object'] = self + 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.map.normalize_url(url) + # @@: This means http://foo.com/bar will potentially + # match foo.com, but /base_paste_url/bar, which is unintuitive + url = (url[0] or self.base_paste_url[0], + self.base_paste_url[1] + url[1]) + 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) |