Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/pylons/pylons/controllers/core.py
0001"""The core WSGIController"""
0002import inspect
0003import logging
0004import types
0005import warnings
0006
0007from paste.httpexceptions import HTTPException
0008from paste.response import HeaderDict
0009from paste.wsgiwrappers import WSGIResponse
0010
0011import pylons
0012
0013__all__ = ['Controller', 'WSGIController']
0014
0015log = logging.getLogger(__name__)
0016
0017class WSGIController(object):
0018    """WSGI Controller that follows WSGI spec for calling and return values
0019    
0020    The Pylons WSGI Controller handles incoming web requests that are 
0021    dispatched from the PylonsBaseWSGIApp. These requests result in a new 
0022    instance of the WSGIController being created, which is then called with the
0023    dict options from the Routes match. The standard WSGI response is then
0024    returned with start_response called as per the WSGI spec.
0025    
0026    By default, the WSGIController will search and attempt to call a 
0027    ``__before__`` method before calling the action, and will try to call a
0028    ``__after__`` method after the action was called. These two methods can act
0029    as filters controlling access to the action, setup variables/objects for 
0030    use with a set of actions, etc.
0031    
0032    Each action to be called is inspected with ``_inspect_call`` so that it is
0033    only passed the arguments in the Routes match dict that it asks for. The
0034    arguments passed into the action can be customized by overriding the 
0035    ``_get_method_args`` function which is expected to return a dict.
0036    
0037    In the event that an action is not found to handle the request, the
0038    Controller will raise an "Action Not Found" error if in debug mode,
0039    otherwise a ``404 Not Found`` error will be returned.
0040    """
0041
0042    __pudge_all__ = ['_inspect_call', '__call__', '_get_method_args',
0043                     '_dispatch_call']
0044
0045    def _inspect_call(self, func):
0046        """Calls a function with arguments from ``_get_method_args``
0047        
0048        Given a function, inspect_call will inspect the function args and call
0049        it with no further keyword args than it asked for.
0050        
0051        If the function has been decorated, it is assumed that the decorator
0052        preserved the function signature.
0053        """
0054        argspec = inspect.getargspec(func)
0055        kargs = self._get_method_args()
0056
0057        # Hide the traceback for everything above this controller
0058        __traceback_hide__ = 'before_and_this'
0059
0060        c = pylons.c._current_obj()
0061        args = None
0062        if argspec[2]:
0063            for k, val in kargs.iteritems():
0064                setattr(c, k, val)
0065            args = kargs
0066        else:
0067            args = {}
0068            argnames = argspec[0][1:]
0069            for name in argnames:
0070                if name in kargs:
0071                    setattr(c, name, kargs[name])
0072                    args[name] = kargs[name]
0073        log.debug("Calling %r method with keyword args: **%r", func.__name__,
0074                  args)
0075        try:
0076            result = func(**args)
0077        except HTTPException, httpe:
0078            log.debug("%r method raised HTTPException: %s (code: %s)",
0079                      func.__name__, httpe.__class__.__name__, httpe.code,
0080                      exc_info=True)
0081            result = httpe.response(pylons.request.environ)
0082            result._exception = True
0083        return result
0084
0085    def _get_method_args(self):
0086        """Retrieve the method arguments to use with inspect call
0087        
0088        By default, this uses Routes to retrieve the arguments, override
0089        this method to customize the arguments your controller actions are
0090        called with.
0091        """
0092        req = pylons.request._current_obj()
0093        kargs = req.environ['pylons.routes_dict'].copy()
0094        kargs['environ'] = req.environ
0095        if hasattr(self, 'start_response'):
0096            kargs['start_response'] = self.start_response
0097        return kargs
0098
0099    def _dispatch_call(self):
0100        """Handles dispatching the request to the function using Routes"""
0101        req = pylons.request._current_obj()
0102        action = req.environ['pylons.routes_dict'].get('action')
0103        action_method = action.replace('-', '_')
0104        log.debug("Looking for %r method to handle the request", action_method)
0105        try:
0106            func = getattr(self, action_method, None)
0107        except UnicodeEncodeError:
0108            func = None
0109        if isinstance(func, types.MethodType):
0110            # Store function used to handle request
0111            req.environ['pylons.action_method'] = func
0112
0113            response = self._inspect_call(func)
0114        else:
0115            log.debug("Couldn't find %r method to handle response", action)
0116            if pylons.config['debug']:
0117                raise NotImplementedError('Action %r is not implemented' %
0118                                          action)
0119            else:
0120                response = WSGIResponse(code=404)
0121        return response
0122
0123    def __call__(self, environ, start_response):
0124        # Keep private methods private
0125        if environ['pylons.routes_dict'].get('action', '').startswith('_'):
0126            log.debug("Action starts with _, private action not allowed. "
0127                      "Returning a 404 response")
0128            return WSGIResponse(code=404)(environ, start_response)
0129
0130        start_response_called = []
0131        def repl_start_response(status, headers, exc_info=None):
0132            response = pylons.response._current_obj()
0133            start_response_called.append(None)
0134
0135            # Copy the headers from the global response
0136            # XXX: TODO: This should really be done with a more efficient 
0137            #            header merging function at some point.
0138            log.debug("Merging pylons.response headers into start_response "
0139                      "call, status: %s", status)
0140            response.headers.update(HeaderDict.fromlist(headers))
0141            headers = response.headers.headeritems()
0142            for c in pylons.response.cookies.values():
0143                headers.append(('Set-Cookie', c.output(header='')))
0144            return start_response(status, headers, exc_info)
0145        self.start_response = repl_start_response
0146
0147        if hasattr(self, '__before__'):
0148            response = self._inspect_call(self.__before__)
0149            if hasattr(response, '_exception'):
0150                return response(environ, self.start_response)
0151
0152        response = self._dispatch_call()
0153        if not start_response_called:
0154            # If its not a WSGI response, and we have content, it needs to
0155            # be wrapped in the response object
0156            if hasattr(response, 'wsgi_response'):
0157                # It's either a legacy WSGIResponse object, or an exception
0158                # that got tossed. 
0159                log.debug("Controller returned a Response object, merging it "
0160                          "with pylons.response")
0161                response.headers.update(pylons.response.headers)
0162                for c in pylons.response.cookies.values():
0163                    response.headers.add('Set-Cookie', c.output(header=''))
0164                registry = environ['paste.registry']
0165                registry.replace(pylons.response, response)
0166            elif isinstance(response, types.GeneratorType):
0167                log.debug("Controller returned a generator, setting it as the "
0168                          "pylons.response content")
0169                pylons.response.content = response
0170            elif response is None:
0171                log.debug("Controller returned None")
0172            else:
0173                log.debug("Assuming controller returned a basestring or "
0174                          "buffer, writing it to pylons.response")
0175                pylons.response.write(response)
0176            response = pylons.response._current_obj()
0177
0178        if hasattr(self, '__after__'):
0179            after = self._inspect_call(self.__after__)
0180            if hasattr(after, '_exception'):
0181                return after(environ, self.start_response)
0182
0183        if hasattr(response, 'wsgi_response'):
0184            # Copy the response object into the testing vars if we're testing
0185            if 'paste.testing_variables' in environ:
0186                environ['paste.testing_variables']['response'] = response
0187            log.debug("Calling Response object to return WSGI data")
0188            return response(environ, self.start_response)
0189
0190        log.debug("Response assumed to be WSGI content, returning un-touched")
0191        return response
0192
0193
0194class Controller(WSGIController):
0195    """Deprecated Pylons Controller for Web Requests
0196    
0197    All Pylons projects should use the WSGIController.
0198    """
0199    def __init__(self, *args, **kwargs):
0200        warnings.warn("Controller class is deprecated, switch to using the"
0201                      "WSGIController class", DeprecationWarning, 2)
0202        WSGIController.__init__(self, *args, **kwargs)
0203
0204    def __call__(self, *args, **kargs):
0205        """Makes our controller a callable to handle requests
0206        
0207        This is called when dispatched to as the Controller class docs explain
0208        more fully.
0209        """
0210        req = pylons.request._current_obj()
0211
0212        # Keep private methods private
0213        if req.environ['pylons.routes_dict'].get('action', '').startswith('_'):
0214            return WSGIResponse(code=404)
0215
0216        if hasattr(self, '__before__'):
0217            self._inspect_call(self.__before__, **kargs)
0218        response = self._dispatch_call()
0219
0220        # If its not a WSGI response, and we have content, it needs to
0221        # be wrapped in the response object
0222        if hasattr(response, 'wsgi_response'):
0223            # It's either a legacy WSGIResponse object, or an exception
0224            # that got tossed. Strip headers if its anything other than a
0225            # 2XX status code, and strip cookies if its anything other than
0226            # a 2XX or 3XX status code.
0227            if response.status_code < 300:
0228                response.headers.update(pylons.response.headers)
0229            if response.status_code < 400:
0230                for c in pylons.response.cookies.values():
0231                    response.headers.add('Set-Cookie', c.output(header=''))
0232            registry = req.environ['paste.registry']
0233            registry.replace(pylons.response, response)
0234        elif isinstance(response, types.GeneratorType):
0235            pylons.response.content = response
0236        elif isinstance(response, basestring):
0237            pylons.response.write(response)
0238        response = pylons.response._current_obj()
0239
0240        if hasattr(self, '__after__'):
0241            self._inspect_call(self.__after__)
0242
0243        return response

Top