1
"""The core WSGIController"""
6
from webob.exc import HTTPException, HTTPNotFound
10
__all__ = ['WSGIController']
12
log = logging.getLogger(__name__)
15
class WSGIController(object):
16
"""WSGI Controller that follows WSGI spec for calling and return
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
26
Special WSGIController methods you may define:
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
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.
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'
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.
70
# Check to see if the class has a cache of argspecs yet
72
cached_argspecs = self.__class__._cached_argspecs
73
except AttributeError:
74
self.__class__._cached_argspecs = cached_argspecs = {}
77
argspec = cached_argspecs[func.im_func]
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
88
if self._py_object.config['pylons.tmpl_context_attach_args']:
89
for k, val in kargs.iteritems():
94
argnames = argspec[0][isinstance(func, types.MethodType)
98
if self._py_object.config['pylons.tmpl_context_attach_args']:
99
setattr(c, name, kargs[name])
100
args[name] = kargs[name]
102
log.debug("Calling %r method with keyword args: **%r",
105
result = self._perform_call(func, args)
106
except HTTPException, httpe:
108
log.debug("%r method raised HTTPException: %s (code: %s)",
109
func.__name__, httpe.__class__.__name__,
110
httpe.wsgi_response.code, exc_info=True)
113
# Store the exception in the environ
114
environ['pylons.controller.exception'] = httpe
116
# 304 Not Modified's shouldn't have a content-type set
117
if result.wsgi_response.status_int == 304:
118
result.wsgi_response.headers.pop('Content-Type', None)
119
result._exception = True
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.
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
140
def _dispatch_call(self):
141
"""Handles dispatching the request to the function using
143
log_debug = self._pylons_log_debug
144
req = self._py_object.request
146
action = req.environ['pylons.routes_dict']['action']
148
raise Exception("No action matched from Routes, unable to"
149
"determine action dispatch.")
150
action_method = action.replace('-', '_')
152
log.debug("Looking for %r method to handle the request",
155
func = getattr(self, action_method, None)
156
except UnicodeEncodeError:
158
if action_method != 'start_response' and callable(func):
159
# Store function used to handle request
160
req.environ['pylons.action_method'] = func
162
response = self._inspect_call(func)
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' %
170
response = HTTPNotFound()
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
177
# Keep a local reference to the req/response objects
178
self._py_object = environ['pylons.pylons']
180
# Keep private methods private
182
if environ['pylons.routes_dict']['action'][:1] in ('_', '-'):
184
log.debug("Action starts with _, private action not "
185
"allowed. Returning a 404 response")
186
return HTTPNotFound()(environ, start_response)
188
# The check later will notice that there's no action
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)
196
# Copy the headers from the global response
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
215
# If its not a WSGI response, and we have content, it needs to
216
# be wrapped in the response object
217
if isinstance(response, str):
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):
224
log.debug("Controller returned a unicode string "
225
", writing it to pylons.response")
226
py_response.unicode_body = py_response.unicode_body + \
228
elif hasattr(response, 'wsgi_response'):
229
# It's an exception that got tossed.
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)
237
response.headers.setdefault(name, value)
239
registry = environ['paste.registry']
240
registry.replace(pylons.response, response)
242
# Ignore the case when someone removes the registry
244
py_response = response
245
elif response is None:
247
log.debug("Controller returned None")
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'):
261
# Copy the response object into the testing vars if we're testing
262
if 'paste.testing_variables' in environ:
263
environ['paste.testing_variables']['response'] = response
265
log.debug("Calling Response object to return WSGI data")
266
return response(environ, self.start_response)
269
log.debug("Response assumed to be WSGI content, returning "