summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyramid/config/views.py90
-rw-r--r--pyramid/tests/test_config/test_views.py116
-rw-r--r--pyramid/tests/test_view.py46
-rw-r--r--pyramid/view.py52
4 files changed, 298 insertions, 6 deletions
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 5cb3f5099..e341922d3 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -213,6 +213,7 @@ class ViewsConfiguratorMixin(object):
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
@@ -701,6 +702,14 @@ 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(
@@ -759,6 +768,11 @@ 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_
@@ -943,10 +957,13 @@ class ViewsConfiguratorMixin(object):
view_iface = ISecuredView
else:
view_iface = IView
- self.registry.registerAdapter(
- derived_view,
- (IViewClassifier, request_iface, context), view_iface, name
- )
+ if not exception_only:
+ self.registry.registerAdapter(
+ derived_view,
+ (IViewClassifier, request_iface, context),
+ view_iface,
+ name
+ )
if isexc:
self.registry.registerAdapter(
derived_view,
@@ -1598,6 +1615,71 @@ class ViewsConfiguratorMixin(object):
set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias
+ @viewdefaults
+ @action_method
+ def add_exception_view(
+ 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,
+ **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.
+
+ .. versionadded:: 1.8
+ """
+ for arg in (
+ 'name', 'permission', 'for_', 'http_cache',
+ 'require_csrf', 'exception_only',
+ ):
+ if arg in view_options:
+ raise ConfigurationError(
+ '%s may not be used as an argument to add_exception_view'
+ % arg
+ )
+ if context is None:
+ raise ConfigurationError('context exception must be specified')
+ settings = 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,
+ permission=NO_PERMISSION_REQUIRED,
+ require_csrf=False,
+ exception_only=True,
+ )
+ return self.add_view(**settings)
+
@action_method
def set_view_mapper(self, mapper):
"""
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index 878574e88..1adde9225 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1815,6 +1815,122 @@ class TestViewsConfigurationMixin(unittest.TestCase):
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, renderer=null_renderer,
+ exception_only=True)
+ 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, renderer=null_renderer,
+ exception_only=True)
+ view = self._getViewCallable(
+ config, ctx_iface=implementedBy(Exception), exception_view=True
+ )
+ 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.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)
+ wrapper = self._getViewCallable(
+ config, ctx_iface=implementedBy(Exception), exception_view=True,
+ )
+ context = Exception()
+ 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_for_(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ for_='foo')
+
+ def test_add_exception_view_disallows_http_cache(self):
+ config = self._makeOne(autocommit=True)
+ self.assertRaises(ConfigurationError,
+ config.add_exception_view,
+ context=Exception(),
+ http_cache='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_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'
+ }
+ 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, ctx_iface=implementedBy(Exception), exception_view=True)
+ 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):
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index 2de44d579..d18c6eca4 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -132,7 +132,49 @@ class Test_forbidden_view_config(BaseTest, unittest.TestCase):
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, **kw):
+ from pyramid.view import exception_view_config
+ return exception_view_config(**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_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 +940,7 @@ class DummyConfig(object):
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
diff --git a/pyramid/view.py b/pyramid/view.py
index 88c6397af..1895de96d 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -463,6 +463,58 @@ class forbidden_view_config(object):
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
+ 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.
+
+ Example:
+
+ .. code-block:: python
+
+ 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)}
+
+ 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):
+ 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,