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