Michael Merickel
2016-09-29 4939e35e878e0ac155f1bf9f33721c9b3ce2d423
Merge branch 'exception_only'
12 files modified
892 ■■■■ changed files
docs/narr/hooks.rst 23 ●●●●● patch | view | raw | blame | history
docs/narr/viewconfig.rst 19 ●●●●● patch | view | raw | blame | history
docs/narr/views.rst 40 ●●●●● patch | view | raw | blame | history
pyramid/config/views.py 275 ●●●● patch | view | raw | blame | history
pyramid/exceptions.py 1 ●●●● patch | view | raw | blame | history
pyramid/interfaces.py 1 ●●●● patch | view | raw | blame | history
pyramid/tests/test_config/test_views.py 324 ●●●● patch | view | raw | blame | history
pyramid/tests/test_exceptions.py 2 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_view.py 55 ●●●●● patch | view | raw | blame | history
pyramid/tests/test_viewderivers.py 22 ●●●●● patch | view | raw | blame | history
pyramid/view.py 73 ●●●●● patch | view | raw | blame | history
pyramid/viewderivers.py 57 ●●●● patch | view | raw | blame | history
docs/narr/hooks.rst
@@ -1654,7 +1654,8 @@
  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.
@@ -1664,7 +1665,8 @@
  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``
@@ -1710,6 +1712,8 @@
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:
@@ -1760,6 +1764,21 @@
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
~~~~~~~~~~~~~~~~~~~~~~
docs/narr/viewconfig.rst
@@ -34,7 +34,7 @@
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 @@
  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.
docs/narr/views.rst
@@ -262,10 +262,16 @@
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 @@
       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 @@
   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 @@
.. 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::
pyramid/config/views.py
@@ -9,12 +9,11 @@
    implementedBy,
    implementer,
    )
from zope.interface.interfaces import IInterface
from pyramid.interfaces import (
    IException,
    IExceptionViewClassifier,
    IException,
    IMultiView,
    IPackageOverrides,
    IRendererFactory,
@@ -213,6 +212,7 @@
        match_param=None,
        check_csrf=None,
        require_csrf=None,
        exception_only=False,
        **view_options):
        """ Add a :term:`view configuration` to the current
        configuration state.  Arguments to ``add_view`` are broken
@@ -502,7 +502,20 @@
          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
@@ -684,7 +697,7 @@
                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
@@ -762,6 +775,12 @@
        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
@@ -797,6 +816,7 @@
            # 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 = {}
@@ -835,6 +855,7 @@
        view_intr.update(dict(
            name=name,
            context=context,
            exception_only=exception_only,
            containment=containment,
            request_param=request_param,
            request_methods=request_method,
@@ -854,7 +875,6 @@
        ))
        view_intr.update(view_options)
        introspectables.append(view_intr)
        predlist = self.get_predlist('view')
        def register(permission=permission, renderer=renderer):
            request_iface = IRequest
@@ -877,12 +897,54 @@
                        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,
@@ -890,6 +952,7 @@
                predicates=preds,
                attr=attr,
                context=context,
                exception_only=isexc_only,
                renderer=renderer,
                wrapper_viewname=wrapper,
                viewname=name,
@@ -902,14 +965,9 @@
                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
            return derived_view
            registered = self.registry.adapters.registered
        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
@@ -929,14 +987,15 @@
            # 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__'):
@@ -945,13 +1004,10 @@
                    view_iface = IView
                self.registry.registerAdapter(
                    derived_view,
                    (IViewClassifier, request_iface, context), view_iface, name
                    (classifier, request_iface, context),
                    view_iface,
                    name
                    )
                if isexc:
                    self.registry.registerAdapter(
                        derived_view,
                        (IExceptionViewClassifier, request_iface, context),
                        view_iface, name)
            is_multiview = IMultiView.providedBy(old_view)
            old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
@@ -988,39 +1044,12 @@
                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(
@@ -1334,7 +1363,8 @@
                     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):
@@ -1372,6 +1402,7 @@
            registry=self.registry,
            package=self.package,
            predicates=predicates,
            exception_only=exception_only,
            options=options,
        )
@@ -1426,21 +1457,25 @@
        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
@@ -1448,6 +1483,7 @@
        settings = dict(
            view=view,
            context=HTTPForbidden,
            exception_only=True,
            wrapper=wrapper,
            request_type=request_type,
            request_method=request_method,
@@ -1496,9 +1532,9 @@
        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
@@ -1516,9 +1552,9 @@
        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
@@ -1545,22 +1581,26 @@
        being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
        be used` for the redirect response if a slash-appended route is found.
        .. versionadded:: 1.3
        .. versionchanged:: 1.6
           The ``append_slash`` argument was modified to allow any object that
           implements the ``IResponse`` interface to specify the response class
           used when a redirect is performed.
        .. 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_notfound_view'
                    % arg
                    )
                    % (arg,))
        if view is None:
            view = default_exceptionresponse_view
@@ -1568,6 +1608,7 @@
        settings = dict(
            view=view,
            context=HTTPNotFound,
            exception_only=True,
            wrapper=wrapper,
            request_type=request_type,
            request_method=request_method,
@@ -1601,6 +1642,47 @@
        return self.add_view(**settings)
    set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias
    @viewdefaults
    @action_method
    def add_exception_view(
        self,
        view=None,
        context=None,
        # force all other arguments to be specified as key=value
        **view_options
        ):
        """ 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', '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,))
        if context is None:
            context = Exception
        view_options.update(dict(
            view=view,
            context=context,
            exception_only=True,
            permission=NO_PERMISSION_REQUIRED,
            require_csrf=False,
        ))
        return self.add_view(**view_options)
    @action_method
    def set_view_mapper(self, mapper):
@@ -1781,14 +1863,63 @@
        (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):
pyramid/exceptions.py
@@ -109,6 +109,7 @@
    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."""
pyramid/interfaces.py
@@ -1234,6 +1234,7 @@
                        '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 """
pyramid/tests/test_config/test_views.py
@@ -20,15 +20,16 @@
        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 @@
        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 @@
        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 @@
        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 @@
        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 @@
            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 @@
        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 @@
        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 @@
        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 @@
        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 @@
            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 @@
                        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),
@@ -1815,6 +1858,124 @@
        self.assertRaises(ConfigurationError, configure_view)
    def test_add_view_exception_only_no_regular_view(self):
        from zope.interface import implementedBy
        from pyramid.renderers import null_renderer
        view1 = lambda *arg: 'OK'
        config = self._makeOne(autocommit=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)
    def test_add_view_exception_only(self):
        from zope.interface import implementedBy
        from pyramid.renderers import null_renderer
        view1 = lambda *arg: 'OK'
        config = self._makeOne(autocommit=True)
        config.add_view(view=view1, context=Exception, exception_only=True,
                        renderer=null_renderer)
        view = self._getViewCallable(
            config, exc_iface=implementedBy(Exception))
        self.assertEqual(view1, view)
    def test_add_view_exception_only_misconfiguration(self):
        view = lambda *arg: 'OK'
        config = self._makeOne(autocommit=True)
        class NotAnException(object):
            pass
        self.assertRaises(
            ConfigurationError,
            config.add_view, view, context=NotAnException, exception_only=True)
    def test_add_exception_view(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, renderer=null_renderer)
        wrapper = self._getViewCallable(
            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,
                          config.add_exception_view,
                          context=Exception(),
                          name='foo')
    def test_add_exception_view_disallows_permission(self):
        config = self._makeOne(autocommit=True)
        self.assertRaises(ConfigurationError,
                          config.add_exception_view,
                          context=Exception(),
                          permission='foo')
    def test_add_exception_view_disallows_require_csrf(self):
        config = self._makeOne(autocommit=True)
        self.assertRaises(ConfigurationError,
                          config.add_exception_view,
                          context=Exception(),
                          require_csrf=True)
    def test_add_exception_view_disallows_for_(self):
        config = self._makeOne(autocommit=True)
        self.assertRaises(ConfigurationError,
                          config.add_exception_view,
                          context=Exception(),
                          for_='foo')
    def test_add_exception_view_disallows_exception_only(self):
        config = self._makeOne(autocommit=True)
        self.assertRaises(ConfigurationError,
                          config.add_exception_view,
                          context=Exception(),
                          exception_only=True)
    def test_add_exception_view_with_view_defaults(self):
        from pyramid.renderers import null_renderer
        from pyramid.exceptions import PredicateMismatch
        from zope.interface import directlyProvides
        from zope.interface import implementedBy
        class view(object):
            __view_defaults__ = {
                'containment': 'pyramid.tests.test_config.IDummy'
                }
            def __init__(self, request):
                pass
            def __call__(self):
                return 'OK'
        config = self._makeOne(autocommit=True)
        config.add_exception_view(
            view=view,
            context=Exception,
            renderer=null_renderer)
        wrapper = self._getViewCallable(
            config, exc_iface=implementedBy(Exception))
        context = DummyContext()
        directlyProvides(context, IDummy)
        request = self._makeRequest(config)
        self.assertEqual(wrapper(context, request), 'OK')
        context = DummyContext()
        request = self._makeRequest(config)
        self.assertRaises(PredicateMismatch, wrapper, context, request)
    def test_derive_view_function(self):
        from pyramid.renderers import null_renderer
        def view(request):
@@ -1927,7 +2088,7 @@
        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')
@@ -1941,7 +2102,7 @@
        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)
@@ -1964,6 +2125,11 @@
        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,
@@ -1973,11 +2139,6 @@
        config = self._makeOne(autocommit=True)
        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
@@ -1999,7 +2160,7 @@
            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)
@@ -2019,7 +2180,7 @@
        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))
@@ -2033,7 +2194,7 @@
        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)
@@ -2056,6 +2217,11 @@
        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,
@@ -2065,11 +2231,6 @@
        config = self._makeOne(autocommit=True)
        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
@@ -2086,7 +2247,7 @@
        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))
@@ -2109,7 +2270,7 @@
        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))
@@ -2135,7 +2296,7 @@
            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)
@@ -2165,7 +2326,7 @@
            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, '{}')
@@ -2182,7 +2343,7 @@
            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, '{}')
@@ -2203,6 +2364,75 @@
        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
pyramid/tests/test_exceptions.py
@@ -90,5 +90,3 @@
        result = str(exc)
        self.assertTrue("'a' sorts before ['c', 'd']" in result)
        self.assertTrue("'c' sorts before ['a']" in result)
pyramid/tests/test_view.py
@@ -132,7 +132,58 @@
        self.assertEqual(settings[0]['view'], None) # comes from call_venusian
        self.assertEqual(settings[0]['attr'], 'view')
        self.assertEqual(settings[0]['_info'], 'codeinfo')
class Test_exception_view_config(BaseTest, unittest.TestCase):
    def _makeOne(self, *args, **kw):
        from pyramid.view import exception_view_config
        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')
        venusian = DummyVenusian()
        decorator.venusian = venusian
        wrapped = decorator(view)
        self.assertTrue(wrapped is view)
        config = call_venusian(venusian)
        settings = config.settings
        self.assertEqual(
            settings,
            [{'venusian': venusian, 'context': Exception,
              'renderer': 'renderer', '_info': 'codeinfo', 'view': None}]
            )
    def test_it_class(self):
        decorator = self._makeOne()
        venusian = DummyVenusian()
        decorator.venusian = venusian
        decorator.venusian.info.scope = 'class'
        class view(object): pass
        wrapped = decorator(view)
        self.assertTrue(wrapped is view)
        config = call_venusian(venusian)
        settings = config.settings
        self.assertEqual(len(settings), 1)
        self.assertEqual(len(settings[0]), 4)
        self.assertEqual(settings[0]['venusian'], venusian)
        self.assertEqual(settings[0]['view'], None) # comes from call_venusian
        self.assertEqual(settings[0]['attr'], 'view')
        self.assertEqual(settings[0]['_info'], 'codeinfo')
class RenderViewToResponseTests(BaseTest, unittest.TestCase):
    def _callFUT(self, *arg, **kw):
        from pyramid.view import render_view_to_response
@@ -898,7 +949,7 @@
    def add_view(self, **kw):
        self.settings.append(kw)
    add_notfound_view = add_forbidden_view = add_view
    add_notfound_view = add_forbidden_view = add_exception_view = add_view
    def with_package(self, pkg):
        self.pkg = pkg
pyramid/tests/test_viewderivers.py
@@ -551,6 +551,28 @@
                         "'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
pyramid/view.py
@@ -17,7 +17,10 @@
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:`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 @@
    .. 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 @@
    .. 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
@@ -463,6 +468,66 @@
        settings['_info'] = info.codeinfo # fbo "action_method"
        return wrapped
class exception_view_config(object):
    """
    .. versionadded:: 1.8
    An analogue of :class:`pyramid.view.view_config` which registers an
    :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
    :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 that dispatches on the request
    :term:`context`.
    Example:
    .. code-block:: python
        from pyramid.view import exception_view_config
        from pyramid.response import Response
        @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, *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):
        settings = self.__dict__.copy()
        def callback(context, name, ob):
            config = context.config.with_package(info.module)
            config.add_exception_view(view=ob, **settings)
        info = self.venusian.attach(wrapped, callback, category='pyramid')
        if info.scope == 'class':
            # if the decorator was attached to a method in a class, or
            # otherwise executed at class scope, we need to set an
            # 'attr' into the settings if one isn't already in there
            if settings.get('attr') is None:
                settings['attr'] = wrapped.__name__
        settings['_info'] = info.codeinfo # fbo "action_method"
        return wrapped
def _find_views(
    registry,
    request_iface,
pyramid/viewderivers.py
@@ -286,18 +286,16 @@
    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 @@
                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 @@
    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 @@
            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 @@
        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)