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