Steve Piercy
2017-06-27 582a27558f96552cc84641c603150a21a8000821
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)