commit | author | age
|
c73eb9
|
1 |
from collections import deque |
5cf9fc
|
2 |
import json |
CM |
3 |
|
3b7334
|
4 |
from zope.interface import implementer |
d66bfb
|
5 |
from zope.interface.interface import InterfaceClass |
CM |
6 |
|
bca03f
|
7 |
from webob import BaseRequest |
dfc2b6
|
8 |
|
0c1c39
|
9 |
from pyramid.interfaces import ( |
CM |
10 |
IRequest, |
04cc91
|
11 |
IRequestExtensions, |
0c1c39
|
12 |
IResponse, |
CM |
13 |
ISessionFactory, |
|
14 |
) |
968209
|
15 |
|
0c1c39
|
16 |
from pyramid.compat import ( |
CM |
17 |
text_, |
|
18 |
bytes_, |
|
19 |
native_, |
04cc91
|
20 |
iteritems_, |
0c1c39
|
21 |
) |
CM |
22 |
|
968209
|
23 |
from pyramid.decorator import reify |
330164
|
24 |
from pyramid.i18n import LocalizerRequestMixin |
25c64c
|
25 |
from pyramid.response import Response, _get_response_factory |
0184b5
|
26 |
from pyramid.security import ( |
CM |
27 |
AuthenticationAPIMixin, |
|
28 |
AuthorizationAPIMixin, |
|
29 |
) |
fb90f0
|
30 |
from pyramid.url import URLMethodsMixin |
04cc91
|
31 |
from pyramid.util import ( |
MM |
32 |
InstancePropertyHelper, |
|
33 |
InstancePropertyMixin, |
|
34 |
) |
e40ef2
|
35 |
from pyramid.view import ViewMethodsMixin |
c4e3f4
|
36 |
|
14dc81
|
37 |
class TemplateContext(object): |
CM |
38 |
pass |
9395f0
|
39 |
|
274435
|
40 |
class CallbackMethodsMixin(object): |
37f3ed
|
41 |
@reify |
MM |
42 |
def finished_callbacks(self): |
|
43 |
return deque() |
|
44 |
|
|
45 |
@reify |
|
46 |
def response_callbacks(self): |
|
47 |
return deque() |
|
48 |
|
844e98
|
49 |
def add_response_callback(self, callback): |
CM |
50 |
""" |
|
51 |
Add a callback to the set of callbacks to be called by the |
|
52 |
:term:`router` at a point after a :term:`response` object is |
fd5ae9
|
53 |
successfully created. :app:`Pyramid` does not have a |
844e98
|
54 |
global response object: this functionality allows an |
CM |
55 |
application to register an action to be performed against the |
|
56 |
response once one is created. |
|
57 |
|
|
58 |
A 'callback' is a callable which accepts two positional |
|
59 |
parameters: ``request`` and ``response``. For example: |
|
60 |
|
|
61 |
.. code-block:: python |
|
62 |
:linenos: |
|
63 |
|
|
64 |
def cache_callback(request, response): |
|
65 |
'Set the cache_control max_age for the response' |
|
66 |
response.cache_control.max_age = 360 |
|
67 |
request.add_response_callback(cache_callback) |
|
68 |
|
|
69 |
Response callbacks are called in the order they're added |
|
70 |
(first-to-most-recently-added). No response callback is |
|
71 |
called if an exception happens in application code, or if the |
|
72 |
response object returned by :term:`view` code is invalid. |
|
73 |
|
25c64c
|
74 |
All response callbacks are called *after* the tweens and |
1e5c58
|
75 |
*before* the :class:`pyramid.events.NewResponse` event is sent. |
844e98
|
76 |
|
CM |
77 |
Errors raised by callbacks are not handled specially. They |
fd5ae9
|
78 |
will be propagated to the caller of the :app:`Pyramid` |
81d3b5
|
79 |
router application. |
CM |
80 |
|
2033ee
|
81 |
.. seealso:: |
SP |
82 |
|
|
83 |
See also :ref:`using_response_callbacks`. |
81d3b5
|
84 |
""" |
844e98
|
85 |
|
37f3ed
|
86 |
self.response_callbacks.append(callback) |
844e98
|
87 |
|
CM |
88 |
def _process_response_callbacks(self, response): |
b847e0
|
89 |
callbacks = self.response_callbacks |
CM |
90 |
while callbacks: |
c73eb9
|
91 |
callback = callbacks.popleft() |
844e98
|
92 |
callback(self, response) |
d66bfb
|
93 |
|
ad6a67
|
94 |
def add_finished_callback(self, callback): |
CM |
95 |
""" |
|
96 |
Add a callback to the set of callbacks to be called |
|
97 |
unconditionally by the :term:`router` at the very end of |
|
98 |
request processing. |
|
99 |
|
|
100 |
``callback`` is a callable which accepts a single positional |
|
101 |
parameter: ``request``. For example: |
|
102 |
|
|
103 |
.. code-block:: python |
|
104 |
:linenos: |
|
105 |
|
|
106 |
import transaction |
|
107 |
|
|
108 |
def commit_callback(request): |
|
109 |
'''commit or abort the transaction associated with request''' |
81d3b5
|
110 |
if request.exception is not None: |
ad6a67
|
111 |
transaction.abort() |
CM |
112 |
else: |
|
113 |
transaction.commit() |
|
114 |
request.add_finished_callback(commit_callback) |
|
115 |
|
|
116 |
Finished callbacks are called in the order they're added ( |
|
117 |
first- to most-recently- added). Finished callbacks (unlike |
|
118 |
response callbacks) are *always* called, even if an exception |
|
119 |
happens in application code that prevents a response from |
|
120 |
being generated. |
|
121 |
|
|
122 |
The set of finished callbacks associated with a request are |
|
123 |
called *very late* in the processing of that request; they are |
|
124 |
essentially the last thing called by the :term:`router`. They |
|
125 |
are called after response processing has already occurred in a |
|
126 |
top-level ``finally:`` block within the router request |
|
127 |
processing code. As a result, mutations performed to the |
|
128 |
``request`` provided to a finished callback will have no |
|
129 |
meaningful effect, because response processing will have |
|
130 |
already occurred, and the request's scope will expire almost |
|
131 |
immediately after all finished callbacks have been processed. |
|
132 |
|
|
133 |
Errors raised by finished callbacks are not handled specially. |
fd5ae9
|
134 |
They will be propagated to the caller of the :app:`Pyramid` |
81d3b5
|
135 |
router application. |
CM |
136 |
|
2033ee
|
137 |
.. seealso:: |
SP |
138 |
|
|
139 |
See also :ref:`using_finished_callbacks`. |
81d3b5
|
140 |
""" |
37f3ed
|
141 |
self.finished_callbacks.append(callback) |
ad6a67
|
142 |
|
CM |
143 |
def _process_finished_callbacks(self): |
b847e0
|
144 |
callbacks = self.finished_callbacks |
CM |
145 |
while callbacks: |
c73eb9
|
146 |
callback = callbacks.popleft() |
ad6a67
|
147 |
callback(self) |
CM |
148 |
|
3b7334
|
149 |
@implementer(IRequest) |
0184b5
|
150 |
class Request( |
CM |
151 |
BaseRequest, |
|
152 |
URLMethodsMixin, |
|
153 |
CallbackMethodsMixin, |
|
154 |
InstancePropertyMixin, |
|
155 |
LocalizerRequestMixin, |
|
156 |
AuthenticationAPIMixin, |
|
157 |
AuthorizationAPIMixin, |
e40ef2
|
158 |
ViewMethodsMixin, |
0184b5
|
159 |
): |
274435
|
160 |
""" |
CM |
161 |
A subclass of the :term:`WebOb` Request class. An instance of |
|
162 |
this class is created by the :term:`router` and is provided to a |
|
163 |
view callable (and to other subsystems) as the ``request`` |
|
164 |
argument. |
|
165 |
|
|
166 |
The documentation below (save for the ``add_response_callback`` and |
|
167 |
``add_finished_callback`` methods, which are defined in this subclass |
|
168 |
itself, and the attributes ``context``, ``registry``, ``root``, |
|
169 |
``subpath``, ``traversed``, ``view_name``, ``virtual_root`` , and |
|
170 |
``virtual_root_path``, each of which is added to the request by the |
|
171 |
:term:`router` at request ingress time) are autogenerated from the WebOb |
|
172 |
source code used when this documentation was generated. |
|
173 |
|
|
174 |
Due to technical constraints, we can't yet display the WebOb |
|
175 |
version number from which this documentation is autogenerated, but |
|
176 |
it will be the 'prevailing WebOb version' at the time of the |
|
177 |
release of this :app:`Pyramid` version. See |
7b1612
|
178 |
https://webob.org/ for further information. |
274435
|
179 |
""" |
CM |
180 |
exception = None |
|
181 |
exc_info = None |
|
182 |
matchdict = None |
|
183 |
matched_route = None |
849196
|
184 |
request_iface = IRequest |
274435
|
185 |
|
c60c0e
|
186 |
ResponseClass = Response |
CM |
187 |
|
274435
|
188 |
@reify |
CM |
189 |
def tmpl_context(self): |
f077a0
|
190 |
# docs-deprecated template context for Pylons-like apps; do not |
CM |
191 |
# remove. |
274435
|
192 |
return TemplateContext() |
CM |
193 |
|
968209
|
194 |
@reify |
CM |
195 |
def session(self): |
|
196 |
""" Obtain the :term:`session` object associated with this |
|
197 |
request. If a :term:`session factory` has not been registered |
|
198 |
during application configuration, a |
|
199 |
:class:`pyramid.exceptions.ConfigurationError` will be raised""" |
|
200 |
factory = self.registry.queryUtility(ISessionFactory) |
|
201 |
if factory is None: |
24b5c1
|
202 |
raise AttributeError( |
968209
|
203 |
'No session factory registered ' |
e277fc
|
204 |
'(see the Sessions chapter of the Pyramid documentation)') |
968209
|
205 |
return factory(self) |
CM |
206 |
|
a7b1a9
|
207 |
@reify |
CM |
208 |
def response(self): |
|
209 |
"""This attribute is actually a "reified" property which returns an |
|
210 |
instance of the :class:`pyramid.response.Response`. class. The |
|
211 |
response object returned does not exist until this attribute is |
cd2e8e
|
212 |
accessed. Subsequent accesses will return the same Response object. |
a7b1a9
|
213 |
|
CM |
214 |
The ``request.response`` API is used by renderers. A render obtains |
|
215 |
the response object it will return from a view that uses that renderer |
|
216 |
by accessing ``request.response``. Therefore, it's possible to use the |
|
217 |
``request.response`` API to set up a response object with "the |
|
218 |
right" attributes (e.g. by calling ``request.response.set_cookie()``) |
|
219 |
within a view that uses a renderer. Mutations to this response object |
|
220 |
will be preserved in the response sent to the client.""" |
32cb80
|
221 |
response_factory = _get_response_factory(self.registry) |
JA |
222 |
return response_factory(self) |
164677
|
223 |
|
920990
|
224 |
def is_response(self, ob): |
CM |
225 |
""" Return ``True`` if the object passed as ``ob`` is a valid |
|
226 |
response object, ``False`` otherwise.""" |
71cd93
|
227 |
if ob.__class__ is Response: |
CM |
228 |
return True |
920990
|
229 |
registry = self.registry |
CM |
230 |
adapted = registry.queryAdapterOrSelf(ob, IResponse) |
|
231 |
if adapted is None: |
|
232 |
return False |
|
233 |
return adapted is ob |
d8c55c
|
234 |
|
b78eff
|
235 |
@property |
6a0602
|
236 |
def json_body(self): |
55fb96
|
237 |
return json.loads(text_(self.body, self.charset)) |
b78eff
|
238 |
|
25c64c
|
239 |
|
333bd0
|
240 |
def route_request_iface(name, bases=()): |
873d9b
|
241 |
# zope.interface treats the __name__ as the __doc__ and changes __name__ |
CM |
242 |
# to None for interfaces that contain spaces if you do not pass a |
|
243 |
# nonempty __doc__ (insane); see |
|
244 |
# zope.interface.interface.Element.__init__ and |
|
245 |
# https://github.com/Pylons/pyramid/issues/232; as a result, always pass |
|
246 |
# __doc__ to the InterfaceClass constructor. |
|
247 |
iface = InterfaceClass('%s_IRequest' % name, bases=bases, |
|
248 |
__doc__="route_request_iface-generated interface") |
abb68d
|
249 |
# for exception view lookups |
873d9b
|
250 |
iface.combined = InterfaceClass( |
CM |
251 |
'%s_combined_IRequest' % name, |
|
252 |
bases=(iface, IRequest), |
25c64c
|
253 |
__doc__='route_request_iface-generated combined interface') |
ff1213
|
254 |
return iface |
dfc2b6
|
255 |
|
25c64c
|
256 |
|
839ea0
|
257 |
def add_global_response_headers(request, headerlist): |
844e98
|
258 |
def add_headers(request, response): |
CM |
259 |
for k, v in headerlist: |
a58c4a
|
260 |
response.headerlist.append((k, v)) |
844e98
|
261 |
request.add_response_callback(add_headers) |
98ea38
|
262 |
|
8cf6dd
|
263 |
def call_app_with_subpath_as_path_info(request, app): |
b64268
|
264 |
# Copy the request. Use the source request's subpath (if it exists) as |
CM |
265 |
# the new request's PATH_INFO. Set the request copy's SCRIPT_NAME to the |
|
266 |
# prefix before the subpath. Call the application with the new request |
|
267 |
# and return a response. |
bcb331
|
268 |
# |
CM |
269 |
# Postconditions: |
|
270 |
# - SCRIPT_NAME and PATH_INFO are empty or start with / |
|
271 |
# - At least one of SCRIPT_NAME or PATH_INFO are set. |
|
272 |
# - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should |
|
273 |
# be '/'). |
|
274 |
|
896fb1
|
275 |
environ = request.environ |
CM |
276 |
script_name = environ.get('SCRIPT_NAME', '') |
|
277 |
path_info = environ.get('PATH_INFO', '/') |
|
278 |
subpath = list(getattr(request, 'subpath', ())) |
bcb331
|
279 |
|
b64268
|
280 |
new_script_name = '' |
bcb331
|
281 |
|
b64268
|
282 |
# compute new_path_info |
c779f1
|
283 |
new_path_info = '/' + '/'.join([native_(x.encode('utf-8'), 'latin-1') |
CM |
284 |
for x in subpath]) |
bcb331
|
285 |
|
896fb1
|
286 |
if new_path_info != '/': # don't want a sole double-slash |
CM |
287 |
if path_info != '/': # if orig path_info is '/', we're already done |
b64268
|
288 |
if path_info.endswith('/'): |
012b97
|
289 |
# readd trailing slash stripped by subpath (traversal) |
b64268
|
290 |
# conversion |
CM |
291 |
new_path_info += '/' |
bcb331
|
292 |
|
b64268
|
293 |
# compute new_script_name |
CM |
294 |
workback = (script_name + path_info).split('/') |
bcb331
|
295 |
|
8cf6dd
|
296 |
tmp = [] |
b64268
|
297 |
while workback: |
CM |
298 |
if tmp == subpath: |
|
299 |
break |
|
300 |
el = workback.pop() |
|
301 |
if el: |
c779f1
|
302 |
tmp.insert(0, text_(bytes_(el, 'latin-1'), 'utf-8')) |
65a5a9
|
303 |
|
CM |
304 |
# strip all trailing slashes from workback to avoid appending undue slashes |
|
305 |
# to end of script_name |
|
306 |
while workback and (workback[-1] == ''): |
|
307 |
workback = workback[:-1] |
012b97
|
308 |
|
b64268
|
309 |
new_script_name = '/'.join(workback) |
CM |
310 |
|
896fb1
|
311 |
new_request = request.copy() |
CM |
312 |
new_request.environ['SCRIPT_NAME'] = new_script_name |
|
313 |
new_request.environ['PATH_INFO'] = new_path_info |
2b4134
|
314 |
|
896fb1
|
315 |
return new_request.get_response(app) |
04cc91
|
316 |
|
MM |
317 |
def apply_request_extensions(request, extensions=None): |
|
318 |
"""Apply request extensions (methods and properties) to an instance of |
|
319 |
:class:`pyramid.interfaces.IRequest`. This method is dependent on the |
|
320 |
``request`` containing a properly initialized registry. |
|
321 |
|
|
322 |
After invoking this method, the ``request`` should have the methods |
|
323 |
and properties that were defined using |
|
324 |
:meth:`pyramid.config.Configurator.add_request_method`. |
|
325 |
""" |
|
326 |
if extensions is None: |
|
327 |
extensions = request.registry.queryUtility(IRequestExtensions) |
|
328 |
if extensions is not None: |
|
329 |
for name, fn in iteritems_(extensions.methods): |
|
330 |
method = fn.__get__(request, request.__class__) |
|
331 |
setattr(request, name, method) |
|
332 |
|
|
333 |
InstancePropertyHelper.apply_properties( |
|
334 |
request, extensions.descriptors) |