summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Saddi <allan@saddi.com>2005-04-15 03:35:05 +0000
committerAllan Saddi <allan@saddi.com>2005-04-15 03:35:05 +0000
commit4f2b800f98e41d53ab0ea5ca0ce16e5e48eb3ae4 (patch)
tree942d43587f39b348676fc7cbdeb8c96dff95e393
parent700071bc26f40727331651b4f273465a4faa0c7d (diff)
downloadflup-4f2b800f98e41d53ab0ea5ca0ce16e5e48eb3ae4.tar.gz
Add publisher stuff to package.
-rw-r--r--flup/middleware/__init__.py1
-rw-r--r--flup/publisher/__init__.py3
-rw-r--r--flup/publisher/publisher.py763
-rw-r--r--flup/resolver/__init__.py3
-rw-r--r--flup/resolver/complex.py112
-rw-r--r--flup/resolver/function.py46
-rw-r--r--flup/resolver/importingmodule.py129
-rw-r--r--flup/resolver/module.py83
-rw-r--r--flup/resolver/nopathinfo.py57
-rw-r--r--flup/resolver/objectpath.py153
-rw-r--r--flup/resolver/resolver.py79
11 files changed, 1427 insertions, 2 deletions
diff --git a/flup/middleware/__init__.py b/flup/middleware/__init__.py
new file mode 100644
index 0000000..792d600
--- /dev/null
+++ b/flup/middleware/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/flup/publisher/__init__.py b/flup/publisher/__init__.py
index 792d600..46f89ca 100644
--- a/flup/publisher/__init__.py
+++ b/flup/publisher/__init__.py
@@ -1 +1,2 @@
-#
+from publisher import *
+del publisher
diff --git a/flup/publisher/publisher.py b/flup/publisher/publisher.py
new file mode 100644
index 0000000..6328a50
--- /dev/null
+++ b/flup/publisher/publisher.py
@@ -0,0 +1,763 @@
+# Copyright (c) 2002, 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+import os
+import inspect
+import cgi
+import types
+from Cookie import SimpleCookie
+
+__all__ = ['Request',
+ 'Response',
+ 'Transaction',
+ 'Publisher',
+ 'File',
+ 'Action',
+ 'Redirect',
+ 'InternalRedirect',
+ 'getexpected',
+ 'trimkw']
+
+class NoDefault(object):
+ """Sentinel object so we can distinguish None in keyword arguments."""
+ pass
+
+class Request(object):
+ """
+ Encapsulates data about the HTTP request.
+
+ Supported attributes that originate from here: (The attributes are
+ read-only, however you may modify the contents of environ.)
+ transaction - Enclosing Transaction object.
+ environ - Environment variables, as passed from WSGI adapter.
+ method - Request method.
+ publisherScriptName - SCRIPT_NAME of Publisher.
+ scriptName - SCRIPT_NAME for request.
+ pathInfo - PATH_INFO for request.
+ form - Dictionary containing data from query string and/or POST request.
+ cookie - Cookie from request.
+ """
+ def __init__(self, transaction, environ):
+ self._transaction = transaction
+
+ self._environ = environ
+ self._publisherScriptName = environ.get('SCRIPT_NAME', '')
+
+ self._form = {}
+ self._parseFormData()
+
+ # Is this safe? Will it ever raise exceptions?
+ self._cookie = SimpleCookie(environ.get('HTTP_COOKIE', None))
+
+ def _parseFormData(self):
+ """
+ Fills self._form with data from a FieldStorage. May be overidden to
+ provide custom form processing. (Like no processing at all!)
+ """
+ # Parse query string and/or POST data.
+ form = FieldStorage(fp=self._environ['wsgi.input'],
+ environ=self._environ, keep_blank_values=1)
+
+ # Collapse FieldStorage into a simple dict.
+ for field in form.list:
+ # Wrap uploaded files
+ if field.filename:
+ val = File(field)
+ else:
+ val = field.value
+
+ # Add File/value to args, constructing a list if there are
+ # multiple values.
+ if self._form.has_key(field.name):
+ self._form[field.name].append(val)
+ else:
+ self._form[field.name] = [val]
+
+ # Unwrap lists with a single item.
+ for name,val in self._form.items():
+ if len(val) == 1:
+ self._form[name] = val[0]
+
+ def _get_transaction(self):
+ return self._transaction
+ transaction = property(_get_transaction, None, None,
+ "Transaction associated with this Request")
+
+ def _get_environ(self):
+ return self._environ
+ environ = property(_get_environ, None, None,
+ "Environment variables passed from adapter")
+
+ def _get_method(self):
+ return self._environ['REQUEST_METHOD']
+ method = property(_get_method, None, None,
+ "Request method")
+
+ def _get_publisherScriptName(self):
+ return self._publisherScriptName
+ publisherScriptName = property(_get_publisherScriptName, None, None,
+ 'SCRIPT_NAME of Publisher')
+
+ def _get_scriptName(self):
+ return self._environ.get('SCRIPT_NAME', '')
+ scriptName = property(_get_scriptName, None, None,
+ "SCRIPT_NAME of request")
+
+ def _get_pathInfo(self):
+ return self._environ.get('PATH_INFO', '')
+ pathInfo = property(_get_pathInfo, None, None,
+ "PATH_INFO of request")
+
+ def _get_form(self):
+ return self._form
+ form = property(_get_form, None, None,
+ "Parsed GET/POST data")
+
+ def _get_cookie(self):
+ return self._cookie
+ cookie = property(_get_cookie, None, None,
+ "Cookie received from client")
+
+class Response(object):
+ """
+ Encapsulates response-related data.
+
+ Supported attributes:
+ transaction - Enclosing Transaction object.
+ status - Response status code (and message).
+ headers - Response headers.
+ cookie - Response cookie.
+ contentType - Content type of body.
+ body - Response body. Must be an iterable that yields strings.
+
+ Since there are multiple ways of passing response info back to
+ Publisher, here is their defined precedence:
+ status - headers['Status'] first, then status
+ cookie - Any values set in this cookie are added to the headers in
+ addition to existing Set-Cookie headers.
+ contentType - headers['Content-Type'] first, then contentType. If
+ neither are specified, defaults to 'text/html'.
+ body - Return of function takes precedence. If function returns None,
+ body is used instead.
+ """
+ _initialHeaders = {
+ 'Pragma': 'no-cache',
+ 'Cache-Control': 'no-cache'
+ }
+
+ def __init__(self, transaction):
+ self._transaction = transaction
+
+ self._status = '200 OK'
+
+ # Initial response headers.
+ self._headers = header_dict(self._initialHeaders)
+
+ self._cookie = SimpleCookie()
+
+ self.body = []
+
+ def _get_transaction(self):
+ return self._transaction
+ transaction = property(_get_transaction, None, None,
+ "Transaction associated with this Response")
+
+ def _get_status(self):
+ status = self._headers.get('Status')
+ if status is not None:
+ return status
+ return self._status
+ def _set_status(self, value):
+ if self._headers.has_key('Status'):
+ self._headers['Status'] = value
+ else:
+ self._status = value
+ status = property(_get_status, _set_status, None,
+ 'Response status')
+
+ def _get_headers(self):
+ return self._headers
+ headers = property(_get_headers, None, None,
+ "Headers to send in response")
+
+ def _get_cookie(self):
+ return self._cookie
+ cookie = property(_get_cookie, None, None,
+ "Cookie to send in response")
+
+ def _get_contentType(self):
+ return self._headers.get('Content-Type', 'text/html')
+ def _set_contentType(self, value):
+ self._headers['Content-Type'] = value
+ contentType = property(_get_contentType, _set_contentType, None,
+ 'Content-Type of the response body')
+
+class Transaction(object):
+ """
+ Encapsulates objects associated with a single transaction (Request,
+ Response, and possibly a Session).
+
+ Public attributes: (all read-only)
+ request - Request object.
+ response - Response object.
+
+ If Publisher sits on top of SessionMiddleware, the public API of
+ SessionService is also available through the Transaction object.
+ """
+ _requestClass = Request
+ _responseClass = Response
+
+ def __init__(self, publisher, environ):
+ self._publisher = publisher
+ self._request = self._requestClass(self, environ)
+ self._response = self._responseClass(self)
+
+ # Session support.
+ self._sessionService = environ.get('com.saddi.service.session')
+ if self._sessionService is not None:
+ self.encodeURL = self._sessionService.encodeURL
+
+ def _get_request(self):
+ return self._request
+ request = property(_get_request, None, None,
+ "Request associated with this Transaction")
+
+ def _get_response(self):
+ return self._response
+ response = property(_get_response, None, None,
+ "Response associated with this Transaction")
+
+ # Export SessionService API
+
+ def _get_session(self):
+ assert self._sessionService is not None, 'No session service found'
+ return self._sessionService.session
+ session = property(_get_session, None, None,
+ 'Returns the Session object associated with this '
+ 'client')
+
+ def _get_hasSession(self):
+ assert self._sessionService is not None, 'No session service found'
+ return self._sessionService.hasSession
+ hasSession = property(_get_hasSession, None, None,
+ 'True if a Session currently exists for this client')
+
+ def _get_isSessionNew(self):
+ assert self._sessionService is not None, 'No session service found'
+ return self._sessionService.isSessionNew
+ isSessionNew = property(_get_isSessionNew, None, None,
+ 'True if the Session was created in this '
+ 'transaction')
+
+ def _get_hasSessionExpired(self):
+ assert self._sessionService is not None, 'No session service found'
+ return self._sessionService.hasSessionExpired
+ hasSessionExpired = property(_get_hasSessionExpired, None, None,
+ 'True if the client was associated with a '
+ 'non-existent Session')
+
+ def _get_encodesSessionInURL(self):
+ assert self._sessionService is not None, 'No session service found'
+ return self._sessionService.encodesSessionInURL
+ def _set_encodesSessionInURL(self, value):
+ assert self._sessionService is not None, 'No session service found'
+ self._sessionService.encodesSessionInURL = value
+ encodesSessionInURL = property(_get_encodesSessionInURL,
+ _set_encodesSessionInURL, None,
+ 'True if the Session ID should be encoded '
+ 'in the URL')
+
+ def prepare(self):
+ """
+ Called before resolved function is invoked. If overridden,
+ super's prepare() MUST be called and it must be called first.
+ """
+ # Pass form values as keyword arguments.
+ args = dict(self._request.form)
+
+ # Pass Transaction to function if it wants it.
+ args['transaction'] = args['trans'] = self
+
+ self._args = args
+
+ def call(self, func, args):
+ """
+ May be overridden to provide custom exception handling and/or
+ per-request additions, e.g. opening a database connection,
+ starting a transaction, etc.
+ """
+ # Trim down keywords to only what the callable expects.
+ expected, varkw = getexpected(func)
+ trimkw(args, expected, varkw)
+
+ return func(**args)
+
+ def run(self, func):
+ """
+ Execute the function, doing any Action processing and also
+ post-processing the function/Action's return value.
+ """
+ try:
+ # Call the function.
+ result = self.call(func, self._args)
+ except Action, a:
+ # Caught an Action, have it do whatever it does
+ result = a.run(self)
+
+ response = self._response
+ headers = response.headers
+
+ if result is not None:
+ if type(result) in types.StringTypes:
+ assert type(result) is str, 'result cannot be unicode!'
+
+ # Set Content-Length, if needed
+ if not headers.has_key('Content-Length'):
+ headers['Content-Length'] = str(len(result))
+
+ # Wrap in list for WSGI
+ response.body = [result]
+ else:
+ if __debug__:
+ try:
+ iter(result)
+ except TypeError:
+ raise AssertionError, 'result not iterable!'
+ response.body = result
+
+ # If result was None, assume response.body was appropriately set.
+
+ def finish(self):
+ """
+ Called after resolved function returns, but before response is
+ sent. If overridden, super's finish() should be called last.
+ """
+ response = self._response
+ headers = response.headers
+
+ # Copy cookie to headers.
+ items = response.cookie.items()
+ items.sort()
+ for name,morsel in items:
+ headers.add('Set-Cookie', morsel.OutputString())
+
+ # If there is a Status header, transfer its value to response.status.
+ # It must not remain in the headers!
+ status = headers.get('Status', NoDefault)
+ if status is not NoDefault:
+ del headers['Status']
+ response.status = status
+
+ code = int(response.status[:3])
+ # Check if it's a response that must not include a body.
+ # (See 4.3 in RFC2068.)
+ if code / 100 == 1 or code in (204, 304):
+ # Remove any trace of the response body, if that's the case.
+ for header,value in headers.items():
+ if header.lower().startswith('content-'):
+ del headers[header]
+ response.body = []
+ else:
+ if self._request.method == 'HEAD':
+ # HEAD reponse must not return a body (but the headers must be
+ # kept intact).
+ response.body = []
+
+ # Add Content-Type, if missing.
+ if not headers.has_key('Content-Type'):
+ headers['Content-Type'] = response.contentType
+
+ # If we have a close() method, ensure that it is called.
+ if hasattr(self, 'close'):
+ response.body = _addClose(response.body, self.close)
+
+class Publisher(object):
+ """
+ WSGI application that publishes Python functions as web pages. Constructor
+ takes an instance of a concrete subclass of Resolver, which is responsible
+ for mapping requests to functions.
+
+ Query string/POST data values are passed to the function as keyword
+ arguments. If the function does not support variable keywords (e.g.
+ does not have a ** parameter), the function will only be passed
+ keywords which it expects. It is recommended that all keyword parameters
+ have defaults so that missing form data does not raise an exception.
+
+ A Transaction instance is always passed to the function via the
+ "transaction" or "trans" keywords. See the Transaction, Request, and
+ Response classes.
+
+ Valid return types for the function are: a string, None, or an iterable
+ that yields strings. If returning None, it is expected that Response.body
+ has been appropriately set. (It must be an iterable that yields strings.)
+
+ An instance of Publisher itself is the WSGI application.
+ """
+ _transactionClass = Transaction
+
+ def __init__(self, resolver, transactionClass=None):
+ self._resolver = resolver
+
+ if transactionClass is not None:
+ self._transactionClass = transactionClass
+
+ def _get_resolver(self):
+ return self._resolver
+ resolver = property(_get_resolver, None, None,
+ 'Associated Resolver for this Publisher')
+
+ def __call__(self, environ, start_response):
+ """
+ WSGI application interface. Creates a Transaction (which does most
+ of the work) and sends the response.
+ """
+ # Set up a Transaction.
+ transaction = self._transactionClass(self, environ)
+
+ # Make any needed preparations.
+ transaction.prepare()
+
+ redirect = False
+
+ while True:
+ # Attempt to resolve the function.
+ func = self._resolver.resolve(transaction.request,
+ redirect=redirect)
+ if func is None:
+ return self._error404(environ, start_response)
+
+ try:
+ # Call the function.
+ transaction.run(func)
+ except InternalRedirect, r:
+ # Internal redirect. Set up environment and resolve again.
+ environ['SCRIPT_NAME'] = transaction.request.publisherScriptName
+ environ['PATH_INFO'] = r.pathInfo
+ redirect = True
+ else:
+ break
+
+ # Give Transaction a chance to do modify/add to the response.
+ transaction.finish()
+
+ # Transform headers into a list. (Need to pay attention to
+ # multiple values.)
+ responseHeaders = []
+ for key,value in transaction.response.headers.items():
+ if type(value) is list:
+ for v in value:
+ responseHeaders.append((key, v))
+ else:
+ responseHeaders.append((key, value))
+
+ start_response(transaction.response.status, responseHeaders)
+ return transaction.response.body
+
+ def _error404(self, environ, start_response):
+ """Error page to display when resolver fails."""
+ start_response('404 Not Found', [('Content-Type', 'text/html')])
+ request_uri = environ.get('REQUEST_URI')
+ if request_uri is None:
+ request_uri = environ.get('SCRIPT_NAME', '') + \
+ environ.get('PATH_INFO', '')
+ return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html><head>
+<title>404 Not Found</title>
+</head><body>
+<h1>Not Found</h1>
+The requested URL %s was not found on this server.<p>
+<hr>
+%s</body></html>
+""" % (request_uri, environ.get('SERVER_SIGNATURE', ''))]
+
+class File(object):
+ """
+ Wrapper so we can identify uploaded files.
+ """
+ def __init__(self, field):
+ self.filename = field.filename
+ self.file = field.file
+ self.type = field.type
+ self.type_options = field.type_options
+ self.headers = field.headers
+
+class Action(Exception):
+ """
+ Abstract base class for 'Actions', which are just exceptions.
+ Within Publisher, Actions have no inherent meaning and are used
+ as a shortcut to perform specific actions where it's ok for a
+ function to abruptly halt. (Like redirects or displaying an
+ error page.)
+
+ I don't know if using exceptions this way is good form (something
+ tells me no ;) Their usage isn't really required, but they're
+ convenient in some situations.
+ """
+ def run(self, transaction):
+ """Override to perform your action."""
+ raise NotImplementedError, self.__class__.__name__ + '.run'
+
+class Redirect(Action):
+ """
+ Redirect to the given URL.
+ """
+ def __init__(self, url, permanent=False):
+ self._url = url
+ self._permanent = permanent
+
+ def run(self, transaction):
+ response = transaction.response
+ response.status = self._permanent and '301 Moved Permanently' or \
+ '302 Moved Temporarily'
+ response.headers.reset()
+ response.headers['Location'] = self._url
+ response.contentType = 'text/html'
+ return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
+<html><head>
+<title>%s</title>
+</head><body>
+<h1>Found</h1>
+<p>The document has moved <a href="%s">here</a>.</p>
+<hr>
+%s</body></html>
+""" % (response.status, self._url,
+ transaction.request.environ.get('SERVER_SIGNATURE', ''))]
+
+class InternalRedirect(Exception):
+ """
+ An internal redirect using a new PATH_INFO (relative to Publisher's
+ SCRIPT_NAME).
+
+ When handling an InternalRedirect, the behavior of all included
+ Resolvers is to expose a larger set of callables (that would normally
+ be hidden). Therefore, it is important that you set pathInfo securely -
+ preferably, it should not depend on any data from the request. Ideally,
+ pathInfo should be a constant string.
+ """
+ def __init__(self, pathInfo):
+ self.pathInfo = pathInfo
+
+class FieldStorage(cgi.FieldStorage):
+ def __init__(self, *args, **kw):
+ """
+ cgi.FieldStorage only parses the query string during a GET or HEAD
+ request. Fix this.
+ """
+ cgi.FieldStorage.__init__(self, *args, **kw)
+
+ environ = kw.get('environ') or os.environ
+ method = environ.get('REQUEST_METHOD', 'GET').upper()
+ if method not in ('GET', 'HEAD'): # cgi.FieldStorage already parsed?
+ qs = environ.get('QUERY_STRING')
+ if qs:
+ if self.list is None:
+ self.list = []
+ for key,value in cgi.parse_qsl(qs, self.keep_blank_values,
+ self.strict_parsing):
+ self.list.append(cgi.MiniFieldStorage(key, value))
+
+class header_dict(dict):
+ """
+ This is essentially a case-insensitive dictionary, with some additions
+ geared towards supporting HTTP headers (like __str__(), add(), and
+ reset()).
+ """
+ def __init__(self, val=None):
+ """
+ If initialized with an existing dictionary, calling reset() will
+ reset our contents back to that initial dictionary.
+ """
+ dict.__init__(self)
+ self._keymap = {}
+ if val is None:
+ val = {}
+ self.update(val)
+ self._reset_state = dict(val)
+
+ def __contains__(self, key):
+ return key.lower() in self._keymap
+
+ def __delitem__(self, key):
+ lower_key = key.lower()
+ real_key = self._keymap.get(lower_key)
+ if real_key is None:
+ raise KeyError, key
+ del self._keymap[lower_key]
+ dict.__delitem__(self, real_key)
+
+ def __getitem__(self, key):
+ lower_key = key.lower()
+ real_key = self._keymap.get(lower_key)
+ if real_key is None:
+ raise KeyError, key
+ return dict.__getitem__(self, real_key)
+
+ def __str__(self):
+ """Output as HTTP headers."""
+ s = ''
+ for k,v in self.items():
+ if type(v) is list:
+ for i in v:
+ s += '%s: %s\n' % (k, i)
+ else:
+ s += '%s: %s\n' % (k, v)
+ return s
+
+ def __setitem__(self, key, value):
+ lower_key = key.lower()
+ real_key = self._keymap.get(lower_key)
+ if real_key is None:
+ self._keymap[lower_key] = key
+ dict.__setitem__(self, key, value)
+ else:
+ dict.__setitem__(self, real_key, value)
+
+ def clear(self):
+ self._keymap.clear()
+ dict.clear(self)
+
+ def copy(self):
+ c = self.__class__(self)
+ c._reset_state = self._reset_state
+ return c
+
+ def get(self, key, failobj=None):
+ lower_key = key.lower()
+ real_key = self._keymap.get(lower_key)
+ if real_key is None:
+ return failobj
+ return dict.__getitem__(self, real_key)
+
+ def has_key(self, key):
+ return key.lower() in self._keymap
+
+ def setdefault(self, key, failobj=None):
+ lower_key = key.lower()
+ real_key = self._keymap.get(lower_key)
+ if real_key is None:
+ self._keymap[lower_key] = key
+ dict.__setitem__(self, key, failobj)
+ return failobj
+ else:
+ return dict.__getitem__(self, real_key)
+
+ def update(self, d):
+ for k,v in d.items():
+ self[k] = v
+
+ def add(self, key, value):
+ """
+ Add a new header value. Does not overwrite previous value of header
+ (in contrast to __setitem__()).
+ """
+ if self.has_key(key):
+ if type(self[key]) is list:
+ self[key].append(value)
+ else:
+ self[key] = [self[key], value]
+ else:
+ self[key] = value
+
+ def reset(self):
+ """Reset contents to that at the time this instance was created."""
+ self.clear()
+ self.update(self._reset_state)
+
+def _addClose(appIter, closeFunc):
+ """
+ Wraps an iterator so that its close() method calls closeFunc. Respects
+ the existence of __len__ and the iterator's own close() method.
+
+ Need to use metaclass magic because __len__ and next are not
+ recognized unless they're part of the class. (Can't assign at
+ __init__ time.)
+ """
+ class metaIterWrapper(type):
+ def __init__(cls, name, bases, clsdict):
+ super(metaIterWrapper, cls).__init__(name, bases, clsdict)
+ if hasattr(appIter, '__len__'):
+ cls.__len__ = appIter.__len__
+ cls.next = iter(appIter).next
+ if hasattr(appIter, 'close'):
+ def _close(self):
+ appIter.close()
+ closeFunc()
+ cls.close = _close
+ else:
+ cls.close = closeFunc
+
+ class iterWrapper(object):
+ __metaclass__ = metaIterWrapper
+ def __iter__(self):
+ return self
+
+ return iterWrapper()
+
+# Utilities which may be useful outside of Publisher? Perhaps for decorators...
+
+def getexpected(func):
+ """
+ Returns as a 2-tuple the passed in object's expected arguments and
+ whether or not it accepts variable keywords.
+ """
+ assert callable(func), 'object not callable'
+
+ if not inspect.isclass(func):
+ # At this point, we assume func is either a function, method, or
+ # callable instance.
+ if not inspect.isfunction(func) and not inspect.ismethod(func):
+ func = getattr(func, '__call__') # When would this fail?
+
+ argspec = inspect.getargspec(func)
+ expected, varkw = argspec[0], argspec[2] is not None
+ if inspect.ismethod(func):
+ expected = expected[1:]
+ else:
+ # A class. Try to figure out the calling conventions of the
+ # constructor.
+ init = getattr(func, '__init__', None)
+ # Sigh, this is getting into the realm of black magic...
+ if init is not None and inspect.ismethod(init):
+ argspec = inspect.getargspec(init)
+ expected, varkw = argspec[0], argspec[2] is not None
+ expected = expected[1:]
+ else:
+ expected, varkw = [], False
+
+ return expected, varkw
+
+def trimkw(kw, expected, varkw):
+ """
+ If necessary, trims down a dictionary of keyword arguments to only
+ what's expected.
+ """
+ if not varkw: # Trimming only necessary if it doesn't accept variable kw
+ for name in kw.keys():
+ if name not in expected:
+ del kw[name]
diff --git a/flup/resolver/__init__.py b/flup/resolver/__init__.py
index 792d600..bc9c7bf 100644
--- a/flup/resolver/__init__.py
+++ b/flup/resolver/__init__.py
@@ -1 +1,2 @@
-#
+from resolver import *
+del resolver
diff --git a/flup/resolver/complex.py b/flup/resolver/complex.py
new file mode 100644
index 0000000..c3016ea
--- /dev/null
+++ b/flup/resolver/complex.py
@@ -0,0 +1,112 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+import re
+
+from resolver import *
+
+__all__ = ['ComplexResolver']
+
+class ComplexResolver(Resolver):
+ """
+ A meta-Resolver that allows you to "graft" different resolvers at various
+ points in your URL space.
+
+ It works as follows: given a PATH_INFO, it will try all matching
+ resolvers, starting with the most specific. The first function returned
+ by a resolver is returned as the result.
+
+ If no matching resolvers return a function, then the search is
+ considered a failure.
+
+ Assumes that none of the registered resolvers modify environ when
+ they fail to resolve.
+
+ Upon successful resolution, SCRIPT_NAME will contain the path up to
+ and including the resolved function (as determined by the resolver) and
+ PATH_INFO will contain all remaining components.
+ """
+ _slashRE = re.compile(r'''/{2,}''')
+
+ def __init__(self):
+ self.resolverMap = {}
+
+ def _canonicalUrl(self, url):
+ if not url: # Represents default
+ return url
+
+ # Get rid of adjacent slashes
+ url = self._slashRE.sub('/', url)
+
+ # No trailing slash
+ if url.endswith('/'):
+ url = url[:-1]
+
+ # Make sure it starts with a slash
+ if not url.startswith('/'):
+ url = '/' + url
+
+ return url
+
+ def addResolver(self, url, resolver):
+ """
+ Registers a resolver at a particular URL. The empty URL ''
+ represents the default resolver. It will be matched when no
+ other matching resolvers are found.
+ """
+ url = self._canonicalUrl(url)
+ self.resolverMap[url] = resolver
+
+ def removeResolver(self, url):
+ """Removes the resolver at a particular URL."""
+ url = self._canonicalUrl(url)
+ del self.resolverMap[url]
+
+ def resolve(self, request, redirect=False):
+ orig_script_name = request.scriptName
+ orig_path_info = path_info = request.pathInfo
+ path_info = path_info.split(';')[0]
+ path_info = path_info.split('/')
+
+ assert len(path_info) > 0
+ assert not path_info[0]
+
+ while path_info:
+ try_path_info = '/'.join(path_info)
+ resolver = self.resolverMap.get(try_path_info)
+ if resolver is not None:
+ self._updatePath(request, len(path_info) - 1)
+ func = resolver.resolve(request, redirect)
+ if func is not None:
+ return func
+ request.environ['SCRIPT_NAME'] = orig_script_name
+ request.environ['PATH_INFO'] = orig_path_info
+ path_info.pop()
+
+ return None
diff --git a/flup/resolver/function.py b/flup/resolver/function.py
new file mode 100644
index 0000000..537caa3
--- /dev/null
+++ b/flup/resolver/function.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+from resolver import *
+
+__all__ = ['FunctionResolver']
+
+class FunctionResolver(Resolver):
+ """
+ Very basic, almost brain-dead Resolver. Simply resolves to the passed
+ in function, no matter what. :)
+
+ Can be used as a decorator and might actually have uses when used with
+ the ComplexResolver.
+ """
+ def __init__(self, func):
+ self._func = func
+
+ def resolve(self, request, redirect=False):
+ return self._func
diff --git a/flup/resolver/importingmodule.py b/flup/resolver/importingmodule.py
new file mode 100644
index 0000000..ba2c18c
--- /dev/null
+++ b/flup/resolver/importingmodule.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+import sys
+import os
+import imp
+
+from resolver import *
+
+__all__ = ['ImportingModuleResolver']
+
+class ImportingModuleResolver(Resolver):
+ """
+ Constructor takes a directory name or a list of directories. Interprets
+ the first two components of PATH_INFO as 'module/function'. Modules
+ are imported as needed and must reside in the directories specified.
+ Module and function names beginning with underscore are ignored.
+
+ If the 'module' part of PATH_INFO is missing, it is assumed to be
+ self.default_module.
+
+ If the 'function' part of PATH_INFO is missing, it is assumed to be
+ self.index_page.
+
+ Upon successful resolution, appends the module and function names to
+ SCRIPT_NAME and updates PATH_INFO as the remaining components of the path.
+
+ NB: I would recommend explicitly setting all modules' __all__ list.
+ Otherwise, be sure all the names of module-level callables that you
+ don't want exported begin with underscore.
+ """
+ # No default module by default.
+ default_module = None
+ index_page = 'index'
+
+ def __init__(self, path, defaultModule=NoDefault, index=NoDefault):
+ self.path = path
+ if defaultModule is not NoDefault:
+ self.default_module = defaultModule
+ if index is not NoDefault:
+ self.index_page = index
+
+ def resolve(self, request, redirect=False):
+ path_info = request.pathInfo.split(';')[0]
+ path_info = path_info.split('/')
+
+ assert len(path_info) > 0
+ assert not path_info[0]
+
+ while len(path_info) < 3:
+ path_info.append('')
+
+ module_name, func_name = path_info[1:3]
+
+ if not module_name:
+ module_name = self.default_module
+
+ if not func_name:
+ func_name = self.index_page
+
+ module = None
+ if module_name and (module_name[0] != '_' or redirect) and \
+ not module_name.count('.'):
+ try:
+ module = _import_module(module_name, path=self.path)
+ except:
+ pass
+
+ if module is not None:
+ if func_name and (func_name[0] != '_' or redirect):
+ module_all = getattr(module, '__all__', None)
+ if module_all is None or func_name in module_all or redirect:
+ func = getattr(module, func_name, None)
+ if callable(func):
+ self._updatePath(request, 2)
+ return func
+
+ return None
+
+def _import_module(name, path=None):
+ """
+ Imports a module. If path is None, module will be searched for in
+ sys.path. If path is given (which may be a single string or a list),
+ the module will only be searched for in those directories.
+ """
+ if path is not None and type(path) is not list:
+ path = [path]
+
+ module = sys.modules.get(name)
+ if module is not None:
+ module_file = getattr(module, '__file__')
+ if module_file is None or \
+ (path is not None and os.path.dirname(module_file) not in path):
+ return None
+
+ return module
+
+ fp, pathname, description = imp.find_module(name, path)
+ try:
+ return imp.load_module(name, fp, pathname, description)
+ finally:
+ if fp:
+ fp.close()
diff --git a/flup/resolver/module.py b/flup/resolver/module.py
new file mode 100644
index 0000000..e0117a8
--- /dev/null
+++ b/flup/resolver/module.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+from resolver import *
+
+__all__ = ['ModuleResolver']
+
+class ModuleResolver(Resolver):
+ """
+ Exposes all top-level callables within a module. The module's __all__
+ attribute is respected, if it exists. Names beginning with underscore
+ are ignored.
+
+ Uses the first component of PATH_INFO as the callable's name and if
+ empty, will instead use self.index_page.
+
+ Upon successful resolution, appends the callable's name to SCRIPT_NAME
+ and updates PATH_INFO as the remaining components of the path.
+
+ NB: I would recommend explicitly setting the module's __all__ list.
+ Otherwise, be sure all the names of module-level callables that you
+ don't want exported begin with underscore.
+ """
+ index_page = 'index'
+
+ def __init__(self, module, index=NoDefault):
+ self.module = module
+ if index is not NoDefault:
+ self.index_page = index
+
+ def resolve(self, request, redirect=False):
+ path_info = request.pathInfo.split(';')[0]
+ path_info = path_info.split('/')
+
+ assert len(path_info) > 0
+ assert not path_info[0]
+
+ if len(path_info) < 2:
+ path_info.append('')
+
+ func_name = path_info[1]
+
+ if func_name:
+ if func_name[0] == '_' and not redirect:
+ func_name = None
+ else:
+ func_name = self.index_page
+
+ if func_name:
+ module_all = getattr(self.module, '__all__', None)
+ if module_all is None or func_name in module_all or redirect:
+ func = getattr(self.module, func_name, None)
+ if callable(func):
+ self._updatePath(request, 1)
+ return func
+
+ return None
diff --git a/flup/resolver/nopathinfo.py b/flup/resolver/nopathinfo.py
new file mode 100644
index 0000000..e0f3182
--- /dev/null
+++ b/flup/resolver/nopathinfo.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+from resolver import *
+
+__all__ = ['NoPathInfoResolver']
+
+class NoPathInfoResolver(Resolver):
+ """
+ Another meta-resolver. Disallows the existence of PATH_INFO (beyond
+ what's needed to resolve the function). Optionally allows a trailing
+ slash.
+ """
+ def __init__(self, resolver, allowTrailingSlash=False):
+ self._resolver = resolver
+ self._allowTrailingSlash = allowTrailingSlash
+
+ def resolve(self, request, redirect=False):
+ orig_script_name, orig_path_info = request.scriptName, request.pathInfo
+ func = self._resolver.resolve(request, redirect)
+ try:
+ if func is not None:
+ path_info = request.pathInfo.split(';')[0]
+ if path_info and \
+ (not self._allowTrailingSlash or path_info != '/'):
+ func = None
+ return func
+ finally:
+ if func is None:
+ request.environ['SCRIPT_NAME'] = orig_script_name
+ request.environ['PATH_INFO'] = orig_path_info
diff --git a/flup/resolver/objectpath.py b/flup/resolver/objectpath.py
new file mode 100644
index 0000000..aa1b21a
--- /dev/null
+++ b/flup/resolver/objectpath.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+import re
+
+from resolver import *
+
+__all__ = ['ObjectPathResolver', 'expose']
+
+class ObjectPathResolver(Resolver):
+ """
+ Inspired by CherryPy <http://www.cherrypy.org/>. :) For an explanation
+ of how this works, see the excellent tutorial at
+ <http://www.cherrypy.org/wiki/CherryPyTutorial>. We support the index and
+ default methods, though the calling convention for the default method
+ is different - we do not pass PATH_INFO as positional arguments. (It
+ is passed through the request/environ as normal.)
+
+ Also, we explicitly block certain function names. See below. I don't
+ know if theres really any harm in letting those attributes be followed,
+ but I'd rather not take the chance. And unfortunately, this solution
+ is pretty half-baked as well (I'd rather only allow certain object
+ types to be traversed, rather than disallow based on names.) Better
+ than nothing though...
+ """
+ index_page = 'index'
+ default_page = 'default'
+
+ def __init__(self, root, index=NoDefault, default=NoDefault,
+ favorIndex=True):
+ """
+ root is the root object of your URL hierarchy. In CherryPy, this
+ would be cpg.root.
+
+ When the last component of a path has an index method and some
+ object along the path has a default method, favorIndex determines
+ which method is called when the URL has a trailing slash. If
+ True, the index method will be called. Otherwise, the default method.
+ """
+ self.root = root
+ if index is not NoDefault:
+ self.index_page = index
+ if default is not NoDefault:
+ self.default_page = default
+ self._favorIndex = favorIndex
+
+ # Certain names should be disallowed for safety. If one of your pages
+ # is showing up unexpectedly as a 404, make sure the function name doesn't
+ # begin with one of these prefixes.
+ _disallowed = re.compile(r'''(?:_|im_|func_|tb_|f_|co_).*''')
+
+ def _exposed(self, obj, redirect):
+ # If redirecting, allow non-exposed objects as well.
+ return callable(obj) and (getattr(obj, 'exposed', False) or redirect)
+
+ def resolve(self, request, redirect=False):
+ path_info = request.pathInfo.split(';')[0]
+ path_info = path_info.split('/')
+
+ assert len(path_info) > 0
+ assert not path_info[0]
+
+ current = self.root
+ current_default = None
+ i = 0
+ for i in range(1, len(path_info)):
+ component = path_info[i]
+
+ # See if we have an index page (needed for index/default
+ # disambiguation, unfortunately).
+ current_index = None
+ if self.index_page:
+ current_index = getattr(current, self.index_page, None)
+ if not self._exposed(current_index, redirect):
+ current_index = None
+
+ if self.default_page:
+ # Remember the last default page we've seen.
+ new_default = getattr(current, self.default_page, None)
+ if self._exposed(new_default, redirect):
+ current_default = (i - 1, new_default)
+
+ # Test for trailing slash.
+ if not component and current_index is not None and \
+ (self._favorIndex or current_default is None):
+ # Breaking out of the loop here favors index over default.
+ break
+
+ # Respect __all__ attribute. (Ok to generalize to all objects?)
+ all = getattr(current, '__all__', None)
+
+ current = getattr(current, component, None)
+ # Path doesn't exist
+ if current is None or self._disallowed.match(component) or \
+ (all is not None and component not in all and not redirect):
+ # Use path up to latest default page.
+ if current_default is not None:
+ i, current = current_default
+ break
+ # No default at all, so we fail.
+ return None
+
+ func = None
+ if self._exposed(current, redirect): # Exposed?
+ func = current
+ else:
+ # If not, see if it as an exposed index page
+ if self.index_page:
+ index = getattr(current, self.index_page, None)
+ if self._exposed(index, redirect): func = index
+ # How about a default page?
+ if func is None and self.default_page:
+ default = getattr(current, self.default_page, None)
+ if self._exposed(default, redirect): func = default
+ # Lastly, see if we have an ancestor's default page to fall back on.
+ if func is None and current_default is not None:
+ i, func = current_default
+
+ if func is not None:
+ self._updatePath(request, i)
+
+ return func
+
+def expose(func):
+ """Decorator to expose functions."""
+ func.exposed = True
+ return func
diff --git a/flup/resolver/resolver.py b/flup/resolver/resolver.py
new file mode 100644
index 0000000..4a61d7d
--- /dev/null
+++ b/flup/resolver/resolver.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $Id$
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision$'
+
+__all__ = ['Resolver']
+
+class Resolver(object):
+ """
+ Abstract base class for 'Resolver' objects. (An instance of which is
+ passed to Publisher's constructor.)
+
+ Given a Request, either return a callable (Publisher expects it to
+ be a function, method, class, or callable instance), or return None.
+ Typically Request.pathInfo is used to resolve the function.
+ Request.environ may be modified by the Resolver, for example, to re-adjust
+ SCRIPT_NAME/PATH_INFO after successful resolution. It is NOT recommended
+ that it be modified if resolution fails.
+
+ When resolving an InternalRedirect, redirect will be True.
+ """
+ def resolve(self, request, redirect=False):
+ raise NotImplementedError, self.__class__.__name__ + '.resolve'
+
+ def _updatePath(self, request, num):
+ """
+ Utility function to update SCRIPT_NAME and PATH_INFO in a sane
+ manner. Transfers num components from PATH_INFO to SCRIPT_NAME.
+ Keeps URL path parameters intact.
+ """
+ assert num >= 0
+ if not num:
+ return # Nothing to do
+ numScriptName = len(request.scriptName.split('/'))
+ totalPath = request.scriptName + request.pathInfo
+ if __debug__:
+ origTotalPath = totalPath
+ # Extract and save params
+ i = totalPath.find(';')
+ if i >= 0:
+ params = totalPath[i:]
+ totalPath = totalPath[:i]
+ else:
+ params = ''
+ totalPath = totalPath.split('/')
+ scriptName = '/'.join(totalPath[:numScriptName + num])
+ pathInfo = '/'.join([''] + totalPath[numScriptName + num:])
+ # SCRIPT_NAME shouldn't have trailing slash
+ if scriptName.endswith('/'):
+ scriptName = scriptName[:-1]
+ # Transfer to PATH_INFO (most likely empty, but just to be safe...)
+ pathInfo = '/' + pathInfo
+ request.environ['SCRIPT_NAME'] = scriptName
+ request.environ['PATH_INFO'] = pathInfo + params
+ assert request.scriptName + request.pathInfo == origTotalPath