pylons.controllers.core
Covered: 190 lines
Missed: 28 lines
Skipped 54 lines
Percent: 87 %
  1
"""The core WSGIController"""
  2
import inspect
  3
import logging
  4
import types
  6
from webob.exc import HTTPException, HTTPNotFound
  8
import pylons
 10
__all__ = ['WSGIController']
 12
log = logging.getLogger(__name__)
 15
class WSGIController(object):
 16
    """WSGI Controller that follows WSGI spec for calling and return
 17
    values
 19
    The Pylons WSGI Controller handles incoming web requests that are 
 20
    dispatched from the PylonsBaseWSGIApp. These requests result in a
 21
    new instance of the WSGIController being created, which is then
 22
    called with the dict options from the Routes match. The standard
 23
    WSGI response is then returned with start_response called as per
 24
    the WSGI spec.
 26
    Special WSGIController methods you may define:
 28
    ``__before__``
 29
        This method is called before your action is, and should be used
 30
        for setting up variables/objects, restricting access to other
 31
        actions, or other tasks which should be executed before the
 32
        action is called.
 34
    ``__after__``
 35
        This method is called after the action is, unless an unexpected
 36
        exception was raised. Subclasses of
 37
        :class:`~webob.exc.HTTPException` (such as those raised by
 38
        ``redirect_to`` and ``abort``) are expected; e.g. ``__after__``
 39
        will be called on redirects.
 41
    Each action to be called is inspected with :meth:`_inspect_call` so
 42
    that it is only passed the arguments in the Routes match dict that
 43
    it asks for. The arguments passed into the action can be customized
 44
    by overriding the :meth:`_get_method_args` function which is
 45
    expected to return a dict.
 47
    In the event that an action is not found to handle the request, the
 48
    Controller will raise an "Action Not Found" error if in debug mode,
 49
    otherwise a ``404 Not Found`` error will be returned.
 51
    """
 52
    _pylons_log_debug = False
 54
    def _perform_call(self, func, args):
 55
        """Hide the traceback for everything above this method"""
 56
        __traceback_hide__ = 'before_and_this'
 57
        return func(**args)
 59
    def _inspect_call(self, func):
 60
        """Calls a function with arguments from
 61
        :meth:`_get_method_args`
 63
        Given a function, inspect_call will inspect the function args
 64
        and call it with no further keyword args than it asked for.
 66
        If the function has been decorated, it is assumed that the
 67
        decorator preserved the function signature.
 69
        """
 71
        try:
 72
            cached_argspecs = self.__class__._cached_argspecs
 73
        except AttributeError:
 74
            self.__class__._cached_argspecs = cached_argspecs = {}
 76
        try:
 77
            argspec = cached_argspecs[func.im_func]
 78
        except KeyError:
 79
            argspec = cached_argspecs[func.im_func] = inspect.getargspec(func)
 80
        kargs = self._get_method_args()
 82
        log_debug = self._pylons_log_debug
 83
        c = self._py_object.tmpl_context
 84
        environ = self._py_object.request.environ
 85
        args = None
 87
        if argspec[2]:
 88
            if self._py_object.config['pylons.tmpl_context_attach_args']:
 89
                for k, val in kargs.iteritems():
 90
                    setattr(c, k, val)
 91
            args = kargs
 92
        else:
 93
            args = {}
 94
            argnames = argspec[0][isinstance(func, types.MethodType)
 95
                                  and 1 or 0:]
 96
            for name in argnames:
 97
                if name in kargs:
 98
                    if self._py_object.config['pylons.tmpl_context_attach_args']:
 99
                        setattr(c, name, kargs[name])
100
                    args[name] = kargs[name]
101
        if log_debug:
102
            log.debug("Calling %r method with keyword args: **%r",
103
                      func.__name__, args)
104
        try:
105
            result = self._perform_call(func, args)
106
        except HTTPException, httpe:
107
            if log_debug:
108
                log.debug("%r method raised HTTPException: %s (code: %s)",
109
                          func.__name__, httpe.__class__.__name__,
110
                          httpe.wsgi_response.code, exc_info=True)
111
            result = httpe
114
            environ['pylons.controller.exception'] = httpe
117
            if result.wsgi_response.status_int == 304:
118
                result.wsgi_response.headers.pop('Content-Type', None)
119
            result._exception = True
121
        return result
123
    def _get_method_args(self):
124
        """Retrieve the method arguments to use with inspect call
126
        By default, this uses Routes to retrieve the arguments,
127
        override this method to customize the arguments your controller
128
        actions are called with.
130
        This method should return a dict.
132
        """
133
        req = self._py_object.request
134
        kargs = req.environ['pylons.routes_dict'].copy()
135
        kargs['environ'] = req.environ
136
        kargs['start_response'] = self.start_response
137
        kargs['pylons'] = self._py_object
138
        return kargs
140
    def _dispatch_call(self):
141
        """Handles dispatching the request to the function using
142
        Routes"""
143
        log_debug = self._pylons_log_debug
144
        req = self._py_object.request
145
        try:
146
            action = req.environ['pylons.routes_dict']['action']
147
        except KeyError:
148
            raise Exception("No action matched from Routes, unable to"
149
                            "determine action dispatch.")
150
        action_method = action.replace('-', '_')
151
        if log_debug:
152
            log.debug("Looking for %r method to handle the request",
153
                      action_method)
154
        try:
155
            func = getattr(self, action_method, None)
156
        except UnicodeEncodeError:
157
            func = None
158
        if action_method != 'start_response' and callable(func):
160
            req.environ['pylons.action_method'] = func
162
            response = self._inspect_call(func)
163
        else:
164
            if log_debug:
165
                log.debug("Couldn't find %r method to handle response", action)
166
            if pylons.config['debug']:
167
                raise NotImplementedError('Action %r is not implemented' %
168
                                          action)
169
            else:
170
                response = HTTPNotFound()
171
        return response
173
    def __call__(self, environ, start_response):
174
        """The main call handler that is called to return a response"""
175
        log_debug = self._pylons_log_debug
178
        self._py_object = environ['pylons.pylons']
181
        try:
182
            if environ['pylons.routes_dict']['action'][:1] in ('_', '-'):
183
                if log_debug:
184
                    log.debug("Action starts with _, private action not "
185
                              "allowed. Returning a 404 response")
186
                return HTTPNotFound()(environ, start_response)
187
        except KeyError:
189
            pass
191
        start_response_called = []
192
        def repl_start_response(status, headers, exc_info=None):
193
            response = self._py_object.response
194
            start_response_called.append(None)
197
            if log_debug:
198
                log.debug("Merging pylons.response headers into "
199
                          "start_response call, status: %s", status)
200
            headers.extend(header for header in response.headerlist
201
                           if header[0] == 'Set-Cookie' or
202
                           header[0].startswith('X-'))
203
            return start_response(status, headers, exc_info)
204
        self.start_response = repl_start_response
206
        if hasattr(self, '__before__'):
207
            response = self._inspect_call(self.__before__)
208
            if hasattr(response, '_exception'):
209
                return response(environ, self.start_response)
211
        response = self._dispatch_call()
212
        if not start_response_called:
213
            self.start_response = start_response
214
            py_response = self._py_object.response
217
            if isinstance(response, str):
218
                if log_debug:
219
                    log.debug("Controller returned a string "
220
                              ", writing it to pylons.response")
221
                py_response.body = py_response.body + response
222
            elif isinstance(response, unicode):
223
                if log_debug:
224
                    log.debug("Controller returned a unicode string "
225
                              ", writing it to pylons.response")
226
                py_response.unicode_body = py_response.unicode_body + \
227
                        response
228
            elif hasattr(response, 'wsgi_response'):
230
                if log_debug:
231
                    log.debug("Controller returned a Response object, merging "
232
                              "it with pylons.response")
233
                for name, value in py_response.headers.items():
234
                    if name.lower() == 'set-cookie':
235
                        response.headers.add(name, value)
236
                    else:
237
                        response.headers.setdefault(name, value)
238
                try:
239
                    registry = environ['paste.registry']
240
                    registry.replace(pylons.response, response)
241
                except KeyError:
243
                    pass
244
                py_response = response
245
            elif response is None:
246
                if log_debug:
247
                    log.debug("Controller returned None")
248
            else:
249
                if log_debug:
250
                    log.debug("Assuming controller returned an iterable, "
251
                              "setting it as pylons.response.app_iter")
252
                py_response.app_iter = response
253
            response = py_response
255
        if hasattr(self, '__after__'):
256
            after = self._inspect_call(self.__after__)
257
            if hasattr(after, '_exception'):
258
                return after(environ, self.start_response)
260
        if hasattr(response, 'wsgi_response'):
262
            if 'paste.testing_variables' in environ:
263
                environ['paste.testing_variables']['response'] = response
264
            if log_debug:
265
                log.debug("Calling Response object to return WSGI data")
266
            return response(environ, self.start_response)
268
        if log_debug:
269
            log.debug("Response assumed to be WSGI content, returning "
270
                      "un-touched")
271
        return response