summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Merickel <michael@merickel.org>2016-09-19 23:52:05 -0500
committerMichael Merickel <michael@merickel.org>2016-09-28 20:33:27 -0500
commite8c66a339e9f7d83bd2408952de53ef30dba0794 (patch)
tree264643f1a6e45e0d3141c751b4724d51e49c0c5e
parent35209e4ac53520e1159bd8a6b47128f38a75db18 (diff)
downloadpyramid-e8c66a339e9f7d83bd2408952de53ef30dba0794.tar.gz
pyramid-e8c66a339e9f7d83bd2408952de53ef30dba0794.tar.bz2
pyramid-e8c66a339e9f7d83bd2408952de53ef30dba0794.zip
derive exception views separately from normal views
- previously the multiview was shared for both exception and hot-route, but now that we allow some exception-only views this needed to be separated - add ViewDeriverInfo.exception_only to detect exception views - do not prevent http_cache on exception views - optimize secured_view and csrf_view derivers to remove themselves from the view pipeline for exception views
-rw-r--r--docs/narr/hooks.rst23
-rw-r--r--docs/narr/viewconfig.rst19
-rw-r--r--docs/narr/views.rst40
-rw-r--r--pyramid/config/views.py319
-rw-r--r--pyramid/exceptions.py1
-rw-r--r--pyramid/interfaces.py1
-rw-r--r--pyramid/tests/test_config/test_views.py258
-rw-r--r--pyramid/tests/test_exceptions.py2
-rw-r--r--pyramid/tests/test_view.py13
-rw-r--r--pyramid/tests/test_viewderivers.py22
-rw-r--r--pyramid/view.py39
-rw-r--r--pyramid/viewderivers.py57
12 files changed, 522 insertions, 272 deletions
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 49ef29d3f..7fbac2080 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -1639,7 +1639,8 @@ the user-defined :term:`view callable`:
Enforce the ``permission`` defined on the view. This element is a no-op if no
permission is defined. Note there will always be a permission defined if a
default permission was assigned via
- :meth:`pyramid.config.Configurator.set_default_permission`.
+ :meth:`pyramid.config.Configurator.set_default_permission` unless the
+ view is an :term:`exception view`.
This element will also output useful debugging information when
``pyramid.debug_authorization`` is enabled.
@@ -1649,7 +1650,8 @@ the user-defined :term:`view callable`:
Used to check the CSRF token provided in the request. This element is a
no-op if ``require_csrf`` view option is not ``True``. Note there will
always be a ``require_csrf`` option if a default value was assigned via
- :meth:`pyramid.config.Configurator.set_default_csrf_options`.
+ :meth:`pyramid.config.Configurator.set_default_csrf_options` unless
+ the view is an :term:`exception view`.
``owrapped_view``
@@ -1695,6 +1697,8 @@ around monitoring and security. In order to register a custom :term:`view
deriver`, you should create a callable that conforms to the
:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with
your application using :meth:`pyramid.config.Configurator.add_view_deriver`.
+The callable should accept the ``view`` to be wrapped and the ``info`` object
+which is an instance of :class:`pyramid.interfaces.IViewDeriverInfo`.
For example, below is a callable that can provide timing information for the
view pipeline:
@@ -1745,6 +1749,21 @@ View derivers are unique in that they have access to most of the options
passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what
to do, and they have a chance to affect every view in the application.
+.. _exception_view_derivers:
+
+Exception Views and View Derivers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A :term:`view deriver` has the opportunity to wrap any view, including
+an :term:`exception view`. In general this is fine, but certain view derivers
+may wish to avoid doing certain things when handling exceptions. For example,
+the ``csrf_view`` and ``secured_view`` built-in view derivers will not perform
+security checks on exception views unless explicitly told to do so.
+
+You can check for ``info.exception_only`` on the
+:class:`pyramid.interfaces.IViewDeriverInfo` object when wrapping the view
+to determine whether you are wrapping an exception view or a normal view.
+
Ordering View Derivers
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst
index cd5b8feb0..76eaf3cc5 100644
--- a/docs/narr/viewconfig.rst
+++ b/docs/narr/viewconfig.rst
@@ -34,7 +34,7 @@ determine the set of circumstances which must be true for the view callable to
be invoked.
A view configuration statement is made about information present in the
-:term:`context` resource and the :term:`request`.
+:term:`context` resource (or exception) and the :term:`request`.
View configuration is performed in one of two ways:
@@ -306,9 +306,26 @@ configured view.
represented class or if the :term:`context` resource provides the represented
interface; it is otherwise false.
+ It is possible to pass an exception class as the context if your context may
+ subclass an exception. In this case **two** views will be registered. One
+ will match normal incoming requests and the other will match as an
+ :term:`exception view` which only occurs when an exception is raised during
+ the normal request processing pipeline.
+
If ``context`` is not supplied, the value ``None``, which matches any
resource, is used.
+``exception_only``
+
+ When this value is ``True`` the ``context`` argument must be a subclass of
+ ``Exception``. This flag indicates that only an :term:`exception view` should
+ be created and that this view should not match if the traversal
+ :term:`context` matches the ``context`` argument. If the ``context`` is a
+ subclass of ``Exception`` and this value is ``False`` (the default) then a
+ view will be registered to match the traversal :term:`context` as well.
+
+ .. versionadded:: 1.8
+
``route_name``
If ``route_name`` is supplied, the view callable will be invoked only when
the named route has matched.
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index 770d27919..465062651 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -262,10 +262,16 @@ specialized views as described in :ref:`special_exceptions_in_callables` can
also be used by application developers to convert arbitrary exceptions to
responses.
-To register a view that should be called whenever a particular exception is
-raised from within :app:`Pyramid` view code, use the exception class (or one of
-its superclasses) as the :term:`context` of a view configuration which points
-at a view callable for which you'd like to generate a response.
+To register a :term:`exception view` that should be called whenever a
+particular exception is raised from within :app:`Pyramid` view code, use
+:meth:`pyramid.config.Configurator.add_exception_view` to register a view
+configuration which matches the exception (or a subclass of the exception) and
+points at a view callable for which you'd like to generate a response. The
+exception will be passed as the ``context`` argument to any
+:term:`view predicate` registered with the view as well as to the view itself.
+For convenience a new decorator exists,
+:class:`pyramid.views.exception_view_config`, which may be used to easily
+register exception views.
For example, given the following exception class in a module named
``helloworld.exceptions``:
@@ -277,17 +283,16 @@ For example, given the following exception class in a module named
def __init__(self, msg):
self.msg = msg
-
You can wire a view callable to be called whenever any of your *other* code
raises a ``helloworld.exceptions.ValidationFailure`` exception:
.. code-block:: python
:linenos:
- from pyramid.view import view_config
+ from pyramid.view import exception_view_config
from helloworld.exceptions import ValidationFailure
- @view_config(context=ValidationFailure)
+ @exception_view_config(ValidationFailure)
def failed_validation(exc, request):
response = Response('Failed validation: %s' % exc.msg)
response.status_int = 500
@@ -308,7 +313,7 @@ view registration:
from pyramid.view import view_config
from helloworld.exceptions import ValidationFailure
- @view_config(context=ValidationFailure, route_name='home')
+ @exception_view_config(ValidationFailure, route_name='home')
def failed_validation(exc, request):
response = Response('Failed validation: %s' % exc.msg)
response.status_int = 500
@@ -327,14 +332,21 @@ which have a name will be ignored.
.. note::
- Normal (i.e., non-exception) views registered against a context resource type
- which inherits from :exc:`Exception` will work normally. When an exception
- view configuration is processed, *two* views are registered. One as a
- "normal" view, the other as an "exception" view. This means that you can use
- an exception as ``context`` for a normal view.
+ In most cases, you should register an :term:`exception view` by using
+ :meth:`pyramid.config.Configurator.add_exception_view`. However, it is
+ possible to register 'normal' (i.e., non-exception) views against a context
+ resource type which inherits from :exc:`Exception` (i.e.,
+ ``config.add_view(context=Exception)``). When the view configuration is
+ processed, *two* views are registered. One as a "normal" view, the other
+ as an :term:`exception view`. This means that you can use an exception as
+ ``context`` for a normal view.
+
+ The view derivers that wrap these two views may behave differently.
+ See :ref:`exception_view_derivers` for more information about this.
Exception views can be configured with any view registration mechanism:
-``@view_config`` decorator or imperative ``add_view`` styles.
+``@exception_view_config`` decorator or imperative ``add_exception_view``
+styles.
.. note::
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index e341922d3..ae180fb10 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -9,12 +9,11 @@ from zope.interface import (
implementedBy,
implementer,
)
-
from zope.interface.interfaces import IInterface
from pyramid.interfaces import (
- IException,
IExceptionViewClassifier,
+ IException,
IMultiView,
IPackageOverrides,
IRendererFactory,
@@ -503,7 +502,20 @@ class ViewsConfiguratorMixin(object):
if the :term:`context` provides the represented interface;
it is otherwise false. This argument may also be provided
to ``add_view`` as ``for_`` (an older, still-supported
- spelling).
+ spelling). If the view should **only** match when handling
+ exceptions then set the ``exception_only`` to ``True``.
+
+ exception_only
+
+ .. versionadded:: 1.8
+
+ When this value is ``True`` the ``context`` argument must be
+ a subclass of ``Exception``. This flag indicates that only an
+ :term:`exception view` should be created and that this view should
+ not match if the traversal :term:`context` matches the ``context``
+ argument. If the ``context`` is a subclass of ``Exception`` and
+ this value is ``False`` (the default) then a view will be
+ registered to match the traversal :term:`context` as well.
route_name
@@ -685,7 +697,7 @@ class ViewsConfiguratorMixin(object):
obsoletes this argument, but it is kept around for backwards
compatibility.
- view_options:
+ view_options
Pass a key/value pair here to use a third-party predicate or set a
value for a view deriver. See
@@ -702,14 +714,6 @@ class ViewsConfiguratorMixin(object):
Support setting view deriver options. Previously, only custom
view predicate values could be supplied.
- exception_only
-
- .. versionadded:: 1.8
-
- A boolean indicating whether the view is registered only as an
- exception view. When this argument is true, the view context must
- be an exception.
-
"""
if custom_predicates:
warnings.warn(
@@ -768,14 +772,15 @@ class ViewsConfiguratorMixin(object):
raise ConfigurationError(
'request_type must be an interface, not %s' % request_type)
- if exception_only and not isexception(context):
- raise ConfigurationError(
- 'context must be an exception when exception_only is true'
- )
-
if context is None:
context = for_
+ isexc = isexception(context)
+ if exception_only and not isexc:
+ raise ConfigurationError(
+ 'view "context" must be an exception type when '
+ '"exception_only" is True')
+
r_context = context
if r_context is None:
r_context = Interface
@@ -811,6 +816,7 @@ class ViewsConfiguratorMixin(object):
# is. It can't be computed any sooner because thirdparty
# predicates/view derivers may not yet exist when add_view is
# called.
+ predlist = self.get_predlist('view')
valid_predicates = predlist.names()
pvals = {}
dvals = {}
@@ -849,6 +855,7 @@ class ViewsConfiguratorMixin(object):
view_intr.update(dict(
name=name,
context=context,
+ exception_only=exception_only,
containment=containment,
request_param=request_param,
request_methods=request_method,
@@ -868,7 +875,6 @@ class ViewsConfiguratorMixin(object):
))
view_intr.update(view_options)
introspectables.append(view_intr)
- predlist = self.get_predlist('view')
def register(permission=permission, renderer=renderer):
request_iface = IRequest
@@ -891,12 +897,54 @@ class ViewsConfiguratorMixin(object):
registry=self.registry
)
+ renderer_type = getattr(renderer, 'type', None)
+ intrspc = self.introspector
+ if (
+ renderer_type is not None and
+ tmpl_intr is not None and
+ intrspc is not None and
+ intrspc.get('renderer factories', renderer_type) is not None
+ ):
+ # allow failure of registered template factories to be deferred
+ # until view execution, like other bad renderer factories; if
+ # we tried to relate this to an existing renderer factory
+ # without checking if it the factory actually existed, we'd end
+ # up with a KeyError at startup time, which is inconsistent
+ # with how other bad renderer registrations behave (they throw
+ # a ValueError at view execution time)
+ tmpl_intr.relate('renderer factories', renderer.type)
+
+ # make a new view separately for normal and exception paths
+ if not exception_only:
+ derived_view = derive_view(False, renderer)
+ register_view(IViewClassifier, request_iface, derived_view)
+ if isexc:
+ derived_exc_view = derive_view(True, renderer)
+ register_view(IExceptionViewClassifier, request_iface,
+ derived_exc_view)
+
+ if exception_only:
+ derived_view = derived_exc_view
+
+ # if there are two derived views, combine them into one for
+ # introspection purposes
+ if not exception_only and isexc:
+ derived_view = runtime_exc_view(derived_view, derived_exc_view)
+
+ derived_view.__discriminator__ = lambda *arg: discriminator
+ # __discriminator__ is used by superdynamic systems
+ # that require it for introspection after manual view lookup;
+ # see also MultiView.__discriminator__
+ view_intr['derived_callable'] = derived_view
+
+ self.registry._clear_view_lookup_cache()
+
+ def derive_view(isexc_only, renderer):
# added by discrim_func above during conflict resolving
preds = view_intr['predicates']
order = view_intr['order']
phash = view_intr['phash']
- # __no_permission_required__ handled by _secure_view
derived_view = self._derive_view(
view,
route_name=route_name,
@@ -904,6 +952,7 @@ class ViewsConfiguratorMixin(object):
predicates=preds,
attr=attr,
context=context,
+ exception_only=isexc_only,
renderer=renderer,
wrapper_viewname=wrapper,
viewname=name,
@@ -916,14 +965,9 @@ class ViewsConfiguratorMixin(object):
require_csrf=require_csrf,
extra_options=ovals,
)
- derived_view.__discriminator__ = lambda *arg: discriminator
- # __discriminator__ is used by superdynamic systems
- # that require it for introspection after manual view lookup;
- # see also MultiView.__discriminator__
- view_intr['derived_callable'] = derived_view
-
- registered = self.registry.adapters.registered
+ return derived_view
+ def register_view(classifier, request_iface, derived_view):
# A multiviews is a set of views which are registered for
# exactly the same context type/request type/name triad. Each
# consituent view in a multiview differs only by the
@@ -943,32 +987,27 @@ class ViewsConfiguratorMixin(object):
# matches on all the arguments it receives.
old_view = None
+ order, phash = view_intr['order'], view_intr['phash']
+ registered = self.registry.adapters.registered
for view_type in (IView, ISecuredView, IMultiView):
- old_view = registered((IViewClassifier, request_iface,
- r_context), view_type, name)
+ old_view = registered(
+ (classifier, request_iface, r_context),
+ view_type, name)
if old_view is not None:
break
- isexc = isexception(context)
-
def regclosure():
if hasattr(derived_view, '__call_permissive__'):
view_iface = ISecuredView
else:
view_iface = IView
- if not exception_only:
- self.registry.registerAdapter(
- derived_view,
- (IViewClassifier, request_iface, context),
- view_iface,
- name
- )
- if isexc:
- self.registry.registerAdapter(
- derived_view,
- (IExceptionViewClassifier, request_iface, context),
- view_iface, name)
+ self.registry.registerAdapter(
+ derived_view,
+ (classifier, request_iface, context),
+ view_iface,
+ name
+ )
is_multiview = IMultiView.providedBy(old_view)
old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
@@ -1005,39 +1044,12 @@ class ViewsConfiguratorMixin(object):
for view_type in (IView, ISecuredView):
# unregister any existing views
self.registry.adapters.unregister(
- (IViewClassifier, request_iface, r_context),
+ (classifier, request_iface, r_context),
view_type, name=name)
- if isexc:
- self.registry.adapters.unregister(
- (IExceptionViewClassifier, request_iface,
- r_context), view_type, name=name)
self.registry.registerAdapter(
multiview,
- (IViewClassifier, request_iface, context),
+ (classifier, request_iface, context),
IMultiView, name=name)
- if isexc:
- self.registry.registerAdapter(
- multiview,
- (IExceptionViewClassifier, request_iface, context),
- IMultiView, name=name)
-
- self.registry._clear_view_lookup_cache()
- renderer_type = getattr(renderer, 'type', None) # gard against None
- intrspc = self.introspector
- if (
- renderer_type is not None and
- tmpl_intr is not None and
- intrspc is not None and
- intrspc.get('renderer factories', renderer_type) is not None
- ):
- # allow failure of registered template factories to be deferred
- # until view execution, like other bad renderer factories; if
- # we tried to relate this to an existing renderer factory
- # without checking if it the factory actually existed, we'd end
- # up with a KeyError at startup time, which is inconsistent
- # with how other bad renderer registrations behave (they throw
- # a ValueError at view execution time)
- tmpl_intr.relate('renderer factories', renderer.type)
if mapper:
mapper_intr = self.introspectable(
@@ -1351,7 +1363,8 @@ class ViewsConfiguratorMixin(object):
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH, decorator=None, route_name=None,
mapper=None, http_cache=None, context=None,
- require_csrf=None, extra_options=None):
+ require_csrf=None, exception_only=False,
+ extra_options=None):
view = self.maybe_dotted(view)
mapper = self.maybe_dotted(mapper)
if isinstance(renderer, string_types):
@@ -1389,6 +1402,7 @@ class ViewsConfiguratorMixin(object):
registry=self.registry,
package=self.package,
predicates=predicates,
+ exception_only=exception_only,
options=options,
)
@@ -1443,21 +1457,25 @@ class ViewsConfiguratorMixin(object):
argument restricts the set of circumstances under which this forbidden
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
- an exception if passed ``name``, ``permission``, ``context``,
- ``for_``, or ``http_cache`` keyword arguments. These argument values
- make no sense in the context of a forbidden view.
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_`` or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a forbidden
+ :term:`exception view`.
.. versionadded:: 1.3
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
"""
for arg in (
- 'name', 'permission', 'context', 'for_', 'http_cache',
- 'require_csrf',
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
):
if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_forbidden_view'
- % arg
- )
+ % (arg,))
if view is None:
view = default_exceptionresponse_view
@@ -1465,6 +1483,7 @@ class ViewsConfiguratorMixin(object):
settings = dict(
view=view,
context=HTTPForbidden,
+ exception_only=True,
wrapper=wrapper,
request_type=request_type,
request_method=request_method,
@@ -1513,9 +1532,9 @@ class ViewsConfiguratorMixin(object):
append_slash=False,
**view_options
):
- """ Add a default Not Found View to the current configuration state.
- The view will be called when Pyramid or application code raises an
- :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a
+ """ Add a default :term:`Not Found View` to the current configuration
+ state. The view will be called when Pyramid or application code raises
+ an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g. when a
view cannot be found for the request). The simplest example is:
.. code-block:: python
@@ -1533,9 +1552,9 @@ class ViewsConfiguratorMixin(object):
argument restricts the set of circumstances under which this notfound
view will be invoked. Unlike
:meth:`pyramid.config.Configurator.add_view`, this method will raise
- an exception if passed ``name``, ``permission``, ``context``,
- ``for_``, or ``http_cache`` keyword arguments. These argument values
- make no sense in the context of a Not Found View.
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_``, or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a Not Found View.
If ``append_slash`` is ``True``, when this Not Found View is invoked,
and the current path info does not end in a slash, the notfound logic
@@ -1562,18 +1581,22 @@ class ViewsConfiguratorMixin(object):
being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
be used` for the redirect response if a slash-appended route is found.
- .. versionchanged:: 1.6
.. versionadded:: 1.3
+
+ .. versionchanged:: 1.6
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
"""
for arg in (
- 'name', 'permission', 'context', 'for_', 'http_cache',
- 'require_csrf',
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
):
if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_notfound_view'
- % arg
- )
+ % (arg,))
if view is None:
view = default_exceptionresponse_view
@@ -1581,6 +1604,7 @@ class ViewsConfiguratorMixin(object):
settings = dict(
view=view,
context=HTTPNotFound,
+ exception_only=True,
wrapper=wrapper,
request_type=request_type,
request_method=request_method,
@@ -1621,64 +1645,40 @@ class ViewsConfiguratorMixin(object):
self,
view=None,
context=None,
- attr=None,
- renderer=None,
- wrapper=None,
- route_name=None,
- request_type=None,
- request_method=None,
- request_param=None,
- containment=None,
- xhr=None,
- accept=None,
- header=None,
- path_info=None,
- custom_predicates=(),
- decorator=None,
- mapper=None,
- match_param=None,
+ # force all other arguments to be specified as key=value
**view_options
- ):
- """ Add a view for an exception to the current configuration state.
- The view will be called when Pyramid or application code raises an
- the given exception.
+ ):
+ """ Add an :term:`exception view` for the specified ``exception`` to
+ the current configuration state. The view will be called when Pyramid
+ or application code raises the given exception.
+
+ This method accepts accepts almost all of the same arguments as
+ :meth:`pyramid.config.Configurator.add_view` except for ``name``,
+ ``permission``, ``for_``, ``require_csrf`` and ``exception_only``.
+
+ By default, this method will set ``context=Exception`` thus
+ registering for most default Python exceptions. Any subclass of
+ ``Exception`` may be specified.
.. versionadded:: 1.8
"""
for arg in (
- 'name', 'permission', 'for_', 'http_cache',
- 'require_csrf', 'exception_only',
+ 'name', 'for_', 'exception_only', 'require_csrf', 'permission',
):
if arg in view_options:
raise ConfigurationError(
'%s may not be used as an argument to add_exception_view'
- % arg
- )
+ % (arg,))
if context is None:
- raise ConfigurationError('context exception must be specified')
- settings = dict(
+ context = Exception
+ view_options.update(dict(
view=view,
context=context,
- wrapper=wrapper,
- renderer=renderer,
- request_type=request_type,
- request_method=request_method,
- request_param=request_param,
- containment=containment,
- xhr=xhr,
- accept=accept,
- header=header,
- path_info=path_info,
- custom_predicates=custom_predicates,
- decorator=decorator,
- mapper=mapper,
- match_param=match_param,
- route_name=route_name,
+ exception_only=True,
permission=NO_PERMISSION_REQUIRED,
require_csrf=False,
- exception_only=True,
- )
- return self.add_view(**settings)
+ ))
+ return self.add_view(**view_options)
@action_method
def set_view_mapper(self, mapper):
@@ -1859,14 +1859,63 @@ def isexception(o):
(inspect.isclass(o) and (issubclass(o, Exception)))
)
+def runtime_exc_view(view, excview):
+ # create a view callable which can pretend to be both a normal view
+ # and an exception view, dispatching to the appropriate one based
+ # on the state of request.exception
+ def wrapper_view(context, request):
+ if getattr(request, 'exception', None):
+ return excview(context, request)
+ return view(context, request)
+
+ # these constants are the same between the two views
+ wrapper_view.__wraps__ = wrapper_view
+ wrapper_view.__original_view__ = getattr(view, '__original_view__', view)
+ wrapper_view.__module__ = view.__module__
+ wrapper_view.__doc__ = view.__doc__
+ wrapper_view.__name__ = view.__name__
+
+ wrapper_view.__accept__ = getattr(view, '__accept__', None)
+ wrapper_view.__order__ = getattr(view, '__order__', MAX_ORDER)
+ wrapper_view.__phash__ = getattr(view, '__phash__', DEFAULT_PHASH)
+ wrapper_view.__view_attr__ = getattr(view, '__view_attr__', None)
+ wrapper_view.__permission__ = getattr(view, '__permission__', None)
+
+ def wrap_fn(attr):
+ def wrapper(context, request):
+ if getattr(request, 'exception', None):
+ selected_view = excview
+ else:
+ selected_view = view
+ fn = getattr(selected_view, attr, None)
+ if fn is not None:
+ return fn(context, request)
+ return wrapper
+
+ # these methods are dynamic per-request and should dispatch to their
+ # respective views based on whether it's an exception or not
+ wrapper_view.__call_permissive__ = wrap_fn('__call_permissive__')
+ wrapper_view.__permitted__ = wrap_fn('__permitted__')
+ wrapper_view.__predicated__ = wrap_fn('__predicated__')
+ wrapper_view.__predicates__ = wrap_fn('__predicates__')
+ return wrapper_view
+
@implementer(IViewDeriverInfo)
class ViewDeriverInfo(object):
- def __init__(self, view, registry, package, predicates, options):
+ def __init__(self,
+ view,
+ registry,
+ package,
+ predicates,
+ exception_only,
+ options,
+ ):
self.original_view = view
self.registry = registry
self.package = package
self.predicates = predicates or []
self.options = options or {}
+ self.exception_only = exception_only
@reify
def settings(self):
diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py
index a8a10f927..c95922eb0 100644
--- a/pyramid/exceptions.py
+++ b/pyramid/exceptions.py
@@ -109,6 +109,7 @@ class ConfigurationExecutionError(ConfigurationError):
def __str__(self):
return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info)
+
class CyclicDependencyError(Exception):
""" The exception raised when the Pyramid topological sorter detects a
cyclic dependency."""
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index b252d0f4a..114f802aa 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -1234,6 +1234,7 @@ class IViewDeriverInfo(Interface):
'default values that were not overriden')
predicates = Attribute('The list of predicates active on the view')
original_view = Attribute('The original view object being wrapped')
+ exception_only = Attribute('The view will only be invoked for exceptions')
class IViewDerivers(Interface):
""" Interface for view derivers list """
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 1adde9225..f020485de 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -20,15 +20,16 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config = Configurator(*arg, **kw)
return config
- def _getViewCallable(self, config, ctx_iface=None, request_iface=None,
- name='', exception_view=False):
+ def _getViewCallable(self, config, ctx_iface=None, exc_iface=None,
+ request_iface=None, name=''):
from zope.interface import Interface
from pyramid.interfaces import IRequest
from pyramid.interfaces import IView
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IExceptionViewClassifier
- if exception_view:
+ if exc_iface:
classifier = IExceptionViewClassifier
+ ctx_iface = exc_iface
else:
classifier = IViewClassifier
if ctx_iface is None:
@@ -489,7 +490,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, xhr=True, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -533,7 +534,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -581,7 +582,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=newview, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertFalse(IMultiView.providedBy(wrapper))
request = DummyRequest()
request.is_xhr = True
@@ -626,7 +627,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
@@ -669,7 +670,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
ISecuredView, name='')
config.add_view(view=view, context=RuntimeError, renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
@@ -755,7 +756,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2, accept='text/html', context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(len(wrapper.views), 1)
self.assertEqual(len(wrapper.media_views), 1)
@@ -816,7 +817,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_view(view=view2, context=RuntimeError,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError))
self.assertTrue(IMultiView.providedBy(wrapper))
self.assertEqual(len(wrapper.views), 1)
self.assertEqual(len(wrapper.media_views), 1)
@@ -843,31 +844,71 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
self.assertEqual(wrapper(None, None), 'OK1')
- def test_add_view_exc_multiview_replaces_multiview(self):
+ def test_add_view_exc_multiview_replaces_multiviews(self):
from pyramid.renderers import null_renderer
from zope.interface import implementedBy
from pyramid.interfaces import IRequest
from pyramid.interfaces import IMultiView
from pyramid.interfaces import IViewClassifier
from pyramid.interfaces import IExceptionViewClassifier
- view = DummyMultiView()
+ hot_view = DummyMultiView()
+ exc_view = DummyMultiView()
config = self._makeOne(autocommit=True)
config.registry.registerAdapter(
- view,
+ hot_view,
(IViewClassifier, IRequest, implementedBy(RuntimeError)),
IMultiView, name='')
config.registry.registerAdapter(
- view,
+ exc_view,
(IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
IMultiView, name='')
view2 = lambda *arg: 'OK2'
config.add_view(view=view2, context=RuntimeError,
renderer=null_renderer)
- wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError), exception_view=True)
- self.assertTrue(IMultiView.providedBy(wrapper))
- self.assertEqual([x[:2] for x in wrapper.views], [(view2, None)])
- self.assertEqual(wrapper(None, None), 'OK1')
+ hot_wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(hot_wrapper))
+ self.assertEqual([x[:2] for x in hot_wrapper.views], [(view2, None)])
+ self.assertEqual(hot_wrapper(None, None), 'OK1')
+
+ exc_wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(exc_wrapper))
+ self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)])
+ self.assertEqual(exc_wrapper(None, None), 'OK1')
+
+ def test_add_view_exc_multiview_replaces_only_exc_multiview(self):
+ from pyramid.renderers import null_renderer
+ from zope.interface import implementedBy
+ from pyramid.interfaces import IRequest
+ from pyramid.interfaces import IMultiView
+ from pyramid.interfaces import IViewClassifier
+ from pyramid.interfaces import IExceptionViewClassifier
+ hot_view = DummyMultiView()
+ exc_view = DummyMultiView()
+ config = self._makeOne(autocommit=True)
+ config.registry.registerAdapter(
+ hot_view,
+ (IViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ config.registry.registerAdapter(
+ exc_view,
+ (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)),
+ IMultiView, name='')
+ view2 = lambda *arg: 'OK2'
+ config.add_view(view=view2, context=RuntimeError, exception_only=True,
+ renderer=null_renderer)
+ hot_wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(hot_wrapper))
+ self.assertEqual(len(hot_wrapper.views), 0)
+ self.assertEqual(hot_wrapper(None, None), 'OK1')
+
+ exc_wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(RuntimeError))
+ self.assertTrue(IMultiView.providedBy(exc_wrapper))
+ self.assertEqual([x[:2] for x in exc_wrapper.views], [(view2, None)])
+ self.assertEqual(exc_wrapper(None, None), 'OK1')
def test_add_view_multiview_context_superclass_then_subclass(self):
from pyramid.renderers import null_renderer
@@ -886,10 +927,12 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.registry.registerAdapter(
view, (IViewClassifier, IRequest, ISuper), IView, name='')
config.add_view(view=view2, for_=ISub, renderer=null_renderer)
- wrapper = self._getViewCallable(config, ISuper, IRequest)
+ wrapper = self._getViewCallable(config, ctx_iface=ISuper,
+ request_iface=IRequest)
self.assertFalse(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK')
- wrapper = self._getViewCallable(config, ISub, IRequest)
+ wrapper = self._getViewCallable(config, ctx_iface=ISub,
+ request_iface=IRequest)
self.assertFalse(IMultiView.providedBy(wrapper))
self.assertEqual(wrapper(None, None), 'OK2')
@@ -914,16 +957,16 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view, (IExceptionViewClassifier, IRequest, Super), IView, name='')
config.add_view(view=view2, for_=Sub, renderer=null_renderer)
wrapper = self._getViewCallable(
- config, implementedBy(Super), IRequest)
+ config, ctx_iface=implementedBy(Super), request_iface=IRequest)
wrapper_exc_view = self._getViewCallable(
- config, implementedBy(Super), IRequest, exception_view=True)
+ config, exc_iface=implementedBy(Super), request_iface=IRequest)
self.assertEqual(wrapper_exc_view, wrapper)
self.assertFalse(IMultiView.providedBy(wrapper_exc_view))
self.assertEqual(wrapper_exc_view(None, None), 'OK')
wrapper = self._getViewCallable(
- config, implementedBy(Sub), IRequest)
+ config, ctx_iface=implementedBy(Sub), request_iface=IRequest)
wrapper_exc_view = self._getViewCallable(
- config, implementedBy(Sub), IRequest, exception_view=True)
+ config, exc_iface=implementedBy(Sub), request_iface=IRequest)
self.assertEqual(wrapper_exc_view, wrapper)
self.assertFalse(IMultiView.providedBy(wrapper_exc_view))
self.assertEqual(wrapper_exc_view(None, None), 'OK2')
@@ -1233,8 +1276,8 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer=null_renderer)
request_iface = self._getRouteRequestIface(config, 'foo')
wrapper_exc_view = self._getViewCallable(
- config, ctx_iface=implementedBy(RuntimeError),
- request_iface=request_iface, exception_view=True)
+ config, exc_iface=implementedBy(RuntimeError),
+ request_iface=request_iface)
self.assertNotEqual(wrapper_exc_view, None)
wrapper = self._getViewCallable(
config, ctx_iface=implementedBy(RuntimeError),
@@ -1820,8 +1863,8 @@ class TestViewsConfigurationMixin(unittest.TestCase):
from pyramid.renderers import null_renderer
view1 = lambda *arg: 'OK'
config = self._makeOne(autocommit=True)
- config.add_view(view=view1, context=Exception, renderer=null_renderer,
- exception_only=True)
+ config.add_view(view=view1, context=Exception, exception_only=True,
+ renderer=null_renderer)
view = self._getViewCallable(config, ctx_iface=implementedBy(Exception))
self.assertTrue(view is None)
@@ -1830,11 +1873,10 @@ class TestViewsConfigurationMixin(unittest.TestCase):
from pyramid.renderers import null_renderer
view1 = lambda *arg: 'OK'
config = self._makeOne(autocommit=True)
- config.add_view(view=view1, context=Exception, renderer=null_renderer,
- exception_only=True)
+ config.add_view(view=view1, context=Exception, exception_only=True,
+ renderer=null_renderer)
view = self._getViewCallable(
- config, ctx_iface=implementedBy(Exception), exception_view=True
- )
+ config, exc_iface=implementedBy(Exception))
self.assertEqual(view1, view)
def test_add_view_exception_only_misconfiguration(self):
@@ -1844,23 +1886,33 @@ class TestViewsConfigurationMixin(unittest.TestCase):
pass
self.assertRaises(
ConfigurationError,
- config.add_view, view, context=NotAnException, exception_only=True
- )
+ config.add_view, view, context=NotAnException, exception_only=True)
def test_add_exception_view(self):
from zope.interface import implementedBy
- from pyramid.interfaces import IRequest
from pyramid.renderers import null_renderer
view1 = lambda *arg: 'OK'
config = self._makeOne(autocommit=True)
- config.add_exception_view(view=view1, context=Exception, renderer=null_renderer)
+ config.add_exception_view(view=view1, renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(Exception), exception_view=True,
- )
+ config, exc_iface=implementedBy(Exception))
context = Exception()
request = self._makeRequest(config)
self.assertEqual(wrapper(context, request), 'OK')
+ def test_add_exception_view_with_subclass(self):
+ from zope.interface import implementedBy
+ from pyramid.renderers import null_renderer
+ view1 = lambda *arg: 'OK'
+ config = self._makeOne(autocommit=True)
+ config.add_exception_view(view=view1, context=ValueError,
+ renderer=null_renderer)
+ wrapper = self._getViewCallable(
+ config, exc_iface=implementedBy(ValueError))
+ context = ValueError()
+ request = self._makeRequest(config)
+ self.assertEqual(wrapper(context, request), 'OK')
+
def test_add_exception_view_disallows_name(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
@@ -1875,19 +1927,19 @@ class TestViewsConfigurationMixin(unittest.TestCase):
context=Exception(),
permission='foo')
- def test_add_exception_view_disallows_for_(self):
+ def test_add_exception_view_disallows_require_csrf(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
config.add_exception_view,
context=Exception(),
- for_='foo')
+ require_csrf=True)
- def test_add_exception_view_disallows_http_cache(self):
+ def test_add_exception_view_disallows_for_(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
config.add_exception_view,
context=Exception(),
- http_cache='foo')
+ for_='foo')
def test_add_exception_view_disallows_exception_only(self):
config = self._makeOne(autocommit=True)
@@ -1896,21 +1948,14 @@ class TestViewsConfigurationMixin(unittest.TestCase):
context=Exception(),
exception_only=True)
- def test_add_exception_view_requires_context(self):
- config = self._makeOne(autocommit=True)
- view = lambda *a: 'OK'
- self.assertRaises(ConfigurationError,
- config.add_exception_view, view=view)
-
def test_add_exception_view_with_view_defaults(self):
from pyramid.renderers import null_renderer
from pyramid.exceptions import PredicateMismatch
- from pyramid.httpexceptions import HTTPNotFound
from zope.interface import directlyProvides
from zope.interface import implementedBy
class view(object):
__view_defaults__ = {
- 'containment':'pyramid.tests.test_config.IDummy'
+ 'containment': 'pyramid.tests.test_config.IDummy'
}
def __init__(self, request):
pass
@@ -1922,7 +1967,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
context=Exception,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(Exception), exception_view=True)
+ config, exc_iface=implementedBy(Exception))
context = DummyContext()
directlyProvides(context, IDummy)
request = self._makeRequest(config)
@@ -2043,7 +2088,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_forbidden_view(view, renderer=null_renderer)
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
result = view(None, request)
self.assertEqual(result, 'OK')
@@ -2057,7 +2102,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_forbidden_view()
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
context = HTTPForbidden()
result = view(context, request)
@@ -2080,6 +2125,11 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_forbidden_view, permission='foo')
+ def test_add_forbidden_view_disallows_require_csrf(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_forbidden_view, require_csrf=True)
+
def test_add_forbidden_view_disallows_context(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
@@ -2090,11 +2140,6 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_forbidden_view, for_='foo')
- def test_add_forbidden_view_disallows_http_cache(self):
- config = self._makeOne(autocommit=True)
- self.assertRaises(ConfigurationError,
- config.add_forbidden_view, http_cache='foo')
-
def test_add_forbidden_view_with_view_defaults(self):
from pyramid.interfaces import IRequest
from pyramid.renderers import null_renderer
@@ -2115,7 +2160,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view=view,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(HTTPForbidden),
+ config, exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
context = DummyContext()
directlyProvides(context, IDummy)
@@ -2135,7 +2180,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_notfound_view(view, renderer=null_renderer)
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertEqual(result, (None, request))
@@ -2149,7 +2194,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
config.add_notfound_view()
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
context = HTTPNotFound()
result = view(context, request)
@@ -2172,6 +2217,11 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_notfound_view, permission='foo')
+ def test_add_notfound_view_disallows_require_csrf(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_notfound_view, require_csrf=True)
+
def test_add_notfound_view_disallows_context(self):
config = self._makeOne(autocommit=True)
self.assertRaises(ConfigurationError,
@@ -2182,11 +2232,6 @@ class TestViewsConfigurationMixin(unittest.TestCase):
self.assertRaises(ConfigurationError,
config.add_notfound_view, for_='foo')
- def test_add_notfound_view_disallows_http_cache(self):
- config = self._makeOne(autocommit=True)
- self.assertRaises(ConfigurationError,
- config.add_notfound_view, http_cache='foo')
-
def test_add_notfound_view_append_slash(self):
from pyramid.response import Response
from pyramid.renderers import null_renderer
@@ -2202,7 +2247,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.query_string = 'a=1&b=2'
request.path = '/scriptname/foo'
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertTrue(isinstance(result, HTTPFound))
@@ -2225,7 +2270,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
request.query_string = 'a=1&b=2'
request.path = '/scriptname/foo'
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self.assertTrue(isinstance(result, HTTPMovedPermanently))
@@ -2251,7 +2296,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
view=view,
renderer=null_renderer)
wrapper = self._getViewCallable(
- config, ctx_iface=implementedBy(HTTPNotFound),
+ config, exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
context = DummyContext()
directlyProvides(context, IDummy)
@@ -2281,7 +2326,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer='json')
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPNotFound),
+ exc_iface=implementedBy(HTTPNotFound),
request_iface=IRequest)
result = view(None, request)
self._assertBody(result, '{}')
@@ -2298,7 +2343,7 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer='json')
request = self._makeRequest(config)
view = self._getViewCallable(config,
- ctx_iface=implementedBy(HTTPForbidden),
+ exc_iface=implementedBy(HTTPForbidden),
request_iface=IRequest)
result = view(None, request)
self._assertBody(result, '{}')
@@ -2319,6 +2364,75 @@ class TestViewsConfigurationMixin(unittest.TestCase):
from pyramid.tests import test_config
self.assertEqual(result, test_config)
+ def test_add_normal_and_exception_view_intr_derived_callable(self):
+ from pyramid.renderers import null_renderer
+ from pyramid.exceptions import BadCSRFToken
+ config = self._makeOne(autocommit=True)
+ introspector = DummyIntrospector()
+ config.introspector = introspector
+ view = lambda r: 'OK'
+ config.set_default_csrf_options(require_csrf=True)
+ config.add_view(view, context=Exception, renderer=null_renderer)
+ view_intr = introspector.introspectables[1]
+ self.assertTrue(view_intr.type_name, 'view')
+ self.assertEqual(view_intr['callable'], view)
+ derived_view = view_intr['derived_callable']
+
+ request = self._makeRequest(config)
+ request.method = 'POST'
+ request.scheme = 'http'
+ request.POST = {}
+ request.headers = {}
+ request.session = DummySession({'csrf_token': 'foo'})
+ self.assertRaises(BadCSRFToken, lambda: derived_view(None, request))
+ request.exception = Exception()
+ self.assertEqual(derived_view(None, request), 'OK')
+
+class Test_runtime_exc_view(unittest.TestCase):
+ def _makeOne(self, view1, view2):
+ from pyramid.config.views import runtime_exc_view
+ return runtime_exc_view(view1, view2)
+
+ def test_call(self):
+ def view1(context, request): return 'OK'
+ def view2(context, request): raise AssertionError
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ result = result_view(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_call_dispatches_on_exception(self):
+ def view1(context, request): raise AssertionError
+ def view2(context, request): return 'OK'
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ request.exception = Exception()
+ result = result_view(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_permitted(self):
+ def errfn(context, request): raise AssertionError
+ def view1(context, request): raise AssertionError
+ view1.__permitted__ = lambda c, r: 'OK'
+ def view2(context, request): raise AssertionError
+ view2.__permitted__ = errfn
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ result = result_view.__permitted__(None, request)
+ self.assertEqual(result, 'OK')
+
+ def test_permitted_dispatches_on_exception(self):
+ def errfn(context, request): raise AssertionError
+ def view1(context, request): raise AssertionError
+ view1.__permitted__ = errfn
+ def view2(context, request): raise AssertionError
+ view2.__permitted__ = lambda c, r: 'OK'
+ result_view = self._makeOne(view1, view2)
+ request = DummyRequest()
+ request.exception = Exception()
+ result = result_view.__permitted__(None, request)
+ self.assertEqual(result, 'OK')
+
class Test_requestonly(unittest.TestCase):
def _callFUT(self, view, attr=None):
from pyramid.config.views import requestonly
diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py
index 993209046..9cb0f58d1 100644
--- a/pyramid/tests/test_exceptions.py
+++ b/pyramid/tests/test_exceptions.py
@@ -90,5 +90,3 @@ class TestCyclicDependencyError(unittest.TestCase):
result = str(exc)
self.assertTrue("'a' sorts before ['c', 'd']" in result)
self.assertTrue("'c' sorts before ['a']" in result)
-
-
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index d18c6eca4..cab42cf48 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -134,15 +134,24 @@ class Test_forbidden_view_config(BaseTest, unittest.TestCase):
self.assertEqual(settings[0]['_info'], 'codeinfo')
class Test_exception_view_config(BaseTest, unittest.TestCase):
- def _makeOne(self, **kw):
+ def _makeOne(self, *args, **kw):
from pyramid.view import exception_view_config
- return exception_view_config(**kw)
+ return exception_view_config(*args, **kw)
def test_ctor(self):
inst = self._makeOne(context=Exception, path_info='path_info')
self.assertEqual(inst.__dict__,
{'context':Exception, 'path_info':'path_info'})
+ def test_ctor_positional_exception(self):
+ inst = self._makeOne(Exception, path_info='path_info')
+ self.assertEqual(inst.__dict__,
+ {'context':Exception, 'path_info':'path_info'})
+
+ def test_ctor_positional_extras(self):
+ from pyramid.exceptions import ConfigurationError
+ self.assertRaises(ConfigurationError, lambda: self._makeOne(Exception, True))
+
def test_it_function(self):
def view(request): pass
decorator = self._makeOne(context=Exception, renderer='renderer')
diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py
index 79fcd6e71..676c6f66a 100644
--- a/pyramid/tests/test_viewderivers.py
+++ b/pyramid/tests/test_viewderivers.py
@@ -551,6 +551,28 @@ class TestDeriveView(unittest.TestCase):
"'view_name' against context None): "
"Allowed (NO_PERMISSION_REQUIRED)")
+ def test_debug_auth_permission_authpol_permitted_excview(self):
+ response = DummyResponse()
+ view = lambda *arg: response
+ self.config.registry.settings = dict(
+ debug_authorization=True, reload_templates=True)
+ logger = self._registerLogger()
+ self._registerSecurityPolicy(True)
+ result = self.config._derive_view(
+ view, context=Exception, permission='view')
+ self.assertEqual(view.__module__, result.__module__)
+ self.assertEqual(view.__doc__, result.__doc__)
+ self.assertEqual(view.__name__, result.__name__)
+ self.assertEqual(result.__call_permissive__.__wraps__, view)
+ request = self._makeRequest()
+ request.view_name = 'view_name'
+ request.url = 'url'
+ self.assertEqual(result(Exception(), request), response)
+ self.assertEqual(len(logger.messages), 1)
+ self.assertEqual(logger.messages[0],
+ "debug_authorization of url url (view name "
+ "'view_name' against context Exception()): True")
+
def test_secured_view_authn_policy_no_authz_policy(self):
response = DummyResponse()
view = lambda *arg: response
diff --git a/pyramid/view.py b/pyramid/view.py
index 1895de96d..2af42b1e7 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -17,7 +17,10 @@ from pyramid.interfaces import (
from pyramid.compat import decode_path_info
-from pyramid.exceptions import PredicateMismatch
+from pyramid.exceptions import (
+ ConfigurationError,
+ PredicateMismatch,
+)
from pyramid.httpexceptions import (
HTTPFound,
@@ -166,7 +169,7 @@ class view_config(object):
:class:`pyramid.view.bfg_view`.
:class:`pyramid.view.view_config` supports the following keyword
- arguments: ``context``, ``permission``, ``name``,
+ arguments: ``context``, ``exception``, ``permission``, ``name``,
``request_type``, ``route_name``, ``request_method``, ``request_param``,
``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
@@ -325,7 +328,8 @@ class notfound_view_config(object):
.. versionadded:: 1.3
An analogue of :class:`pyramid.view.view_config` which registers a
- :term:`Not Found View`.
+ :term:`Not Found View` using
+ :meth:`pyramid.config.Configurator.add_notfound_view`.
The ``notfound_view_config`` constructor accepts most of the same arguments
as the constructor of :class:`pyramid.view.view_config`. It can be used
@@ -413,7 +417,8 @@ class forbidden_view_config(object):
.. versionadded:: 1.3
An analogue of :class:`pyramid.view.view_config` which registers a
- :term:`forbidden view`.
+ :term:`forbidden view` using
+ :meth:`pyramid.config.Configurator.add_forbidden_view`.
The forbidden_view_config constructor accepts most of the same arguments
as the constructor of :class:`pyramid.view.view_config`. It can be used
@@ -468,13 +473,15 @@ class exception_view_config(object):
.. versionadded:: 1.8
An analogue of :class:`pyramid.view.view_config` which registers an
- exception view.
+ :term:`exception view` using
+ :meth:`pyramid.config.Configurator.add_exception_view`.
- The exception_view_config constructor requires an exception context, and
- additionally accepts most of the same arguments as the constructor of
+ The ``exception_view_config`` constructor requires an exception context,
+ and additionally accepts most of the same arguments as the constructor of
:class:`pyramid.view.view_config`. It can be used in the same places,
- and behaves in largely the same way, except it always registers an exception
- view instead of a 'normal' view.
+ and behaves in largely the same way, except it always registers an
+ exception view instead of a 'normal' view that dispatches on the request
+ :term:`context`.
Example:
@@ -483,17 +490,23 @@ class exception_view_config(object):
from pyramid.view import exception_view_config
from pyramid.response import Response
- @exception_view_config(context=ValueError, renderer='json')
- def error_view(context, request):
- return {'error': str(context)}
+ @exception_view_config(ValueError, renderer='json')
+ def error_view(request):
+ return {'error': str(request.exception)}
All arguments passed to this function have the same meaning as
:meth:`pyramid.view.view_config` and each predicate argument restricts
the set of circumstances under which this exception view will be invoked.
+
"""
venusian = venusian
- def __init__(self, **settings):
+ def __init__(self, *args, **settings):
+ if 'context' not in settings and len(args) > 0:
+ exception, args = args[0], args[1:]
+ settings['context'] = exception
+ if len(args) > 0:
+ raise ConfigurationError('unknown positional arguments')
self.__dict__.update(settings)
def __call__(self, wrapped):
diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py
index 5d138a02a..513ddf022 100644
--- a/pyramid/viewderivers.py
+++ b/pyramid/viewderivers.py
@@ -286,18 +286,16 @@ def _secured_view(view, info):
authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
if authn_policy and authz_policy and (permission is not None):
- def _permitted(context, request):
+ def permitted(context, request):
principals = authn_policy.effective_principals(request)
return authz_policy.permits(context, principals, permission)
- def _secured_view(context, request):
- if (
- getattr(request, 'exception', None) is not None and
- explicit_val is None
- ):
- return view(context, request)
-
- result = _permitted(context, request)
+ def secured_view(context, request):
+ result = permitted(context, request)
if result:
return view(context, request)
view_name = getattr(view, '__name__', view)
@@ -305,10 +303,10 @@ def _secured_view(view, info):
request, 'authdebug_message',
'Unauthorized: %s failed permission check' % view_name)
raise HTTPForbidden(msg, result=result)
- _secured_view.__call_permissive__ = view
- _secured_view.__permitted__ = _permitted
- _secured_view.__permission__ = permission
- wrapped_view = _secured_view
+ wrapped_view = secured_view
+ wrapped_view.__call_permissive__ = view
+ wrapped_view.__permitted__ = permitted
+ wrapped_view.__permission__ = permission
return wrapped_view
@@ -321,14 +319,13 @@ def _authdebug_view(view, info):
authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
logger = info.registry.queryUtility(IDebugLogger)
- if settings and settings.get('debug_authorization', False):
- def _authdebug_view(context, request):
- if (
- getattr(request, 'exception', None) is not None and
- explicit_val is None
- ):
- return view(context, request)
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
+ if settings and settings.get('debug_authorization', False):
+ def authdebug_view(context, request):
view_name = getattr(request, 'view_name', None)
if authn_policy and authz_policy:
@@ -352,8 +349,7 @@ def _authdebug_view(view, info):
if request is not None:
request.authdebug_message = msg
return view(context, request)
-
- wrapped_view = _authdebug_view
+ wrapped_view = authdebug_view
return wrapped_view
@@ -490,23 +486,22 @@ def csrf_view(view, info):
token = defaults.token
header = defaults.header
safe_methods = defaults.safe_methods
+
enabled = (
explicit_val is True or
- (explicit_val is not False and default_val)
+ # fallback to the default val if not explicitly enabled
+ # but only if the view is not an exception view
+ (
+ explicit_val is not False and default_val and
+ not info.exception_only
+ )
)
# disable if both header and token are disabled
enabled = enabled and (token or header)
wrapped_view = view
if enabled:
def csrf_view(context, request):
- if (
- request.method not in safe_methods and
- (
- # skip exception views unless value is explicitly defined
- getattr(request, 'exception', None) is None or
- explicit_val is not None
- )
- ):
+ if request.method not in safe_methods:
check_csrf_origin(request, raises=True)
check_csrf_token(request, token, header, raises=True)
return view(context, request)