Chris McDonough
2018-07-05 881bfd57bb49613172eab043e93cb9dc28d4fb77
commit | author | age
fb2824 1 import itertools
252fa5 2 import sys
CM 3
e6fa66 4 import venusian
CM 5
4174e4 6 from zope.interface import providedBy
7e2c6c 7
0c1c39 8 from pyramid.interfaces import (
CM 9     IRoutesMapper,
85d801 10     IMultiView,
MM 11     ISecuredView,
0c1c39 12     IView,
CM 13     IViewClassifier,
17c7f4 14     IRequest,
531428 15     IExceptionViewClassifier,
0c1c39 16     )
d66bfb 17
849196 18 from pyramid.compat import decode_path_info
6f2f04 19 from pyramid.compat import reraise as reraise_
0db4a1 20
e8c66a 21 from pyramid.exceptions import (
MM 22     ConfigurationError,
23     PredicateMismatch,
24 )
e6c2d2 25
0c1c39 26 from pyramid.httpexceptions import (
CM 27     HTTPFound,
e045cf 28     HTTPNotFound,
0c1c39 29     default_exceptionresponse_view,
CM 30     )
0db4a1 31
dda9fa 32 from pyramid.threadlocal import (
BJR 33     get_current_registry,
34     manager,
35     )
36
19016b 37 from pyramid.util import hide_attrs
70d504 38
f66290 39 _marker = object()
56d0fe 40
40d68c 41 class _UseSubrequest(object):
CM 42     """ Object passed to :meth:`pyramid.config.Configurator.add_notfound_view`
43     as the value to ``append_slash`` if you wish to cause a subrequest
44     rather than a redirect """
45
46 UseSubrequest = _UseSubrequest() # singleton
47
7e2c6c 48 def render_view_to_response(context, request, name='', secure=True):
b93d19 49     """ Call the :term:`view callable` configured with a :term:`view
c5f24b 50     configuration` that matches the :term:`view name` ``name``
b93d19 51     registered against the specified ``context`` and ``request`` and
CM 52     return a :term:`response` object.  This function will return
53     ``None`` if a corresponding :term:`view callable` cannot be found
54     (when no :term:`view configuration` matches the combination of
55     ``name`` / ``context`` / and ``request``).
8b1f6e 56
b93d19 57     If `secure`` is ``True``, and the :term:`view callable` found is
99edc5 58     protected by a permission, the permission will be checked before calling
CM 59     the view function.  If the permission check disallows view execution
60     (based on the current :term:`authorization policy`), a
61     :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised.
62     The exception's ``args`` attribute explains why the view access was
63     disallowed.
b93d19 64
7e2c6c 65     If ``secure`` is ``False``, no permission checking is done."""
17ce57 66
849196 67     registry = getattr(request, 'registry', None)
CM 68     if registry is None:
69         registry = get_current_registry()
d66bfb 70
849196 71     context_iface = providedBy(context)
713bc5 72     # We explicitly pass in the interfaces provided by the request as
CM 73     # request_iface to _call_view; we don't want _call_view to use
74     # request.request_iface, because render_view_to_response and friends are
75     # pretty much limited to finding views that are not views associated with
76     # routes, and the only thing request.request_iface is used for is to find
77     # route-based views.  The render_view_to_response API is (and always has
78     # been) a stepchild API reserved for use of those who actually use
79     # traversal.  Doing this fixes an infinite recursion bug introduced in
80     # Pyramid 1.6a1, and causes the render_view* APIs to behave as they did in
81     # 1.5 and previous. We should probably provide some sort of different API
82     # that would allow people to find views for routes.  See
83     # https://github.com/Pylons/pyramid/issues/1643 for more info.
84     request_iface = providedBy(request)
849196 85
CM 86     response = _call_view(
87         registry,
88         request,
89         context,
90         context_iface,
91         name,
03c11e 92         secure=secure,
713bc5 93         request_iface=request_iface,
849196 94         )
CM 95
96     return response # NB: might be None
97
7e2c6c 98
CM 99 def render_view_to_iterable(context, request, name='', secure=True):
b93d19 100     """ Call the :term:`view callable` configured with a :term:`view
c5f24b 101     configuration` that matches the :term:`view name` ``name``
b93d19 102     registered against the specified ``context`` and ``request`` and
CM 103     return an iterable object which represents the body of a response.
104     This function will return ``None`` if a corresponding :term:`view
105     callable` cannot be found (when no :term:`view configuration`
106     matches the combination of ``name`` / ``context`` / and
107     ``request``).  Additionally, this function will raise a
108     :exc:`ValueError` if a view function is found and called but the
109     view function's result does not have an ``app_iter`` attribute.
8b1f6e 110
3d2dd3 111     You can usually get the bytestring representation of the return value of
CM 112     this function by calling ``b''.join(iterable)``, or just use
c81aad 113     :func:`pyramid.view.render_view` instead.
8b1f6e 114
99edc5 115     If ``secure`` is ``True``, and the view is protected by a permission, the
CM 116     permission will be checked before the view function is invoked.  If the
117     permission check disallows view execution (based on the current
118     :term:`authentication policy`), a
119     :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its
120     ``args`` attribute explains why the view access was disallowed.
b93d19 121
CM 122     If ``secure`` is ``False``, no permission checking is
123     done."""
7e2c6c 124     response = render_view_to_response(context, request, name, secure)
CM 125     if response is None:
126         return None
127     return response.app_iter
128
885bfb 129 def render_view(context, request, name='', secure=True):
b93d19 130     """ Call the :term:`view callable` configured with a :term:`view
c5f24b 131     configuration` that matches the :term:`view name` ``name``
27d735 132     registered against the specified ``context`` and ``request``
c5f24b 133     and unwind the view response's ``app_iter`` (see
23de5b 134     :ref:`the_response`) into a single bytestring.  This function will
b93d19 135     return ``None`` if a corresponding :term:`view callable` cannot be
CM 136     found (when no :term:`view configuration` matches the combination
137     of ``name`` / ``context`` / and ``request``).  Additionally, this
138     function will raise a :exc:`ValueError` if a view function is
139     found and called but the view function's result does not have an
140     ``app_iter`` attribute. This function will return ``None`` if a
141     corresponding view cannot be found.
8b1f6e 142
99edc5 143     If ``secure`` is ``True``, and the view is protected by a permission, the
CM 144     permission will be checked before the view is invoked.  If the permission
145     check disallows view execution (based on the current :term:`authorization
146     policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be
147     raised; its ``args`` attribute explains why the view access was
b93d19 148     disallowed.
CM 149
885bfb 150     If ``secure`` is ``False``, no permission checking is done."""
CM 151     iterable = render_view_to_iterable(context, request, name, secure)
152     if iterable is None:
153         return None
0a8ea9 154     return b''.join(iterable)
885bfb 155
197f0c 156 class view_config(object):
8b1f6e 157     """ A function, class or method :term:`decorator` which allows a
CM 158     developer to create view registrations nearer to a :term:`view
c1eb0c 159     callable` definition than use :term:`imperative
8b1f6e 160     configuration` to do the same.
5a7f9a 161
8b1f6e 162     For example, this code in a module ``views.py``::
5a7f9a 163
3e2f12 164       from resources import MyResource
5a7f9a 165
3e2f12 166       @view_config(name='my_view', context=MyResource, permission='read',
197f0c 167                    route_name='site1')
5a7f9a 168       def my_view(context, request):
8b1f6e 169           return 'OK'
5a7f9a 170
b93d19 171     Might replace the following call to the
aff443 172     :meth:`pyramid.config.Configurator.add_view` method::
8b1f6e 173
CM 174        import views
3e2f12 175        from resources import MyResource
CM 176        config.add_view(views.my_view, context=MyResource, name='my_view',
34606c 177                        permission='read', route_name='site1')
5a7f9a 178
197f0c 179     .. note: :class:`pyramid.view.view_config` is also importable, for
CM 180              backwards compatibility purposes, as the name
181              :class:`pyramid.view.bfg_view`.
182
8eb19b 183     :class:`pyramid.view.view_config` supports the following keyword
e8c66a 184     arguments: ``context``, ``exception``, ``permission``, ``name``,
cf7d8b 185     ``request_type``, ``route_name``, ``request_method``, ``request_param``,
CM 186     ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
602ac1 187     ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
769da1 188     ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and
MM 189     ``view_options``.
5a7f9a 190
cf7d8b 191     The meanings of these arguments are the same as the arguments passed to
4c29ef 192     :meth:`pyramid.config.Configurator.add_view`.  If any argument is left
CM 193     out, its default will be the equivalent ``add_view`` default.
0b0e74 194
ea4928 195     Two additional keyword arguments which will be passed to the
0afdad 196     :term:`venusian` ``attach`` function are ``_depth`` and ``_category``.
ea4928 197
T 198     ``_depth`` is provided for people who wish to reuse this class from another
199     decorator. The default value is ``0`` and should be specified relative to
200     the ``view_config`` invocation. It will be passed in to the
201     :term:`venusian` ``attach`` function as the depth of the callstack when
202     Venusian checks if the decorator is being used in a class or module
203     context. It's not often used, but it can be useful in this circumstance.
204
0afdad 205     ``_category`` sets the decorator category name. It can be useful in
eb596c 206     combination with the ``category`` argument of ``scan`` to control which
ea4928 207     views should be processed.
T 208
498158 209     See the :py:func:`venusian.attach` function in Venusian for more
MM 210     information about the ``_depth`` and ``_category`` arguments.
2033ee 211     
SP 212     .. seealso::
213     
214         See also :ref:`mapping_views_using_a_decorator_section` for
215         details about using :class:`pyramid.view.view_config`.
a8d71c 216
2033ee 217     .. warning::
SP 218     
219         ``view_config`` will work ONLY on module top level members
220         because of the limitation of ``venusian.Scanner.scan``.
a8f669 221
5a7f9a 222     """
e6fa66 223     venusian = venusian # for testing injection
8ec8e2 224     def __init__(self, **settings):
CM 225         if 'for_' in settings:
226             if settings.get('context') is None:
227                 settings['context'] = settings['for_']
228         self.__dict__.update(settings)
5a7f9a 229
CM 230     def __call__(self, wrapped):
e6fa66 231         settings = self.__dict__.copy()
ed1419 232         depth = settings.pop('_depth', 0)
0afdad 233         category = settings.pop('_category', 'pyramid')
e6fa66 234
CM 235         def callback(context, name, ob):
b2c4e0 236             config = context.config.with_package(info.module)
CM 237             config.add_view(view=ob, **settings)
e6fa66 238
4be1e6 239         info = self.venusian.attach(wrapped, callback, category=category,
ed1419 240                                     depth=depth + 1)
e6fa66 241
CM 242         if info.scope == 'class':
32418e 243             # if the decorator was attached to a method in a class, or
CM 244             # otherwise executed at class scope, we need to set an
245             # 'attr' into the settings if one isn't already in there
4c29ef 246             if settings.get('attr') is None:
e6fa66 247                 settings['attr'] = wrapped.__name__
89968d 248
b2c4e0 249         settings['_info'] = info.codeinfo # fbo "action_method"
c89bcb 250         return wrapped
5a7f9a 251
33516a 252 bfg_view = view_config # bw compat (forever)
5f4780 253
914abe 254 class view_defaults(view_config):
4375cf 255     """ A class :term:`decorator` which, when applied to a class, will
CM 256     provide defaults for all view configurations that use the class.  This
257     decorator accepts all the arguments accepted by
aaedf5 258     :meth:`pyramid.view.view_config`, and each has the same meaning.
4375cf 259
CM 260     See :ref:`view_defaults` for more information.
261     """
a8f669 262
914abe 263     def __call__(self, wrapped):
CM 264         wrapped.__view_defaults__ = self.__dict__.copy()
265         return wrapped
266
d96ff9 267 class AppendSlashNotFoundViewFactory(object):
CM 268     """ There can only be one :term:`Not Found view` in any
fd5ae9 269     :app:`Pyramid` application.  Even if you use
c81aad 270     :func:`pyramid.view.append_slash_notfound_view` as the Not
fd5ae9 271     Found view, :app:`Pyramid` still must generate a ``404 Not
d96ff9 272     Found`` response when it cannot redirect to a slash-appended URL;
CM 273     this not found response will be visible to site users.
274
275     If you don't care what this 404 response looks like, and you only
276     need redirections to slash-appended route URLs, you may use the
c81aad 277     :func:`pyramid.view.append_slash_notfound_view` object as the
d96ff9 278     Not Found view.  However, if you wish to use a *custom* notfound
CM 279     view callable when a URL cannot be redirected to a slash-appended
280     URL, you may wish to use an instance of this class as the Not
281     Found view, supplying a :term:`view callable` to be used as the
282     custom notfound view as the first argument to its constructor.
283     For instance:
284
285     .. code-block:: python
286
99edc5 287        from pyramid.httpexceptions import HTTPNotFound
c81aad 288        from pyramid.view import AppendSlashNotFoundViewFactory
d96ff9 289
1b4360 290        def notfound_view(context, request): return HTTPNotFound('nope')
d96ff9 291
CM 292        custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
a7e625 293        config.add_view(custom_append_slash, context=HTTPNotFound)
d96ff9 294
CM 295     The ``notfound_view`` supplied must adhere to the two-argument
296     view callable calling convention of ``(context, request)``
297     (``context`` will be the exception object).
298
299     """
12b6f5 300     def __init__(self, notfound_view=None, redirect_class=HTTPFound):
d96ff9 301         if notfound_view is None:
CM 302             notfound_view = default_exceptionresponse_view
303         self.notfound_view = notfound_view
12b6f5 304         self.redirect_class = redirect_class
d96ff9 305
CM 306     def __call__(self, context, request):
0db4a1 307         path = decode_path_info(request.environ['PATH_INFO'] or '/')
30e64f 308         registry = request.registry
d96ff9 309         mapper = registry.queryUtility(IRoutesMapper)
CM 310         if mapper is not None and not path.endswith('/'):
311             slashpath = path + '/'
312             for route in mapper.get_routes():
313                 if route.match(slashpath) is not None:
40d68c 314                     if self.redirect_class is UseSubrequest:
CM 315                         subreq = request.copy()
316                         subreq.path_info = request.path + '/'
317                         return request.invoke_subrequest(
318                             subreq,
319                             use_tweens=True
320                         )
321                     else:
322                         qs = request.query_string
323                         if qs:
324                             qs = '?' + qs
325                         return self.redirect_class(
326                             location=request.path + '/' + qs
327                         )
328
d96ff9 329         return self.notfound_view(context, request)
a9454c 330
d96ff9 331 append_slash_notfound_view = AppendSlashNotFoundViewFactory()
CM 332 append_slash_notfound_view.__doc__ = """\
333 For behavior like Django's ``APPEND_SLASH=True``, use this view as the
334 :term:`Not Found view` in your application.
a9454c 335
87a85f 336 When this view is the Not Found view (indicating that no view was found), and
CM 337 any routes have been defined in the configuration of your application, if the
338 value of the ``PATH_INFO`` WSGI environment variable does not already end in
339 a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's
340 path, do an HTTP redirect to the slash-appended PATH_INFO.  Note that this
341 will *lose* ``POST`` data information (turning it into a GET), so you
342 shouldn't rely on this to redirect POST requests.  Note also that static
343 routes are not considered when attempting to find a matching route.
d66bfb 344
c1eb0c 345 Use the :meth:`pyramid.config.Configurator.add_view` method to configure this
CM 346 view as the Not Found view::
8b1f6e 347
99edc5 348   from pyramid.httpexceptions import HTTPNotFound
c81aad 349   from pyramid.view import append_slash_notfound_view
a7e625 350   config.add_view(append_slash_notfound_view, context=HTTPNotFound)
8b1f6e 351
2033ee 352 .. deprecated:: 1.3
d66bfb 353
d96ff9 354 """
d66bfb 355
0db4a1 356 class notfound_view_config(object):
CM 357     """
05e928 358     .. versionadded:: 1.3
0db4a1 359
CM 360     An analogue of :class:`pyramid.view.view_config` which registers a
e8c66a 361     :term:`Not Found View` using
MM 362     :meth:`pyramid.config.Configurator.add_notfound_view`.
0db4a1 363
e450ca 364     The ``notfound_view_config`` constructor accepts most of the same arguments
0db4a1 365     as the constructor of :class:`pyramid.view.view_config`.  It can be used
CM 366     in the same places, and behaves in largely the same way, except it always
8ec8e2 367     registers a not found exception view instead of a 'normal' view.
0db4a1 368
CM 369     Example:
370
371     .. code-block:: python
372
373         from pyramid.view import notfound_view_config
374         from pyramid.response import Response
a8f669 375
15e3b1 376         @notfound_view_config()
0db4a1 377         def notfound(request):
c898dd 378             return Response('Not found!', status='404 Not Found')
0db4a1 379
CM 380     All arguments except ``append_slash`` have the same meaning as
381     :meth:`pyramid.view.view_config` and each predicate
382     argument restricts the set of circumstances under which this notfound
383     view will be invoked.
384
cec2b0 385     If ``append_slash`` is ``True``, when the Not Found View is invoked, and
0db4a1 386     the current path info does not end in a slash, the notfound logic will
CM 387     attempt to find a :term:`route` that matches the request's path info
388     suffixed with a slash.  If such a route exists, Pyramid will issue a
389     redirect to the URL implied by the route; if it does not, Pyramid will
390     return the result of the view callable provided as ``view``, as normal.
391
24358c 392     If the argument provided as ``append_slash`` is not a boolean but
CM 393     instead implements :class:`~pyramid.interfaces.IResponse`, the
394     append_slash logic will behave as if ``append_slash=True`` was passed,
395     but the provided class will be used as the response class instead of
396     the default :class:`~pyramid.httpexceptions.HTTPFound` response class
397     when a redirect is performed.  For example:
398
399       .. code-block:: python
400
401         from pyramid.httpexceptions import (
402             HTTPMovedPermanently,
403             HTTPNotFound
404             )
405
406         @notfound_view_config(append_slash=HTTPMovedPermanently)
407         def aview(request):
408             return HTTPNotFound('not found')
409
410     The above means that a redirect to a slash-appended route will be
411     attempted, but instead of :class:`~pyramid.httpexceptions.HTTPFound`
412     being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
413     be used` for the redirect response if a slash-appended route is found.
414
40d68c 415     If the argument supplied as ``append_slash`` is the special object
CM 416     :attr:`~pyramid.views.UseSubrequest`, a :term:`subrequest` will be issued
417     instead of a redirect.  This makes it possible to successfully invoke a
418     slash-appended URL without losing the HTTP verb, POST data, or any other
419     information contained in the original request.  Instead of returning a
420     redirect response when a slash-appended route is detected during the
881bfd 421     notfound processing, Pyramid will call the view associated with the
40d68c 422     slash-appended route "under the hood" and will return whatever response is
CM 423     returned by that view.  This has the potential downside that both URLs (the
424     slash-appended and the non-slash-appended URLs) in an application will be
425     "canonical" to clients; they will behave exactly the same, and the client
426     will never be notified that the slash-appended URL is "better than" the
427     non-slash-appended URL by virtue of a redirect.  It, however, has the
428     upside that a POST request with a body can be handled successfully
429     with an append-slash during notfound processing.
430
0db4a1 431     See :ref:`changing_the_notfound_view` for detailed usage information.
498158 432
MM 433     .. versionchanged:: 1.9.1
434        Added the ``_depth`` and ``_category`` arguments.
0db4a1 435
40d68c 436     .. versionchanged:: 1.10
CM 437        Added the functionality to use a subrequest rather than a redirect by
438        using :class:`~pyramid.views.UseSubrequest` as an argument to
439        ``append_slash``.
440
0db4a1 441     """
CM 442
443     venusian = venusian
444
8ec8e2 445     def __init__(self, **settings):
CM 446         self.__dict__.update(settings)
0db4a1 447
CM 448     def __call__(self, wrapped):
449         settings = self.__dict__.copy()
498158 450         depth = settings.pop('_depth', 0)
MM 451         category = settings.pop('_category', 'pyramid')
0db4a1 452
CM 453         def callback(context, name, ob):
454             config = context.config.with_package(info.module)
455             config.add_notfound_view(view=ob, **settings)
456
498158 457         info = self.venusian.attach(wrapped, callback, category=category,
MM 458                                     depth=depth + 1)
0db4a1 459
CM 460         if info.scope == 'class':
461             # if the decorator was attached to a method in a class, or
462             # otherwise executed at class scope, we need to set an
463             # 'attr' into the settings if one isn't already in there
464             if settings.get('attr') is None:
465                 settings['attr'] = wrapped.__name__
466
467         settings['_info'] = info.codeinfo # fbo "action_method"
468         return wrapped
469
a7fe30 470 class forbidden_view_config(object):
CM 471     """
05e928 472     .. versionadded:: 1.3
a7fe30 473
CM 474     An analogue of :class:`pyramid.view.view_config` which registers a
e8c66a 475     :term:`forbidden view` using
MM 476     :meth:`pyramid.config.Configurator.add_forbidden_view`.
a7fe30 477
CM 478     The forbidden_view_config constructor accepts most of the same arguments
479     as the constructor of :class:`pyramid.view.view_config`.  It can be used
480     in the same places, and behaves in largely the same way, except it always
8ec8e2 481     registers a forbidden exception view instead of a 'normal' view.
a7fe30 482
CM 483     Example:
484
485     .. code-block:: python
486
487         from pyramid.view import forbidden_view_config
488         from pyramid.response import Response
a8f669 489
15e3b1 490         @forbidden_view_config()
bbd3ab 491         def forbidden(request):
59e7cc 492             return Response('You are not allowed', status='403 Forbidden')
a7fe30 493
8ec8e2 494     All arguments passed to this function have the same meaning as
CM 495     :meth:`pyramid.view.view_config` and each predicate argument restricts
496     the set of circumstances under which this notfound view will be invoked.
a7fe30 497
CM 498     See :ref:`changing_the_forbidden_view` for detailed usage information.
499
498158 500     .. versionchanged:: 1.9.1
MM 501        Added the ``_depth`` and ``_category`` arguments.
502
a7fe30 503     """
CM 504
505     venusian = venusian
506
8ec8e2 507     def __init__(self, **settings):
CM 508         self.__dict__.update(settings)
a7fe30 509
CM 510     def __call__(self, wrapped):
511         settings = self.__dict__.copy()
498158 512         depth = settings.pop('_depth', 0)
MM 513         category = settings.pop('_category', 'pyramid')
a7fe30 514
CM 515         def callback(context, name, ob):
516             config = context.config.with_package(info.module)
517             config.add_forbidden_view(view=ob, **settings)
518
498158 519         info = self.venusian.attach(wrapped, callback, category=category,
MM 520                                     depth=depth + 1)
a7fe30 521
CM 522         if info.scope == 'class':
523             # if the decorator was attached to a method in a class, or
524             # otherwise executed at class scope, we need to set an
525             # 'attr' into the settings if one isn't already in there
526             if settings.get('attr') is None:
527                 settings['attr'] = wrapped.__name__
528
529         settings['_info'] = info.codeinfo # fbo "action_method"
530         return wrapped
a8f669 531
93c94b 532 class exception_view_config(object):
AL 533     """
534     .. versionadded:: 1.8
535
536     An analogue of :class:`pyramid.view.view_config` which registers an
e8c66a 537     :term:`exception view` using
MM 538     :meth:`pyramid.config.Configurator.add_exception_view`.
93c94b 539
e8c66a 540     The ``exception_view_config`` constructor requires an exception context,
MM 541     and additionally accepts most of the same arguments as the constructor of
93c94b 542     :class:`pyramid.view.view_config`.  It can be used in the same places,
e8c66a 543     and behaves in largely the same way, except it always registers an
160aab 544     exception view instead of a "normal" view that dispatches on the request
e8c66a 545     :term:`context`.
93c94b 546
AL 547     Example:
548
549     .. code-block:: python
550
551         from pyramid.view import exception_view_config
552         from pyramid.response import Response
553
e8c66a 554         @exception_view_config(ValueError, renderer='json')
MM 555         def error_view(request):
556             return {'error': str(request.exception)}
93c94b 557
AL 558     All arguments passed to this function have the same meaning as
160aab 559     :meth:`pyramid.view.view_config`, and each predicate argument restricts
93c94b 560     the set of circumstances under which this exception view will be invoked.
e8c66a 561
498158 562     .. versionchanged:: 1.9.1
MM 563        Added the ``_depth`` and ``_category`` arguments.
564
93c94b 565     """
74842a 566     venusian = venusian
93c94b 567
e8c66a 568     def __init__(self, *args, **settings):
MM 569         if 'context' not in settings and len(args) > 0:
570             exception, args = args[0], args[1:]
571             settings['context'] = exception
572         if len(args) > 0:
573             raise ConfigurationError('unknown positional arguments')
93c94b 574         self.__dict__.update(settings)
AL 575
576     def __call__(self, wrapped):
577         settings = self.__dict__.copy()
498158 578         depth = settings.pop('_depth', 0)
MM 579         category = settings.pop('_category', 'pyramid')
93c94b 580
AL 581         def callback(context, name, ob):
582             config = context.config.with_package(info.module)
583             config.add_exception_view(view=ob, **settings)
584
498158 585         info = self.venusian.attach(wrapped, callback, category=category,
MM 586                                     depth=depth + 1)
93c94b 587
AL 588         if info.scope == 'class':
589             # if the decorator was attached to a method in a class, or
590             # otherwise executed at class scope, we need to set an
160aab 591             # 'attr' in the settings if one isn't already in there
93c94b 592             if settings.get('attr') is None:
AL 593                 settings['attr'] = wrapped.__name__
594
595         settings['_info'] = info.codeinfo # fbo "action_method"
596         return wrapped
597
17c7f4 598 def _find_views(
CM 599     registry,
600     request_iface,
601     context_iface,
602     view_name,
603     view_types=None,
604     view_classifier=None,
605     ):
03c11e 606     if view_types is None:
17c7f4 607         view_types = (IView, ISecuredView, IMultiView)
03c11e 608     if view_classifier is None:
17c7f4 609         view_classifier = IViewClassifier
fb2824 610     registered = registry.adapters.registered
c15cbc 611     cache = registry._view_lookup_cache
CM 612     views = cache.get((request_iface, context_iface, view_name))
613     if views is None:
614         views = []
615         for req_type, ctx_type in itertools.product(
616             request_iface.__sro__, context_iface.__sro__
617         ):
17c7f4 618             source_ifaces = (view_classifier, req_type, ctx_type)
c15cbc 619             for view_type in view_types:
CM 620                 view_callable = registered(
621                     source_ifaces,
622                     view_type,
623                     name=view_name,
624                 )
625                 if view_callable is not None:
626                     views.append(view_callable)
627         if views:
628             # do not cache view lookup misses.  rationale: dont allow cache to
629             # grow without bound if somebody tries to hit the site with many
630             # missing URLs.  we could use an LRU cache instead, but then
631             # purposeful misses by an attacker would just blow out the cache
632             # anyway. downside: misses will almost always consume more CPU than
633             # hits in steady state.
634             with registry._lock:
635                 cache[(request_iface, context_iface, view_name)] = views
849196 636
99bc0c 637     return views
eb3ac8 638
849196 639 def _call_view(
CM 640     registry,
641     request,
642     context,
643     context_iface,
644     view_name,
17c7f4 645     view_types=None,
CM 646     view_classifier=None,
849196 647     secure=True,
17c7f4 648     request_iface=None,
849196 649     ):
17c7f4 650     if request_iface is None:
CM 651         request_iface = getattr(request, 'request_iface', IRequest)
eb3ac8 652     view_callables = _find_views(
CM 653         registry,
17c7f4 654         request_iface,
eb3ac8 655         context_iface,
CM 656         view_name,
17c7f4 657         view_types=view_types,
CM 658         view_classifier=view_classifier,
eb3ac8 659         )
CM 660
661     pme = None
662     response = None
663
664     for view_callable in view_callables:
665         # look for views that meet the predicate criteria
666         try:
849196 667             if not secure:
CM 668                 # the view will have a __call_permissive__ attribute if it's
669                 # secured; otherwise it won't.
670                 view_callable = getattr(
671                     view_callable,
672                     '__call_permissive__',
673                     view_callable
674                     )
675
676             # if this view is secured, it will raise a Forbidden
677             # appropriately if the executing user does not have the proper
678             # permission
eb3ac8 679             response = view_callable(context, request)
CM 680             return response
681         except PredicateMismatch as _pme:
682             pme = _pme
683
684     if pme is not None:
685         raise pme
686
687     return response
531428 688
CM 689 class ViewMethodsMixin(object):
690     """ Request methods mixin for BaseRequest having to do with executing
691     views """
692     def invoke_exception_view(
693         self,
e40ef2 694         exc_info=None,
531428 695         request=None,
6f2f04 696         secure=True,
MM 697         reraise=False,
698     ):
e40ef2 699         """ Executes an exception view related to the request it's called upon.
CM 700         The arguments it takes are these:
701
702         ``exc_info``
703
704             If provided, should be a 3-tuple in the form provided by
252fa5 705             ``sys.exc_info()``.  If not provided,
CM 706             ``sys.exc_info()`` will be called to obtain the current
e40ef2 707             interpreter exception information.  Default: ``None``.
CM 708
709         ``request``
710
711             If the request to be used is not the same one as the instance that
712             this method is called upon, it may be passed here.  Default:
713             ``None``.
714
715         ``secure``
716
717             If the exception view should not be rendered if the current user
718             does not have the appropriate permission, this should be ``True``.
719             Default: ``True``.
720
6f2f04 721         ``reraise``
e40ef2 722
6f2f04 723             A boolean indicating whether the original error should be reraised
MM 724             if a :term:`response` object could not be created. If ``False``
725             then an :class:`pyramid.httpexceptions.HTTPNotFound`` exception
726             will be raised. Default: ``False``.
e40ef2 727
3b886e 728         If a response is generated then ``request.exception`` and
MM 729         ``request.exc_info`` will be left at the values used to render the
730         response. Otherwise the previous values for ``request.exception`` and
731         ``request.exc_info`` will be restored.
732
daf06d 733         .. versionadded:: 1.7
MM 734
e2e51b 735         .. versionchanged:: 1.9
MM 736            The ``request.exception`` and ``request.exc_info`` properties will
737            reflect the exception used to render the response where previously
738            they were reset to the values prior to invoking the method.
6f2f04 739
MM 740            Also added the ``reraise`` argument.
e2e51b 741
3b886e 742         """
531428 743         if request is None:
CM 744             request = self
745         registry = getattr(request, 'registry', None)
746         if registry is None:
747             registry = get_current_registry()
dda9fa 748
BJR 749         if registry is None:
750             raise RuntimeError("Unable to retrieve registry")
751
e40ef2 752         if exc_info is None:
252fa5 753             exc_info = sys.exc_info()
dda9fa 754
d52257 755         exc = exc_info[1]
ca529f 756         attrs = request.__dict__
d52257 757         context_iface = providedBy(exc)
19016b 758
MM 759         # clear old generated request.response, if any; it may
760         # have been mutated by the view, and its state is not
761         # sane (e.g. caching headers)
3b886e 762         with hide_attrs(request, 'response', 'exc_info', 'exception'):
d52257 763             attrs['exception'] = exc
68b303 764             attrs['exc_info'] = exc_info
19016b 765             # we use .get instead of .__getitem__ below due to
MM 766             # https://github.com/Pylons/pyramid/issues/700
767             request_iface = attrs.get('request_iface', IRequest)
dda9fa 768
4f6635 769             manager.push({'request': request, 'registry': registry})
dda9fa 770
4f6635 771             try:
dda9fa 772                 response = _call_view(
BJR 773                     registry,
774                     request,
775                     exc,
776                     context_iface,
777                     '',
778                     view_types=None,
779                     view_classifier=IExceptionViewClassifier,
780                     secure=secure,
781                     request_iface=request_iface.combined,
782                     )
87b7b7 783             except Exception:
6f2f04 784                 if reraise:
MM 785                     reraise_(*exc_info)
786                 raise
dda9fa 787             finally:
BJR 788                 manager.pop()
3b886e 789
MM 790         if response is None:
6f2f04 791             if reraise:
MM 792                 reraise_(*exc_info)
3b886e 793             raise HTTPNotFound
MM 794
795         # successful response, overwrite exception/exc_info
796         attrs['exception'] = exc
797         attrs['exc_info'] = exc_info
798         return response