diff options
| -rw-r--r-- | pyramid/config/views.py | 90 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 116 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 46 | ||||
| -rw-r--r-- | pyramid/view.py | 52 |
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, |
